diff options
5 files changed, 401 insertions, 33 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java new file mode 100644 index 000000000000..a78357984912 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2019 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.server.backup.encryption.keys; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.Slog; + +import com.android.server.backup.encryption.protos.nano.WrappedKeyProto; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Optional; + +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +/** + * Gets the correct tertiary key to use during a backup, rotating it if required. + * + * <p>Calling any method on this class will count a incremental backup against the app, and the key + * will be rotated if required. + */ +public class TertiaryKeyManager { + + private static final String TAG = "TertiaryKeyMgr"; + + private final TertiaryKeyStore mKeyStore; + private final TertiaryKeyGenerator mKeyGenerator; + private final TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler; + private final RecoverableKeyStoreSecondaryKey mSecondaryKey; + private final String mPackageName; + + private boolean mKeyRotated; + @Nullable private SecretKey mTertiaryKey; + + public TertiaryKeyManager( + Context context, + SecureRandom secureRandom, + TertiaryKeyRotationScheduler tertiaryKeyRotationScheduler, + RecoverableKeyStoreSecondaryKey secondaryKey, + String packageName) { + mSecondaryKey = secondaryKey; + mPackageName = packageName; + mKeyGenerator = new TertiaryKeyGenerator(secureRandom); + mKeyStore = TertiaryKeyStore.newInstance(context, secondaryKey); + mTertiaryKeyRotationScheduler = tertiaryKeyRotationScheduler; + } + + /** + * Returns either the previously used tertiary key, or a new tertiary key if there was no + * previous key or it needed to be rotated. + */ + public SecretKey getKey() + throws InvalidKeyException, IOException, IllegalBlockSizeException, + NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + init(); + return mTertiaryKey; + } + + /** Returns the key given by {@link #getKey()} wrapped by the secondary key. */ + public WrappedKeyProto.WrappedKey getWrappedKey() + throws InvalidKeyException, IOException, IllegalBlockSizeException, + NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + init(); + return KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), mTertiaryKey); + } + + /** + * Returns {@code true} if a new tertiary key was generated at the start of this session, + * otherwise {@code false}. + */ + public boolean wasKeyRotated() + throws InvalidKeyException, IllegalBlockSizeException, IOException, + NoSuchPaddingException, NoSuchAlgorithmException, + InvalidAlgorithmParameterException { + init(); + return mKeyRotated; + } + + private void init() + throws IllegalBlockSizeException, InvalidKeyException, IOException, + NoSuchAlgorithmException, NoSuchPaddingException, + InvalidAlgorithmParameterException { + if (mTertiaryKey != null) { + return; + } + + Optional<SecretKey> key = getExistingKeyIfNotRotated(); + + if (!key.isPresent()) { + Slog.d(TAG, "Generating new tertiary key for " + mPackageName); + + key = Optional.of(mKeyGenerator.generate()); + mKeyRotated = true; + mTertiaryKeyRotationScheduler.recordKeyRotation(mPackageName); + mKeyStore.save(mPackageName, key.get()); + } + + mTertiaryKey = key.get(); + + mTertiaryKeyRotationScheduler.recordBackup(mPackageName); + } + + private Optional<SecretKey> getExistingKeyIfNotRotated() + throws InvalidKeyException, IOException, InvalidAlgorithmParameterException, + NoSuchAlgorithmException, NoSuchPaddingException { + if (mTertiaryKeyRotationScheduler.isKeyRotationDue(mPackageName)) { + Slog.i(TAG, "Tertiary key rotation was required for " + mPackageName); + return Optional.empty(); + } else { + Slog.i(TAG, "Tertiary key rotation was not required"); + return mKeyStore.load(mPackageName); + } + } +} diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java new file mode 100644 index 000000000000..1ed8309c3a48 --- /dev/null +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2019 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.server.backup.encryption.keys; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.RuntimeEnvironment.application; + +import android.security.keystore.recovery.RecoveryController; + +import com.android.server.backup.encryption.protos.nano.WrappedKeyProto; +import com.android.server.testing.shadows.ShadowRecoveryController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.security.SecureRandom; + +import javax.crypto.SecretKey; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRecoveryController.class) +public class TertiaryKeyManagerTest { + + private static final String TEST_PACKAGE_1 = "com.example.app1"; + private static final String TEST_PACKAGE_2 = "com.example.app2"; + + private SecureRandom mSecureRandom; + private RecoverableKeyStoreSecondaryKey mSecondaryKey; + + @Mock private TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mSecureRandom = new SecureRandom(); + mSecondaryKey = + new RecoverableKeyStoreSecondaryKeyManager( + RecoveryController.getInstance(application), mSecureRandom) + .generate(); + ShadowRecoveryController.reset(); + } + + private TertiaryKeyManager createNewManager(String packageName) { + return new TertiaryKeyManager( + application, + mSecureRandom, + mTertiaryKeyRotationScheduler, + mSecondaryKey, + packageName); + } + + @Test + public void getKey_noExistingKey_returnsNewKey() throws Exception { + assertThat(createNewManager(TEST_PACKAGE_1).getKey()).isNotNull(); + } + + @Test + public void getKey_noExistingKey_recordsIncrementalBackup() throws Exception { + createNewManager(TEST_PACKAGE_1).getKey(); + verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1); + } + + @Test + public void getKey_existingKey_returnsExistingKey() throws Exception { + TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1); + SecretKey existingKey = manager.getKey(); + + assertThat(manager.getKey()).isEqualTo(existingKey); + } + + @Test + public void getKey_existingKey_recordsBackupButNotRotation() throws Exception { + createNewManager(TEST_PACKAGE_1).getKey(); + reset(mTertiaryKeyRotationScheduler); + + createNewManager(TEST_PACKAGE_1).getKey(); + + verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1); + verify(mTertiaryKeyRotationScheduler, never()).recordKeyRotation(any()); + } + + @Test + public void getKey_existingKeyButRotationRequired_returnsNewKey() throws Exception { + SecretKey firstKey = createNewManager(TEST_PACKAGE_1).getKey(); + when(mTertiaryKeyRotationScheduler.isKeyRotationDue(TEST_PACKAGE_1)).thenReturn(true); + + SecretKey secondKey = createNewManager(TEST_PACKAGE_1).getKey(); + + assertThat(secondKey).isNotEqualTo(firstKey); + } + + @Test + public void getKey_existingKeyButRotationRequired_recordsKeyRotationAndBackup() + throws Exception { + when(mTertiaryKeyRotationScheduler.isKeyRotationDue(TEST_PACKAGE_1)).thenReturn(true); + createNewManager(TEST_PACKAGE_1).getKey(); + + InOrder inOrder = inOrder(mTertiaryKeyRotationScheduler); + inOrder.verify(mTertiaryKeyRotationScheduler).recordKeyRotation(TEST_PACKAGE_1); + inOrder.verify(mTertiaryKeyRotationScheduler).recordBackup(TEST_PACKAGE_1); + } + + @Test + public void getKey_twoApps_returnsDifferentKeys() throws Exception { + TertiaryKeyManager firstManager = createNewManager(TEST_PACKAGE_1); + TertiaryKeyManager secondManager = createNewManager(TEST_PACKAGE_2); + SecretKey firstKey = firstManager.getKey(); + + assertThat(secondManager.getKey()).isNotEqualTo(firstKey); + } + + @Test + public void getWrappedKey_noExistingKey_returnsWrappedNewKey() throws Exception { + TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1); + SecretKey unwrappedKey = manager.getKey(); + WrappedKeyProto.WrappedKey wrappedKey = manager.getWrappedKey(); + + SecretKey expectedUnwrappedKey = + KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey); + assertThat(unwrappedKey).isEqualTo(expectedUnwrappedKey); + } + + @Test + public void getWrappedKey_existingKey_returnsWrappedExistingKey() throws Exception { + TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1); + WrappedKeyProto.WrappedKey wrappedKey = manager.getWrappedKey(); + SecretKey unwrappedKey = manager.getKey(); + + SecretKey expectedUnwrappedKey = + KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey); + assertThat(unwrappedKey).isEqualTo(expectedUnwrappedKey); + } + + @Test + public void wasKeyRotated_noExistingKey_returnsTrue() throws Exception { + TertiaryKeyManager manager = createNewManager(TEST_PACKAGE_1); + assertThat(manager.wasKeyRotated()).isTrue(); + } + + @Test + public void wasKeyRotated_existingKey_returnsFalse() throws Exception { + createNewManager(TEST_PACKAGE_1).getKey(); + assertThat(createNewManager(TEST_PACKAGE_1).wasKeyRotated()).isFalse(); + } +} diff --git a/services/core/java/com/android/server/PackageWatchdog.java b/services/core/java/com/android/server/PackageWatchdog.java index 20a9b20dd385..548665ba3a32 100644 --- a/services/core/java/com/android/server/PackageWatchdog.java +++ b/services/core/java/com/android/server/PackageWatchdog.java @@ -80,10 +80,12 @@ public class PackageWatchdog { "watchdog_explicit_health_check_enabled"; // Duration to count package failures before it resets to 0 - private static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS = + @VisibleForTesting + static final int DEFAULT_TRIGGER_FAILURE_DURATION_MS = (int) TimeUnit.MINUTES.toMillis(1); // Number of package failures within the duration above before we notify observers - private static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5; + @VisibleForTesting + static final int DEFAULT_TRIGGER_FAILURE_COUNT = 5; // Whether explicit health checks are enabled or not private static final boolean DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED = true; @@ -722,7 +724,7 @@ public class PackageWatchdog { PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, DEFAULT_TRIGGER_FAILURE_DURATION_MS); if (mTriggerFailureDurationMs <= 0) { - mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_COUNT; + mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; } setExplicitHealthCheckEnabled(DeviceConfig.getBoolean( diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 9b6333d7bef4..3464cab99d93 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -443,45 +443,39 @@ public class LauncherAppsService extends SystemService { if (isManagedProfileAdmin(user, appInfo.packageName)) { return false; } - // If app does not have any components or any permissions, the app can legitimately - // have no icon so we do not show the synthetic activity. - return hasComponentsAndRequestsPermissions(appInfo.packageName); - } - - private boolean hasComponentsAndRequestsPermissions(@NonNull String packageName) { final PackageManagerInternal pmInt = LocalServices.getService(PackageManagerInternal.class); - final PackageParser.Package pkg = pmInt.getPackage(packageName); + final PackageParser.Package pkg = pmInt.getPackage(appInfo.packageName); if (pkg == null) { // Should not happen, but we shouldn't be failing if it does return false; } - if (ArrayUtils.isEmpty(pkg.requestedPermissions)) { - return false; - } - if (!hasApplicationDeclaredActivities(pkg) - && ArrayUtils.isEmpty(pkg.receivers) - && ArrayUtils.isEmpty(pkg.providers) - && ArrayUtils.isEmpty(pkg.services)) { - return false; - } - return true; + // If app does not have any default enabled launcher activity or any permissions, + // the app can legitimately have no icon so we do not show the synthetic activity. + return requestsPermissions(pkg) && hasDefaultEnableLauncherActivity( + appInfo.packageName); } - private boolean hasApplicationDeclaredActivities(@NonNull PackageParser.Package pkg) { - if (pkg.activities == null) { - return false; - } - if (ArrayUtils.isEmpty(pkg.activities)) { - return false; - } - // If it only contains synthetic AppDetailsActivity only, it means application does - // not have actual activity declared in manifest. - if (pkg.activities.size() == 1 && PackageManager.APP_DETAILS_ACTIVITY_CLASS_NAME.equals( - pkg.activities.get(0).className)) { - return false; + private boolean requestsPermissions(@NonNull PackageParser.Package pkg) { + return !ArrayUtils.isEmpty(pkg.requestedPermissions); + } + + private boolean hasDefaultEnableLauncherActivity(@NonNull String packageName) { + final PackageManagerInternal pmInt = + LocalServices.getService(PackageManagerInternal.class); + final Intent matchIntent = new Intent(Intent.ACTION_MAIN); + matchIntent.addCategory(Intent.CATEGORY_LAUNCHER); + matchIntent.setPackage(packageName); + final List<ResolveInfo> infoList = pmInt.queryIntentActivities(matchIntent, + PackageManager.MATCH_DISABLED_COMPONENTS, Binder.getCallingUid(), + getCallingUserId()); + final int size = infoList.size(); + for (int i = 0; i < size; i++) { + if (infoList.get(i).activityInfo.enabled) { + return true; + } } - return true; + return false; } private boolean isManagedProfileAdmin(UserHandle user, String packageName) { diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 9a6033058c53..ab31ed7389a3 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -702,6 +702,69 @@ public class PackageWatchdogTest { assertThat(observer.mMitigatedPackages).containsExactly(APP_A); } + /** Test default values are used when device property is invalid. */ + @Test + public void testInvalidConfig_watchdogTriggerFailureCount() { + adoptShellPermissions( + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(-1), /*makeDefault*/false); + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A), SHORT_DURATION); + // Fail APP_A below the threshold which should not trigger package failures + for (int i = 0; i < PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT - 1; i++) { + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + } + mTestLooper.dispatchAll(); + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + + // One more to trigger the package failure + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + mTestLooper.dispatchAll(); + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_A); + } + + /** Test default values are used when device property is invalid. */ + @Test + public void testInvalidConfig_watchdogTriggerDurationMillis() { + adoptShellPermissions( + Manifest.permission.WRITE_DEVICE_CONFIG, + Manifest.permission.READ_DEVICE_CONFIG); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(2), /*makeDefault*/false); + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_DURATION_MILLIS, + Integer.toString(-1), /*makeDefault*/false); + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer = new TestObserver(OBSERVER_NAME_1); + + watchdog.startObservingHealth(observer, Arrays.asList(APP_A, APP_B), Long.MAX_VALUE); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS + 1); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE))); + mTestLooper.dispatchAll(); + + // We shouldn't receive APP_A since the interval of 2 failures is greater than + // DEFAULT_TRIGGER_FAILURE_DURATION_MS. + assertThat(observer.mHealthCheckFailedPackages).isEmpty(); + + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE))); + mTestLooper.dispatchAll(); + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS - 1); + watchdog.onPackageFailure(Arrays.asList(new VersionedPackage(APP_B, VERSION_CODE))); + mTestLooper.dispatchAll(); + + // We should receive APP_B since the interval of 2 failures is less than + // DEFAULT_TRIGGER_FAILURE_DURATION_MS. + assertThat(observer.mHealthCheckFailedPackages).containsExactly(APP_B); + } + private void adoptShellPermissions(String... permissions) { InstrumentationRegistry .getInstrumentation() |