| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.cts.deviceandprofileowner; |
| |
| import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT; |
| import static android.app.admin.DevicePolicyManager.PASSWORD_COMPLEXITY_HIGH; |
| import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX; |
| import static android.app.admin.SecurityLog.LEVEL_ERROR; |
| import static android.app.admin.SecurityLog.LEVEL_INFO; |
| import static android.app.admin.SecurityLog.LEVEL_WARNING; |
| import static android.app.admin.SecurityLog.TAG_ADB_SHELL_CMD; |
| import static android.app.admin.SecurityLog.TAG_ADB_SHELL_INTERACTIVE; |
| import static android.app.admin.SecurityLog.TAG_APP_PROCESS_START; |
| import static android.app.admin.SecurityLog.TAG_CAMERA_POLICY_SET; |
| import static android.app.admin.SecurityLog.TAG_CERT_AUTHORITY_INSTALLED; |
| import static android.app.admin.SecurityLog.TAG_CERT_AUTHORITY_REMOVED; |
| import static android.app.admin.SecurityLog.TAG_CERT_VALIDATION_FAILURE; |
| import static android.app.admin.SecurityLog.TAG_CRYPTO_SELF_TEST_COMPLETED; |
| import static android.app.admin.SecurityLog.TAG_KEYGUARD_DISABLED_FEATURES_SET; |
| import static android.app.admin.SecurityLog.TAG_KEYGUARD_DISMISSED; |
| import static android.app.admin.SecurityLog.TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT; |
| import static android.app.admin.SecurityLog.TAG_KEYGUARD_SECURED; |
| import static android.app.admin.SecurityLog.TAG_KEY_DESTRUCTION; |
| import static android.app.admin.SecurityLog.TAG_KEY_GENERATED; |
| import static android.app.admin.SecurityLog.TAG_KEY_IMPORT; |
| import static android.app.admin.SecurityLog.TAG_KEY_INTEGRITY_VIOLATION; |
| import static android.app.admin.SecurityLog.TAG_LOGGING_STARTED; |
| import static android.app.admin.SecurityLog.TAG_LOGGING_STOPPED; |
| import static android.app.admin.SecurityLog.TAG_LOG_BUFFER_SIZE_CRITICAL; |
| import static android.app.admin.SecurityLog.TAG_MAX_PASSWORD_ATTEMPTS_SET; |
| import static android.app.admin.SecurityLog.TAG_MAX_SCREEN_LOCK_TIMEOUT_SET; |
| import static android.app.admin.SecurityLog.TAG_MEDIA_MOUNT; |
| import static android.app.admin.SecurityLog.TAG_MEDIA_UNMOUNT; |
| import static android.app.admin.SecurityLog.TAG_OS_SHUTDOWN; |
| import static android.app.admin.SecurityLog.TAG_OS_STARTUP; |
| import static android.app.admin.SecurityLog.TAG_PASSWORD_COMPLEXITY_REQUIRED; |
| import static android.app.admin.SecurityLog.TAG_PASSWORD_COMPLEXITY_SET; |
| import static android.app.admin.SecurityLog.TAG_PASSWORD_EXPIRATION_SET; |
| import static android.app.admin.SecurityLog.TAG_PASSWORD_HISTORY_LENGTH_SET; |
| import static android.app.admin.SecurityLog.TAG_REMOTE_LOCK; |
| import static android.app.admin.SecurityLog.TAG_SYNC_RECV_FILE; |
| import static android.app.admin.SecurityLog.TAG_SYNC_SEND_FILE; |
| import static android.app.admin.SecurityLog.TAG_USER_RESTRICTION_ADDED; |
| import static android.app.admin.SecurityLog.TAG_USER_RESTRICTION_REMOVED; |
| import static android.app.admin.SecurityLog.TAG_WIPE_FAILURE; |
| |
| import static com.android.cts.devicepolicy.TestCertificates.TEST_CA; |
| import static com.android.cts.devicepolicy.TestCertificates.TEST_CA_SUBJECT; |
| |
| import static com.google.common.collect.ImmutableList.of; |
| import static com.google.common.truth.Truth.assertThat; |
| |
| import android.app.admin.DevicePolicyManager; |
| import android.app.admin.SecurityLog; |
| import android.app.admin.SecurityLog.SecurityEvent; |
| import android.content.Context; |
| import android.content.SharedPreferences; |
| import android.os.Parcel; |
| import android.os.Process; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.security.keystore.KeyGenParameterSpec; |
| import android.security.keystore.KeyProperties; |
| import android.security.keystore.KeyProtection; |
| import android.support.test.uiautomator.UiDevice; |
| import android.text.TextUtils; |
| import android.util.DebugUtils; |
| import android.util.Log; |
| |
| import androidx.test.InstrumentationRegistry; |
| |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| |
| import junit.framework.AssertionFailedError; |
| |
| import java.security.KeyPair; |
| import java.security.KeyPairGenerator; |
| import java.security.KeyStore; |
| import java.util.Arrays; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Predicate; |
| import java.util.stream.Collectors; |
| |
| import javax.crypto.spec.SecretKeySpec; |
| |
| public class SecurityLoggingTest extends BaseDeviceAdminTest { |
| private static final String TAG = "SecurityLoggingTest"; |
| private static final String ARG_BATCH_NUMBER = "batchNumber"; |
| private static final String PREF_KEY_PREFIX = "batch-last-id-"; |
| private static final String PREF_NAME = "batchIds"; |
| // system/core/liblog/event.logtags: 1006 liblog (dropped|1) |
| private static final int TAG_LIBLOG_DROPPED = 1006; |
| private static final String DELEGATE_APP_PKG = "com.android.cts.delegate"; |
| private static final String DELEGATION_SECURITY_LOGGING = "delegation-security-logging"; |
| private static final boolean VERBOSE = false; |
| |
| // For brevity. |
| private static final Class<String> S = String.class; |
| private static final Class<Long> L = Long.class; |
| private static final Class<Integer> I = Integer.class; |
| |
| private static final Map<Integer, List<Class>> PAYLOAD_TYPES_MAP = |
| new ImmutableMap.Builder<Integer, List<Class>>() |
| .put(TAG_ADB_SHELL_INTERACTIVE, of()) |
| .put(TAG_ADB_SHELL_CMD, of(S)) |
| .put(TAG_SYNC_RECV_FILE, of(S)) |
| .put(TAG_SYNC_SEND_FILE, of(S)) |
| .put(TAG_APP_PROCESS_START, of(S, L, I, I, S, S)) |
| .put(TAG_KEYGUARD_DISMISSED, of()) |
| .put(TAG_KEYGUARD_DISMISS_AUTH_ATTEMPT, of(I, I)) |
| .put(TAG_KEYGUARD_SECURED, of()) |
| .put(TAG_OS_STARTUP, of(S, S)) |
| .put(TAG_OS_SHUTDOWN, of()) |
| .put(TAG_LOGGING_STARTED, of()) |
| .put(TAG_LOGGING_STOPPED, of()) |
| .put(TAG_MEDIA_MOUNT, of(S, S)) |
| .put(TAG_MEDIA_UNMOUNT, of(S, S)) |
| .put(TAG_LOG_BUFFER_SIZE_CRITICAL, of()) |
| .put(TAG_PASSWORD_EXPIRATION_SET, of(S, I, I, L)) |
| .put(TAG_PASSWORD_COMPLEXITY_SET, of(S, I, I, I, I, I, I, I, I, I, I)) |
| .put(TAG_PASSWORD_HISTORY_LENGTH_SET, of(S, I, I, I)) |
| .put(TAG_MAX_SCREEN_LOCK_TIMEOUT_SET, of(S, I, I, L)) |
| .put(TAG_MAX_PASSWORD_ATTEMPTS_SET, of(S, I, I, I)) |
| .put(TAG_KEYGUARD_DISABLED_FEATURES_SET, of(S, I, I, I)) |
| .put(TAG_REMOTE_LOCK, of(S, I, I)) |
| .put(TAG_WIPE_FAILURE, of()) |
| .put(TAG_KEY_GENERATED, of(I, S, I)) |
| .put(TAG_KEY_IMPORT, of(I, S, I)) |
| .put(TAG_KEY_DESTRUCTION, of(I, S, I)) |
| .put(TAG_CERT_AUTHORITY_INSTALLED, of(I, S, I)) |
| .put(TAG_CERT_AUTHORITY_REMOVED, of(I, S, I)) |
| .put(TAG_USER_RESTRICTION_ADDED, of(S, I, S)) |
| .put(TAG_USER_RESTRICTION_REMOVED, of(S, I, S)) |
| .put(TAG_CRYPTO_SELF_TEST_COMPLETED, of(I)) |
| .put(TAG_KEY_INTEGRITY_VIOLATION, of(S, I)) |
| .put(TAG_CERT_VALIDATION_FAILURE, of(S)) |
| .put(TAG_CAMERA_POLICY_SET, of(S, I, I, I)) |
| .put(TAG_PASSWORD_COMPLEXITY_REQUIRED, of(S, I, I, I)) |
| .build(); |
| |
| private static final String GENERATED_KEY_ALIAS = "generated_key_alias"; |
| private static final String IMPORTED_KEY_ALIAS = "imported_key_alias"; |
| |
| // Indices of various fields in event payload. |
| private static final int SUCCESS_INDEX = 0; |
| private static final int ALIAS_INDEX = 1; |
| private static final int UID_INDEX = 2; |
| private static final int USERID_INDEX = 2; |
| private static final int SUBJECT_INDEX = 1; |
| private static final int ADMIN_PKG_INDEX = 0; |
| private static final int ADMIN_USER_INDEX = 1; |
| private static final int TARGET_USER_INDEX = 2; |
| private static final int PWD_LEN_INDEX = 3; |
| private static final int PWD_QUALITY_INDEX = 4; |
| private static final int LETTERS_INDEX = 5; |
| private static final int NON_LETTERS_INDEX = 6; |
| private static final int NUMERIC_INDEX = 7; |
| private static final int UPPERCASE_INDEX = 8; |
| private static final int LOWERCASE_INDEX = 9; |
| private static final int SYMBOLS_INDEX = 10; |
| private static final int PWD_EXPIRATION_INDEX = 3; |
| private static final int PWD_HIST_LEN_INDEX = 3; |
| private static final int USER_RESTRICTION_INDEX = 2; |
| private static final int MAX_PWD_ATTEMPTS_INDEX = 3; |
| private static final int KEYGUARD_FEATURES_INDEX = 3; |
| private static final int MAX_SCREEN_TIMEOUT_INDEX = 3; |
| private static final int CAMERA_DISABLED_INDEX = 3; |
| |
| // Value that indicates success in events that have corresponding field in their payload. |
| private static final int SUCCESS_VALUE = 1; |
| |
| private static final int TEST_PWD_LENGTH = 10; |
| // Min number of various character types to use. |
| private static final int TEST_PWD_CHARS = 2; |
| |
| private static final long TEST_PWD_EXPIRATION_TIMEOUT = TimeUnit.DAYS.toMillis(356); |
| private static final int TEST_PWD_HISTORY_LENGTH = 3; |
| private static final int TEST_PWD_MAX_ATTEMPTS = 5; |
| private static final long TEST_MAX_TIME_TO_LOCK = TimeUnit.HOURS.toMillis(1); |
| |
| /** |
| * Test: retrieving security logs can only be done if there's one user on the device or all |
| * secondary users / profiles are affiliated. |
| */ |
| public void testRetrievingSecurityLogsThrowsSecurityException() { |
| try { |
| mDevicePolicyManager.retrieveSecurityLogs(ADMIN_RECEIVER_COMPONENT); |
| fail("did not throw expected SecurityException"); |
| } catch (SecurityException expected) { |
| } |
| } |
| |
| /** |
| * Test: retrieving previous security logs can only be done if there's one user on the device or |
| * all secondary users / profiles are affiliated. |
| */ |
| public void testRetrievingPreviousSecurityLogsThrowsSecurityException() { |
| try { |
| mDevicePolicyManager.retrievePreRebootSecurityLogs(ADMIN_RECEIVER_COMPONENT); |
| fail("did not throw expected SecurityException"); |
| } catch (SecurityException expected) { |
| } |
| } |
| |
| /** |
| * Test: retrieves security logs and verifies that all events generated as a result of host |
| * side actions and by {@link #testGenerateLogs()} are there. |
| */ |
| public void testVerifyGeneratedLogs() throws Exception { |
| forceSecurityLogs(); |
| |
| final List<SecurityEvent> events = getEvents(); |
| |
| verifyAutomaticEventsPresent(events); |
| verifyKeystoreEventsPresent(events); |
| verifyKeyChainEventsPresent(events); |
| verifyAdminEventsPresent(events); |
| verifyAdbShellCommand(events); // Event generated from host side logic |
| if (mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) { |
| verifyEventsRedacted(events); |
| } |
| } |
| |
| private void forceSecurityLogs() throws Exception { |
| UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) |
| .executeShellCommand("dpm force-security-logs"); |
| } |
| |
| private void verifyAutomaticEventsPresent(List<SecurityEvent> events) { |
| verifyOsStartupEventPresent(events); |
| verifyLoggingStartedEventPresent(events); |
| verifyCryptoSelfTestEventPresent(events); |
| } |
| |
| private void verifyKeyChainEventsPresent(List<SecurityEvent> events) { |
| verifyCertInstalledEventPresent(events); |
| verifyCertUninstalledEventPresent(events); |
| } |
| |
| private void verifyKeystoreEventsPresent(List<SecurityEvent> events) { |
| verifyKeyGeneratedEventPresent(events, GENERATED_KEY_ALIAS); |
| verifyKeyDeletedEventPresent(events, GENERATED_KEY_ALIAS); |
| verifyKeyImportedEventPresent(events, IMPORTED_KEY_ALIAS); |
| verifyKeyDeletedEventPresent(events, IMPORTED_KEY_ALIAS); |
| } |
| |
| private void verifyAdminEventsPresent(List<SecurityEvent> events) { |
| if (mHasSecureLockScreen) { |
| verifyPasswordComplexityEventsPresent(events); |
| verifyNewStylePasswordComplexityEventPresent(events); |
| } |
| verifyLockingPolicyEventsPresent(events); |
| verifyUserRestrictionEventsPresent(events); |
| verifyCameraPolicyEvents(events); |
| } |
| private void verifyAdbShellCommand(List<SecurityEvent> events) { |
| // Won't be able to locate the command on org-owned devices, as it will be redacted. |
| if (!mDevicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile()) { |
| findEvent("adb command", events, |
| e -> e.getTag() == TAG_ADB_SHELL_CMD && |
| e.getData().equals("whoami")); |
| |
| } |
| } |
| |
| private void verifyEventsRedacted(List<SecurityEvent> events) { |
| final int userId = Process.myUserHandle().getIdentifier(); |
| for (SecurityEvent event : events) { |
| switch (event.getTag()) { |
| case TAG_ADB_SHELL_CMD: |
| assertTrue(TextUtils.isEmpty((String) event.getData())); |
| break; |
| case TAG_APP_PROCESS_START: |
| case TAG_KEY_GENERATED: |
| case TAG_KEY_IMPORT: |
| case TAG_KEY_DESTRUCTION: |
| assertEquals(userId, UserHandle.getUserId(getInt(event, UID_INDEX))); |
| break; |
| case TAG_CERT_AUTHORITY_INSTALLED: |
| case TAG_CERT_AUTHORITY_REMOVED: |
| assertEquals(userId, getInt(event, USERID_INDEX)); |
| break; |
| case TAG_KEY_INTEGRITY_VIOLATION: |
| assertEquals(userId, UserHandle.getUserId(getInt(event, 1))); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Generates events for positive test cases. |
| */ |
| public void testGenerateLogs() throws Exception { |
| generateKeystoreEvents(); |
| generateKeyChainEvents(); |
| generateAdminEvents(); |
| } |
| |
| private void generateKeyChainEvents() { |
| installCaCert(); |
| uninstallCaCert(); |
| } |
| |
| private void generateKeystoreEvents() throws Exception { |
| generateKey(GENERATED_KEY_ALIAS); |
| deleteKey(GENERATED_KEY_ALIAS); |
| importKey(IMPORTED_KEY_ALIAS); |
| deleteKey(IMPORTED_KEY_ALIAS); |
| } |
| |
| private void generateAdminEvents() { |
| if (mHasSecureLockScreen) { |
| generatePasswordComplexityEvents(); |
| generateNewStylePasswordComplexityEvents(); |
| } |
| generateLockingPolicyEvents(); |
| generateUserRestrictionEvents(); |
| generateCameraPolicyEvents(); |
| } |
| |
| /** |
| * Fetches and checks the events. |
| */ |
| private List<SecurityEvent> getEvents() throws Exception { |
| List<SecurityEvent> events = null; |
| // Retry once after seeping for 1 second, in case "dpm force-security-logs" hasn't taken |
| // effect just yet. |
| for (int i = 0; i < 2 && events == null; i++) { |
| events = mDevicePolicyManager.retrieveSecurityLogs(ADMIN_RECEIVER_COMPONENT); |
| Log.v(TAG, "getEvents(), batch #" + i + ": " + (events == null ? -1 : events.size()) |
| + " events"); |
| if (events == null) sleep(1000); |
| } |
| |
| Log.d(TAG, "getEvents(): received " + events.size() + " events"); |
| if (VERBOSE) dumpSecurityLogs(events); |
| |
| try { |
| verifySecurityLogs(events); |
| } catch (AssertionFailedError e) { |
| dumpSecurityLogs(events); |
| throw e; |
| } |
| |
| return events; |
| } |
| |
| /** |
| * Test: check that there are no gaps between ids in two consecutive batches. Shared preference |
| * is used to store these numbers between test invocations. |
| */ |
| public void testVerifyLogIds() throws Exception { |
| forceSecurityLogs(); |
| final String param = InstrumentationRegistry.getArguments().getString(ARG_BATCH_NUMBER); |
| final int batchId = param == null ? 0 : Integer.parseInt(param); |
| final List<SecurityEvent> events = getEvents(); |
| final SharedPreferences prefs = |
| mContext.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); |
| |
| final long firstId = events.get(0).getId(); |
| if (batchId == 0) { |
| assertEquals("Event id wasn't reset.", 0L, firstId); |
| } else { |
| final String prevBatchLastIdKey = PREF_KEY_PREFIX + (batchId - 1); |
| assertTrue("Last event id from previous batch not found in shared prefs", |
| prefs.contains(prevBatchLastIdKey)); |
| final long prevBatchLastId = prefs.getLong(prevBatchLastIdKey, 0); |
| assertEquals("Event ids aren't consecutive between batches", |
| firstId, prevBatchLastId + 1); |
| } |
| |
| final String currBatchLastIdKey = PREF_KEY_PREFIX + batchId; |
| final long lastId = events.get(events.size() - 1).getId(); |
| prefs.edit().putLong(currBatchLastIdKey, lastId).commit(); |
| } |
| |
| private void verifySecurityLogs(List<SecurityEvent> events) { |
| assertTrue("Unable to get events", events != null && events.size() > 0); |
| |
| // We don't know much about the events, so just call public API methods. |
| for (int i = 0; i < events.size(); i++) { |
| final SecurityEvent event = events.get(i); |
| |
| // Skip liblog dropped event. |
| if (event.getTag() == TAG_LIBLOG_DROPPED) { |
| continue; |
| } |
| |
| verifyPayloadTypes(event); |
| |
| // Test id for monotonically increasing. |
| if (i > 0) { |
| assertEquals("Event IDs are not monotonically increasing within the batch", |
| events.get(i - 1).getId() + 1, event.getId()); |
| } |
| |
| // Test parcelling: flatten to a parcel. |
| Parcel p = Parcel.obtain(); |
| event.writeToParcel(p, 0); |
| p.setDataPosition(0); |
| |
| // Restore from parcel and check contents. |
| final SecurityEvent restored = SecurityEvent.CREATOR.createFromParcel(p); |
| p.recycle(); |
| |
| final int level = event.getLogLevel(); |
| assertTrue(level == LEVEL_INFO || level == LEVEL_WARNING || level == LEVEL_ERROR); |
| |
| // For some events data is encapsulated into Object array. |
| if (event.getData() instanceof Object[]) { |
| assertTrue("Parcelling changed the array returned by getData", |
| Arrays.equals((Object[]) event.getData(), (Object[]) restored.getData())); |
| } else { |
| assertEquals("Parcelling changed the result of getData", |
| event.getData(), restored.getData()); |
| } |
| assertEquals("Parcelling changed the result of getId", |
| event.getId(), restored.getId()); |
| assertEquals("Parcelling changed the result of getTag", |
| event.getTag(), restored.getTag()); |
| assertEquals("Parcelling changed the result of getTimeNanos", |
| event.getTimeNanos(), restored.getTimeNanos()); |
| assertEquals("Parcelling changed the result of describeContents", |
| event.describeContents(), restored.describeContents()); |
| } |
| } |
| |
| private void verifyPayloadTypes(SecurityEvent event) { |
| final List<Class> payloadTypes = PAYLOAD_TYPES_MAP.get(event.getTag()); |
| assertNotNull("event type unknown: " + event.getTag(), payloadTypes); |
| |
| if (payloadTypes.size() == 0) { |
| // No payload. |
| assertNull("non-null payload", event.getData()); |
| } else if (payloadTypes.size() == 1) { |
| // Singleton payload. |
| assertTrue(payloadTypes.get(0).isInstance(event.getData())); |
| } else { |
| // Payload is incapsulated into Object[] |
| assertTrue(event.getData() instanceof Object[]); |
| final Object[] dataArray = (Object[]) event.getData(); |
| assertEquals(payloadTypes.size(), dataArray.length); |
| for (int i = 0; i < payloadTypes.size(); i++) { |
| assertTrue(payloadTypes.get(i).isInstance(dataArray[i])); |
| } |
| } |
| } |
| |
| private void verifyOsStartupEventPresent(List<SecurityEvent> events) { |
| final SecurityEvent event = findEvent("os startup", events, TAG_OS_STARTUP); |
| // Verified boot state, empty if running on emulator |
| assertOneOf(ImmutableSet.of("", "green", "yellow", "orange"), getString(event, 0)); |
| // dm-verity mode, empty if it is disabled |
| assertOneOf(ImmutableSet.of("", "enforcing", "eio", "disabled"), getString(event, 1)); |
| } |
| |
| private void assertOneOf(Set<String> allowed, String s) { |
| assertTrue(String.format("\"%s\" is not one of [%s]", s, String.join(", ", allowed)), |
| allowed.contains(s)); |
| } |
| |
| private void verifyCryptoSelfTestEventPresent(List<SecurityEvent> events) { |
| final SecurityEvent event = findEvent("crypto self test complete", |
| events, TAG_CRYPTO_SELF_TEST_COMPLETED); |
| // Success code. |
| assertEquals(1, getInt(event)); |
| } |
| |
| private void verifyLoggingStartedEventPresent(List<SecurityEvent> events) { |
| findEvent("logging started", events, TAG_LOGGING_STARTED); |
| } |
| |
| private SecurityEvent findEvent(String description, List<SecurityEvent> events, int tag) { |
| return findEvent(description, events, e -> e.getTag() == tag); |
| } |
| |
| private List<SecurityEvent> findEvents(List<SecurityEvent> events, |
| Predicate<SecurityEvent> predicate) { |
| return events.stream().filter(predicate).collect(Collectors.toList()); |
| } |
| |
| private SecurityEvent findEvent(String description, List<SecurityEvent> events, |
| Predicate<SecurityEvent> predicate) { |
| final List<SecurityEvent> matches = findEvents(events, predicate); |
| assertEquals("Invalid number of matching events: " + description, 1, matches.size()); |
| return matches.get(0); |
| } |
| |
| private void assertNumberEvents(String description, List<SecurityEvent> events, |
| Predicate<SecurityEvent> predicate, int expectedSize) { |
| assertEquals("Invalid number of matching events: " + description, expectedSize, |
| findEvents(events, predicate).size()); |
| } |
| |
| private static Object getDatum(SecurityEvent event, int index) { |
| final Object[] dataArray = (Object[]) event.getData(); |
| return dataArray[index]; |
| } |
| |
| private static String getString(SecurityEvent event, int index) { |
| return (String) getDatum(event, index); |
| } |
| |
| private static int getInt(SecurityEvent event) { |
| return (Integer) event.getData(); |
| } |
| |
| private static int getInt(SecurityEvent event, int index) { |
| return (Integer) getDatum(event, index); |
| } |
| |
| private static long getLong(SecurityEvent event, int index) { |
| return (Long) getDatum(event, index); |
| } |
| |
| /** |
| * Test: Test enabling security logging. This test should be executed after installing a device |
| * owner so that we check that logging is not enabled by default. This test has a side effect: |
| * security logging is enabled after its execution. |
| */ |
| public void testEnablingSecurityLogging() { |
| assertFalse(mDevicePolicyManager.isSecurityLoggingEnabled(ADMIN_RECEIVER_COMPONENT)); |
| mDevicePolicyManager.setSecurityLoggingEnabled(ADMIN_RECEIVER_COMPONENT, true); |
| assertTrue(mDevicePolicyManager.isSecurityLoggingEnabled(ADMIN_RECEIVER_COMPONENT)); |
| } |
| |
| /** |
| * Test: Test disabling security logging. This test has a side effect: security logging is |
| * disabled after its execution. |
| */ |
| public void testDisablingSecurityLogging() { |
| mDevicePolicyManager.setSecurityLoggingEnabled(ADMIN_RECEIVER_COMPONENT, false); |
| assertFalse(mDevicePolicyManager.isSecurityLoggingEnabled(ADMIN_RECEIVER_COMPONENT)); |
| |
| // Verify that logs are actually not available. |
| assertNull(mDevicePolicyManager.retrieveSecurityLogs(ADMIN_RECEIVER_COMPONENT)); |
| } |
| |
| /** |
| * Test: retrieving security logs should be rate limited - subsequent attempts should return |
| * null. |
| */ |
| public void testSecurityLoggingRetrievalRateLimited() { |
| final List<SecurityEvent> logs = mDevicePolicyManager.retrieveSecurityLogs( |
| ADMIN_RECEIVER_COMPONENT); |
| // if logs is null it means that that attempt was rate limited => test PASS |
| if (logs != null) { |
| assertNull(mDevicePolicyManager.retrieveSecurityLogs(ADMIN_RECEIVER_COMPONENT)); |
| assertNull(mDevicePolicyManager.retrieveSecurityLogs(ADMIN_RECEIVER_COMPONENT)); |
| } |
| } |
| |
| public void testSetDelegateScope_delegationSecurityLogging() { |
| setDelegatedScopes(DELEGATE_APP_PKG, Arrays.asList(DELEGATION_SECURITY_LOGGING)); |
| |
| assertThat(mDevicePolicyManager.getDelegatedScopes( |
| ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG)).contains(DELEGATION_SECURITY_LOGGING); |
| } |
| |
| public void testSetDelegateScope_noDelegation() { |
| setDelegatedScopes(DELEGATE_APP_PKG, Arrays.asList()); |
| |
| assertThat(mDevicePolicyManager.getDelegatedScopes( |
| ADMIN_RECEIVER_COMPONENT, DELEGATE_APP_PKG)) |
| .doesNotContain(DELEGATION_SECURITY_LOGGING); |
| } |
| |
| private void generateKey(String keyAlias) throws Exception { |
| final KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore"); |
| generator.initialize( |
| new KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_SIGN).build()); |
| final KeyPair keyPair = generator.generateKeyPair(); |
| assertNotNull(keyPair); |
| } |
| |
| private void verifyKeyGeneratedEventPresent(List<SecurityEvent> events, String alias) { |
| findEvent("key generated", events, |
| e -> e.getTag() == TAG_KEY_GENERATED |
| && getInt(e, SUCCESS_INDEX) == SUCCESS_VALUE |
| && getString(e, ALIAS_INDEX).contains(alias) |
| && getInt(e, UID_INDEX) == Process.myUid()); |
| } |
| |
| private void importKey(String alias) throws Exception{ |
| final KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); |
| ks.load(null); |
| ks.setEntry(alias, new KeyStore.SecretKeyEntry(new SecretKeySpec(new byte[32], "AES")), |
| new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT).build()); |
| } |
| |
| private void verifyKeyImportedEventPresent(List<SecurityEvent> events, String alias) { |
| findEvent("key imported", events, |
| e -> e.getTag() == TAG_KEY_IMPORT |
| && getInt(e, SUCCESS_INDEX) == SUCCESS_VALUE |
| && getString(e, ALIAS_INDEX).contains(alias) |
| && getInt(e, UID_INDEX) == Process.myUid()); |
| } |
| |
| private void deleteKey(String keyAlias) throws Exception { |
| final KeyStore ks = KeyStore.getInstance("AndroidKeyStore"); |
| ks.load(null); |
| ks.deleteEntry(keyAlias); |
| } |
| |
| private void verifyKeyDeletedEventPresent(List<SecurityEvent> events, String alias) { |
| findEvent("key deleted", events, |
| e -> e.getTag() == TAG_KEY_DESTRUCTION |
| && getInt(e, SUCCESS_INDEX) == SUCCESS_VALUE |
| && getString(e, ALIAS_INDEX).contains(alias) |
| && getInt(e, UID_INDEX) == Process.myUid()); |
| } |
| |
| private void installCaCert() { |
| assertTrue( |
| mDevicePolicyManager.installCaCert(ADMIN_RECEIVER_COMPONENT, TEST_CA.getBytes())); |
| } |
| |
| private void verifyCertInstalledEventPresent(List<SecurityEvent> events) { |
| findEvent("cert authority installed", events, |
| e -> e.getTag() == TAG_CERT_AUTHORITY_INSTALLED |
| && getInt(e, SUCCESS_INDEX) == SUCCESS_VALUE |
| && getString(e, SUBJECT_INDEX).equals(TEST_CA_SUBJECT)); |
| } |
| |
| private void uninstallCaCert() { |
| mDevicePolicyManager.uninstallCaCert(ADMIN_RECEIVER_COMPONENT, TEST_CA.getBytes()); |
| } |
| |
| private void verifyCertUninstalledEventPresent(List<SecurityEvent> events) { |
| findEvent("cert authority removed", events, |
| e -> e.getTag() == TAG_CERT_AUTHORITY_REMOVED |
| && getInt(e, SUCCESS_INDEX) == SUCCESS_VALUE |
| && getString(e, SUBJECT_INDEX).equals(TEST_CA_SUBJECT)); |
| } |
| |
| private void generatePasswordComplexityEvents() { |
| DevicePolicyManager dpm = getDpmToGenerateEvents(); |
| |
| dpm.setPasswordQuality(ADMIN_RECEIVER_COMPONENT, PASSWORD_QUALITY_COMPLEX); |
| dpm.setPasswordMinimumLength(ADMIN_RECEIVER_COMPONENT, TEST_PWD_LENGTH); |
| dpm.setPasswordMinimumLetters(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS); |
| dpm.setPasswordMinimumNonLetter(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS); |
| dpm.setPasswordMinimumUpperCase(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS); |
| dpm.setPasswordMinimumLowerCase(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS); |
| dpm.setPasswordMinimumNumeric(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS); |
| dpm.setPasswordMinimumSymbols(ADMIN_RECEIVER_COMPONENT, TEST_PWD_CHARS); |
| } |
| |
| private void generateNewStylePasswordComplexityEvents() { |
| DevicePolicyManager dpm = getDpmToGenerateEvents(); |
| |
| dpm.setRequiredPasswordComplexity(PASSWORD_COMPLEXITY_HIGH); |
| } |
| |
| private void verifyPasswordComplexityEventsPresent(List<SecurityEvent> events) { |
| final int userId = Process.myUserHandle().getIdentifier(); |
| // This reflects default values for password complexity event payload fields. |
| final Object[] expectedPayload = new Object[] { |
| ADMIN_RECEIVER_COMPONENT.getPackageName(), // admin package |
| userId, // admin user |
| userId, // target user |
| 0, // default password length |
| 0, // default password quality |
| 1, // default min letters |
| 0, // default min non-letters |
| 1, // default min numeric |
| 0, // default min uppercase |
| 0, // default min lowercase |
| 1, // default min symbols |
| }; |
| |
| // The order should be consistent with the order in generatePasswordComplexityEvents(), so |
| // that the expected values change in the same sequence as when setting password policies. |
| expectedPayload[PWD_QUALITY_INDEX] = PASSWORD_QUALITY_COMPLEX; |
| assertPasswordComplexityEvent("set pwd quality", events, expectedPayload); |
| expectedPayload[PWD_LEN_INDEX] = TEST_PWD_LENGTH; |
| assertPasswordComplexityEvent("set pwd length", events, expectedPayload); |
| expectedPayload[LETTERS_INDEX] = TEST_PWD_CHARS; |
| assertPasswordComplexityEvent("set pwd min letters", events, expectedPayload); |
| expectedPayload[NON_LETTERS_INDEX] = TEST_PWD_CHARS; |
| assertPasswordComplexityEvent("set pwd min non-letters", events, expectedPayload); |
| expectedPayload[UPPERCASE_INDEX] = TEST_PWD_CHARS; |
| assertPasswordComplexityEvent("set pwd min uppercase", events, expectedPayload); |
| expectedPayload[LOWERCASE_INDEX] = TEST_PWD_CHARS; |
| assertPasswordComplexityEvent("set pwd min lowercase", events, expectedPayload); |
| expectedPayload[NUMERIC_INDEX] = TEST_PWD_CHARS; |
| assertPasswordComplexityEvent("set pwd min numeric", events, expectedPayload); |
| expectedPayload[SYMBOLS_INDEX] = TEST_PWD_CHARS; |
| assertPasswordComplexityEvent("set pwd min symbols", events, expectedPayload); |
| } |
| |
| private void verifyNewStylePasswordComplexityEventPresent(List<SecurityEvent> events) { |
| final int userId = Process.myUserHandle().getIdentifier(); |
| // This reflects default values for password complexity event payload fields. |
| final Object[] expectedPayload = new Object[] { |
| ADMIN_RECEIVER_COMPONENT.getPackageName(), // admin package |
| userId, // admin user |
| userId, // target user |
| PASSWORD_COMPLEXITY_HIGH // password complexity |
| }; |
| |
| findNewStylePasswordComplexityEvent("require password complexity", events, expectedPayload); |
| } |
| |
| private void generateLockingPolicyEvents() { |
| DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); |
| |
| if (mHasSecureLockScreen) { |
| dpm.setPasswordExpirationTimeout(ADMIN_RECEIVER_COMPONENT, TEST_PWD_EXPIRATION_TIMEOUT); |
| dpm.setPasswordHistoryLength(ADMIN_RECEIVER_COMPONENT, TEST_PWD_HISTORY_LENGTH); |
| dpm.setMaximumFailedPasswordsForWipe(ADMIN_RECEIVER_COMPONENT, TEST_PWD_MAX_ATTEMPTS); |
| } |
| dpm.setKeyguardDisabledFeatures(ADMIN_RECEIVER_COMPONENT, |
| KEYGUARD_DISABLE_FINGERPRINT); |
| dpm.setMaximumTimeToLock(ADMIN_RECEIVER_COMPONENT, TEST_MAX_TIME_TO_LOCK); |
| dpm.lockNow(); |
| } |
| |
| private void verifyLockingPolicyEventsPresent(List<SecurityEvent> events) { |
| final int userId = Process.myUserHandle().getIdentifier(); |
| final String packageName = ADMIN_RECEIVER_COMPONENT.getPackageName(); |
| if (mHasSecureLockScreen) { |
| findEvent("set password expiration", events, |
| e -> e.getTag() == TAG_PASSWORD_EXPIRATION_SET && |
| getString(e, ADMIN_PKG_INDEX).equals(packageName) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getLong(e, PWD_EXPIRATION_INDEX) == TEST_PWD_EXPIRATION_TIMEOUT); |
| |
| findEvent("set password history length", events, |
| e -> e.getTag() == TAG_PASSWORD_HISTORY_LENGTH_SET && |
| getString(e, ADMIN_PKG_INDEX).equals(packageName) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getInt(e, PWD_HIST_LEN_INDEX) == TEST_PWD_HISTORY_LENGTH); |
| |
| findEvent("set password attempts", events, |
| e -> e.getTag() == TAG_MAX_PASSWORD_ATTEMPTS_SET && |
| getString(e, ADMIN_PKG_INDEX).equals(packageName) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getInt(e, MAX_PWD_ATTEMPTS_INDEX) == TEST_PWD_MAX_ATTEMPTS); |
| } |
| |
| findEvent("set keyguard disabled features", events, |
| e -> e.getTag() == TAG_KEYGUARD_DISABLED_FEATURES_SET && |
| getString(e, ADMIN_PKG_INDEX).equals(packageName) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getInt(e, KEYGUARD_FEATURES_INDEX) == KEYGUARD_DISABLE_FINGERPRINT); |
| |
| findEvent("set screen lock timeout", events, |
| e -> e.getTag() == TAG_MAX_SCREEN_LOCK_TIMEOUT_SET && |
| getString(e, ADMIN_PKG_INDEX).equals(packageName) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getLong(e, MAX_SCREEN_TIMEOUT_INDEX) == TEST_MAX_TIME_TO_LOCK); |
| |
| findEvent("set screen lock timeout", events, |
| e -> e.getTag() == TAG_REMOTE_LOCK && |
| getString(e, ADMIN_PKG_INDEX).equals(packageName) && |
| getInt(e, ADMIN_USER_INDEX) == userId); |
| } |
| |
| private void assertPasswordComplexityEvent( |
| String description, List<SecurityEvent> events, Object[] expectedPayload) { |
| int expectedSize = mIsAutomotive ? 0 : 1; |
| assertNumberEvents(description, events, |
| byTagAndPayload(TAG_PASSWORD_COMPLEXITY_SET, expectedPayload), expectedSize); |
| } |
| |
| private void findNewStylePasswordComplexityEvent( |
| String description, List<SecurityEvent> events, Object[] expectedPayload) { |
| findEvent(description, events, |
| byTagAndPayload(TAG_PASSWORD_COMPLEXITY_REQUIRED, expectedPayload)); |
| } |
| |
| private Predicate<SecurityEvent> byTagAndPayload(int expectedTag, Object[] expectedPayload) { |
| return (event) -> { |
| boolean tagMatch = event.getTag() == expectedTag; |
| if (!tagMatch) return false; |
| |
| Object[] payload = (Object[]) event.getData(); |
| boolean payloadMatch = Arrays.equals(payload, expectedPayload); |
| |
| if (!payloadMatch) { |
| Log.w(TAG, "Found event (id=" + event.getId() + ") with tag " |
| + eventLogtoString(event.getTag()) + ", but invalid payload: " |
| + "expected=" + Arrays.toString(expectedPayload) |
| + ", actual=" + Arrays.toString(payload)); |
| } else if (VERBOSE) { |
| Log.v(TAG, "Found event (id=" + event.getId() + ") with tag " |
| + eventLogtoString(event.getTag()) + ", and expected payload (" |
| + Arrays.toString(payload) + ")"); |
| } |
| return payloadMatch; |
| }; |
| } |
| |
| private void generateUserRestrictionEvents() { |
| DevicePolicyManager dpm = getDpmToGenerateEvents(); |
| |
| dpm.addUserRestriction(ADMIN_RECEIVER_COMPONENT, UserManager.DISALLOW_PRINTING); |
| dpm.clearUserRestriction(ADMIN_RECEIVER_COMPONENT, UserManager.DISALLOW_PRINTING); |
| } |
| |
| private void verifyUserRestrictionEventsPresent(List<SecurityEvent> events) { |
| findUserRestrictionEvent("set user restriction", events, TAG_USER_RESTRICTION_ADDED); |
| findUserRestrictionEvent("clear user restriction", events, TAG_USER_RESTRICTION_REMOVED); |
| } |
| |
| private void findUserRestrictionEvent(String description, List<SecurityEvent> events, int tag) { |
| final int userId = Process.myUserHandle().getIdentifier(); |
| findEvent(description, events, |
| e -> e.getTag() == tag && |
| getString(e, ADMIN_PKG_INDEX).equals( |
| ADMIN_RECEIVER_COMPONENT.getPackageName()) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| UserManager.DISALLOW_PRINTING.equals(getString(e, USER_RESTRICTION_INDEX))); |
| } |
| |
| private void generateCameraPolicyEvents() { |
| DevicePolicyManager dpm = getDpmToGenerateEvents(); |
| |
| dpm.setCameraDisabled(ADMIN_RECEIVER_COMPONENT, true); |
| dpm.setCameraDisabled(ADMIN_RECEIVER_COMPONENT, false); |
| } |
| |
| private void verifyCameraPolicyEvents(List<SecurityEvent> events) { |
| final int userId = Process.myUserHandle().getIdentifier(); |
| |
| findEvent("set camera disabled", events, |
| e -> e.getTag() == TAG_CAMERA_POLICY_SET && |
| getString(e, ADMIN_PKG_INDEX).equals( |
| ADMIN_RECEIVER_COMPONENT.getPackageName()) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getInt(e, CAMERA_DISABLED_INDEX) == 1); |
| |
| findEvent("set camera enabled", events, |
| e -> e.getTag() == TAG_CAMERA_POLICY_SET && |
| getString(e, ADMIN_PKG_INDEX).equals( |
| ADMIN_RECEIVER_COMPONENT.getPackageName()) && |
| getInt(e, ADMIN_USER_INDEX) == userId && |
| getInt(e, TARGET_USER_INDEX) == userId && |
| getInt(e, CAMERA_DISABLED_INDEX) == 0); |
| } |
| |
| private DevicePolicyManager getDpmToGenerateEvents() { |
| // It must use the dpm for the current user, as mDevicePolicyManager tunnels the calls to |
| // the device owner user on headless system user, which would cause a mismatch in the events |
| return mContext.getSystemService(DevicePolicyManager.class); |
| } |
| |
| private static String eventLogtoString(int log) { |
| return DebugUtils.constantToString(SecurityLog.class, "TAG_", log); |
| } |
| |
| private static String toString(SecurityEvent event) { |
| return "Event[id=" + event.getId() + ",tag=" + eventLogtoString(event.getTag()) + "]"; |
| } |
| |
| private void dumpSecurityLogs(List<SecurityEvent> events) { |
| Log.d(TAG, "Security events dump (" + events.size() + " events):"); |
| events.forEach((event) -> Log.d(TAG, toString(event))); |
| } |
| } |