summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyManager.java136
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyManagerTest.java173
-rw-r--r--services/core/java/com/android/server/PackageWatchdog.java8
-rw-r--r--services/core/java/com/android/server/pm/LauncherAppsService.java54
-rw-r--r--tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java63
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()