summaryrefslogtreecommitdiff
path: root/packages/BackupEncryption
diff options
context:
space:
mode:
authorRuslan Tkhakokhov <rthakohov@google.com>2019-09-30 07:19:32 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2019-09-30 07:19:32 +0000
commit736d858a5fca0ea2290cae269f1e492f6a10ab9a (patch)
tree98abc63cc22bc2eea31671c11569851cb66facad /packages/BackupEncryption
parent0234e05cd399b11cc39386e787ae9c4797ae5451 (diff)
parent16857a093605babb8c9e8dc5357a7ee9e5e4f258 (diff)
Merge "Import RotateSecondaryKeyTask"
Diffstat (limited to 'packages/BackupEncryption')
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java30
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java27
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java28
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java270
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java363
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java90
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java143
7 files changed, 951 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java
new file mode 100644
index 000000000000..9e31385c9525
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/client/UnexpectedActiveSecondaryOnServerException.java
@@ -0,0 +1,30 @@
+/*
+ * 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.client;
+
+/**
+ * Error thrown when the user attempts to retrieve a key set from the server, but is asking for keys
+ * from an inactive secondary.
+ *
+ * <p>Although we could just return old keys, there is no good reason to do this. It almost
+ * certainly indicates a logic error on the client.
+ */
+public class UnexpectedActiveSecondaryOnServerException extends Exception {
+ public UnexpectedActiveSecondaryOnServerException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java
new file mode 100644
index 000000000000..2e8a61f05970
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/ActiveSecondaryNotInKeychainException.java
@@ -0,0 +1,27 @@
+/*
+ * 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.tasks;
+
+/**
+ * Error thrown when the server's active secondary key does not exist in the user's recoverable
+ * keychain. This means the backup data cannot be decrypted, and should be wiped.
+ */
+public class ActiveSecondaryNotInKeychainException extends Exception {
+ public ActiveSecondaryNotInKeychainException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java
new file mode 100644
index 000000000000..72e8a89f1df3
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NoActiveSecondaryKeyException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.tasks;
+
+/**
+ * Error thrown if attempting to rotate key when there is no current active secondary key set
+ * locally. This means the device needs to re-initialize, asking the backup server what the active
+ * secondary key is.
+ */
+public class NoActiveSecondaryKeyException extends Exception {
+ public NoActiveSecondaryKeyException(String message) {
+ super(message);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java
new file mode 100644
index 000000000000..d58cb66ef6b4
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTask.java
@@ -0,0 +1,270 @@
+/*
+ * 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.tasks;
+
+import static android.os.Build.VERSION_CODES.P;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
+import android.util.Slog;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyStore;
+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.UnrecoverableKeyException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+
+/**
+ * Finishes a rotation for a {@link
+ * com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey}.
+ */
+public class RotateSecondaryKeyTask {
+ private static final String TAG = "RotateSecondaryKeyTask";
+
+ private final Context mContext;
+ private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
+ private final CryptoBackupServer mBackupServer;
+ private final CryptoSettings mCryptoSettings;
+ private final RecoveryController mRecoveryController;
+
+ /**
+ * A new instance.
+ *
+ * @param secondaryKeyManager For loading the currently active and next secondary key.
+ * @param backupServer For loading and storing tertiary keys and for setting active secondary
+ * key.
+ * @param cryptoSettings For checking the stored aliases for the next and active key.
+ * @param recoveryController For communicating with the Framework apis.
+ */
+ public RotateSecondaryKeyTask(
+ Context context,
+ RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
+ CryptoBackupServer backupServer,
+ CryptoSettings cryptoSettings,
+ RecoveryController recoveryController) {
+ mContext = context;
+ mSecondaryKeyManager = checkNotNull(secondaryKeyManager);
+ mCryptoSettings = checkNotNull(cryptoSettings);
+ mBackupServer = checkNotNull(backupServer);
+ mRecoveryController = checkNotNull(recoveryController);
+ }
+
+ /** Runs the task. */
+ public void run() {
+ // Never run more than one of these at the same time.
+ synchronized (RotateSecondaryKeyTask.class) {
+ runInternal();
+ }
+ }
+
+ private void runInternal() {
+ Optional<RecoverableKeyStoreSecondaryKey> maybeNextKey;
+ try {
+ maybeNextKey = getNextKey();
+ } catch (Exception e) {
+ Slog.e(TAG, "Error checking for next key", e);
+ return;
+ }
+
+ if (!maybeNextKey.isPresent()) {
+ Slog.d(TAG, "No secondary key rotation task pending. Exiting.");
+ return;
+ }
+
+ RecoverableKeyStoreSecondaryKey nextKey = maybeNextKey.get();
+ boolean isReady;
+ try {
+ isReady = isSecondaryKeyRotationReady(nextKey);
+ } catch (InternalRecoveryServiceException e) {
+ Slog.e(TAG, "Error encountered checking whether next secondary key is synced", e);
+ return;
+ }
+
+ if (!isReady) {
+ return;
+ }
+
+ try {
+ rotateToKey(nextKey);
+ } catch (Exception e) {
+ Slog.e(TAG, "Error trying to rotate to new secondary key", e);
+ }
+ }
+
+ private Optional<RecoverableKeyStoreSecondaryKey> getNextKey()
+ throws InternalRecoveryServiceException, UnrecoverableKeyException {
+ Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
+ if (!maybeNextAlias.isPresent()) {
+ return Optional.empty();
+ }
+ return mSecondaryKeyManager.get(maybeNextAlias.get());
+ }
+
+ private boolean isSecondaryKeyRotationReady(RecoverableKeyStoreSecondaryKey nextKey)
+ throws InternalRecoveryServiceException {
+ String nextAlias = nextKey.getAlias();
+ Slog.i(TAG, "Key rotation to " + nextAlias + " is pending. Checking key sync status.");
+ int status = mRecoveryController.getRecoveryStatus(nextAlias);
+
+ if (status == RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE) {
+ Slog.e(
+ TAG,
+ "Permanent failure to sync " + nextAlias + ". Cannot possibly rotate to it.");
+ mCryptoSettings.removeNextSecondaryKeyAlias();
+ return false;
+ }
+
+ if (status == RecoveryController.RECOVERY_STATUS_SYNCED) {
+ Slog.i(TAG, "Secondary key " + nextAlias + " has now synced! Commencing rotation.");
+ } else {
+ Slog.i(TAG, "Sync still pending for " + nextAlias);
+ }
+ return status == RecoveryController.RECOVERY_STATUS_SYNCED;
+ }
+
+ /**
+ * @throws ActiveSecondaryNotInKeychainException if the currently active secondary key is not in
+ * the keychain.
+ * @throws IOException if there is an IO issue communicating with the server or loading from
+ * disk.
+ * @throws NoActiveSecondaryKeyException if there is no active key set.
+ * @throws IllegalBlockSizeException if there is an issue decrypting a tertiary key.
+ * @throws InvalidKeyException if any of the secondary keys cannot be used for wrapping or
+ * unwrapping tertiary keys.
+ */
+ private void rotateToKey(RecoverableKeyStoreSecondaryKey newSecondaryKey)
+ throws ActiveSecondaryNotInKeychainException, IOException,
+ NoActiveSecondaryKeyException, IllegalBlockSizeException, InvalidKeyException,
+ InternalRecoveryServiceException, UnrecoverableKeyException,
+ InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+ NoSuchPaddingException {
+ RecoverableKeyStoreSecondaryKey activeSecondaryKey = getActiveSecondaryKey();
+ String activeSecondaryKeyAlias = activeSecondaryKey.getAlias();
+ String newSecondaryKeyAlias = newSecondaryKey.getAlias();
+ if (newSecondaryKeyAlias.equals(activeSecondaryKeyAlias)) {
+ Slog.i(TAG, activeSecondaryKeyAlias + " was already the active alias.");
+ return;
+ }
+
+ TertiaryKeyStore tertiaryKeyStore =
+ TertiaryKeyStore.newInstance(mContext, activeSecondaryKey);
+ Map<String, SecretKey> tertiaryKeys = tertiaryKeyStore.getAll();
+
+ if (tertiaryKeys.isEmpty()) {
+ Slog.i(
+ TAG,
+ "No tertiary keys for " + activeSecondaryKeyAlias + ". No need to rewrap. ");
+ mBackupServer.setActiveSecondaryKeyAlias(
+ newSecondaryKeyAlias, /*tertiaryKeys=*/ Collections.emptyMap());
+ } else {
+ Map<String, WrappedKeyProto.WrappedKey> rewrappedTertiaryKeys =
+ rewrapAll(newSecondaryKey, tertiaryKeys);
+ TertiaryKeyStore.newInstance(mContext, newSecondaryKey).putAll(rewrappedTertiaryKeys);
+ Slog.i(
+ TAG,
+ "Successfully rewrapped " + rewrappedTertiaryKeys.size() + " tertiary keys");
+ mBackupServer.setActiveSecondaryKeyAlias(newSecondaryKeyAlias, rewrappedTertiaryKeys);
+ Slog.i(
+ TAG,
+ "Successfully uploaded new set of tertiary keys to "
+ + newSecondaryKeyAlias
+ + " alias");
+ }
+
+ mCryptoSettings.setActiveSecondaryKeyAlias(newSecondaryKeyAlias);
+ mCryptoSettings.removeNextSecondaryKeyAlias();
+ try {
+ mRecoveryController.removeKey(activeSecondaryKeyAlias);
+ } catch (InternalRecoveryServiceException e) {
+ Slog.e(TAG, "Error removing old secondary key from RecoverableKeyStoreLoader", e);
+ }
+ }
+
+ private RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
+ throws NoActiveSecondaryKeyException, ActiveSecondaryNotInKeychainException,
+ InternalRecoveryServiceException, UnrecoverableKeyException {
+
+ Optional<String> activeSecondaryAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
+
+ if (!activeSecondaryAlias.isPresent()) {
+ Slog.i(
+ TAG,
+ "Was asked to rotate secondary key, but local config did not have a secondary "
+ + "key alias set.");
+ throw new NoActiveSecondaryKeyException("No local active secondary key set.");
+ }
+
+ String activeSecondaryKeyAlias = activeSecondaryAlias.get();
+ Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
+ mSecondaryKeyManager.get(activeSecondaryKeyAlias);
+
+ if (!secondaryKey.isPresent()) {
+ throw new ActiveSecondaryNotInKeychainException(
+ String.format(
+ Locale.US,
+ "Had local active recoverable key alias of %s but key was not in"
+ + " user's keychain.",
+ activeSecondaryKeyAlias));
+ }
+
+ return secondaryKey.get();
+ }
+
+ /**
+ * Rewraps all the tertiary keys.
+ *
+ * @param newSecondaryKey The secondary key with which to rewrap the tertiaries.
+ * @param tertiaryKeys The tertiary keys, by package name.
+ * @return The newly wrapped tertiary keys, by package name.
+ * @throws InvalidKeyException if any key is unusable.
+ * @throws IllegalBlockSizeException if could not decrypt.
+ */
+ private Map<String, WrappedKeyProto.WrappedKey> rewrapAll(
+ RecoverableKeyStoreSecondaryKey newSecondaryKey, Map<String, SecretKey> tertiaryKeys)
+ throws InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException,
+ NoSuchAlgorithmException {
+ Map<String, WrappedKeyProto.WrappedKey> wrappedKeys = new HashMap<>();
+
+ for (String packageName : tertiaryKeys.keySet()) {
+ SecretKey tertiaryKey = tertiaryKeys.get(packageName);
+ wrappedKeys.put(
+ packageName, KeyWrapUtils.wrap(newSecondaryKey.getSecretKey(), tertiaryKey));
+ }
+
+ return wrappedKeys;
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java
new file mode 100644
index 000000000000..cda73174a3e2
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/RotateSecondaryKeyTaskTest.java
@@ -0,0 +1,363 @@
+/*
+ * 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.tasks;
+
+import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertFalse;
+
+import android.app.Application;
+import android.platform.test.annotations.Presubmit;
+import android.security.keystore.recovery.RecoveryController;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.CryptoSettings;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.TertiaryKeyStore;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.testing.fakes.FakeCryptoBackupServer;
+import com.android.server.testing.shadows.ShadowRecoveryController;
+
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.SecretKey;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+@Config(shadows = {ShadowRecoveryController.class, ShadowRecoveryController.class})
+public class RotateSecondaryKeyTaskTest {
+ private static final String APP_1 = "app1";
+ private static final String APP_2 = "app2";
+ private static final String APP_3 = "app3";
+
+ private static final String CURRENT_SECONDARY_KEY_ALIAS =
+ "recoverablekey.alias/d524796bd07de3c2225c63d434eff698";
+ private static final String NEXT_SECONDARY_KEY_ALIAS =
+ "recoverablekey.alias/6c6d198a7f12e662b6bc45f4849db170";
+
+ private Application mApplication;
+ private RotateSecondaryKeyTask mTask;
+ private RecoveryController mRecoveryController;
+ private FakeCryptoBackupServer mBackupServer;
+ private CryptoSettings mCryptoSettings;
+ private Map<String, SecretKey> mTertiaryKeysByPackageName;
+ private RecoverableKeyStoreSecondaryKeyManager mRecoverableSecondaryKeyManager;
+
+ @Before
+ public void setUp() throws Exception {
+ mApplication = ApplicationProvider.getApplicationContext();
+
+ mTertiaryKeysByPackageName = new HashMap<>();
+ mTertiaryKeysByPackageName.put(APP_1, generateAesKey());
+ mTertiaryKeysByPackageName.put(APP_2, generateAesKey());
+ mTertiaryKeysByPackageName.put(APP_3, generateAesKey());
+
+ mRecoveryController = RecoveryController.getInstance(mApplication);
+ mRecoverableSecondaryKeyManager =
+ new RecoverableKeyStoreSecondaryKeyManager(
+ RecoveryController.getInstance(mApplication), new SecureRandom());
+ mBackupServer = new FakeCryptoBackupServer();
+ mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
+ addNextSecondaryKeyToRecoveryController();
+ mCryptoSettings.setNextSecondaryAlias(NEXT_SECONDARY_KEY_ALIAS);
+
+ mTask =
+ new RotateSecondaryKeyTask(
+ mApplication,
+ mRecoverableSecondaryKeyManager,
+ mBackupServer,
+ mCryptoSettings,
+ mRecoveryController);
+
+ ShadowRecoveryController.reset();
+ }
+
+ @Test
+ public void run_failsIfThereIsNoActiveSecondaryKey() throws Exception {
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertFalse(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent());
+ }
+
+ @Test
+ public void run_failsIfActiveSecondaryIsNotInRecoveryController() throws Exception {
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ // Have to add it first as otherwise CryptoSettings throws an exception when trying to set
+ // it
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_doesNothingIfFlagIsDisabled() throws Exception {
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+ addWrappedTertiaries();
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_setsActiveSecondary() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+ addWrappedTertiaries();
+
+ mTask.run();
+
+ assertThat(mBackupServer.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_rewrapsExistingTertiaryKeys() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+ addWrappedTertiaries();
+
+ mTask.run();
+
+ Map<String, WrappedKeyProto.WrappedKey> rewrappedKeys =
+ mBackupServer.getAllTertiaryKeys(NEXT_SECONDARY_KEY_ALIAS);
+ SecretKey secondaryKey = (SecretKey) mRecoveryController.getKey(NEXT_SECONDARY_KEY_ALIAS);
+ for (String packageName : mTertiaryKeysByPackageName.keySet()) {
+ WrappedKeyProto.WrappedKey rewrappedKey = rewrappedKeys.get(packageName);
+ assertThat(KeyWrapUtils.unwrap(secondaryKey, rewrappedKey))
+ .isEqualTo(mTertiaryKeysByPackageName.get(packageName));
+ }
+ }
+
+ @Test
+ public void run_persistsRewrappedKeysToDisk() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+ addWrappedTertiaries();
+
+ mTask.run();
+
+ RecoverableKeyStoreSecondaryKey secondaryKey = getRecoverableKey(NEXT_SECONDARY_KEY_ALIAS);
+ Map<String, SecretKey> keys =
+ TertiaryKeyStore.newInstance(mApplication, secondaryKey).getAll();
+ for (String packageName : mTertiaryKeysByPackageName.keySet()) {
+ SecretKey tertiaryKey = mTertiaryKeysByPackageName.get(packageName);
+ SecretKey newlyWrappedKey = keys.get(packageName);
+ assertThat(tertiaryKey.getEncoded()).isEqualTo(newlyWrappedKey.getEncoded());
+ }
+ }
+
+ @Test
+ public void run_stillSetsActiveSecondaryIfNoTertiaries() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mBackupServer.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_setsActiveSecondaryKeyAliasInSettings() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_removesNextSecondaryKeyAliasInSettings() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
+ }
+
+ @Test
+ public void run_deletesOldKeyFromRecoverableKeyStoreLoader() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNCED);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mRecoveryController.getKey(CURRENT_SECONDARY_KEY_ALIAS)).isNull();
+ }
+
+ @Test
+ public void run_doesNotRotateIfNoNextAlias() throws Exception {
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+ mCryptoSettings.removeNextSecondaryKeyAlias();
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+ assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
+ }
+
+ @Test
+ public void run_doesNotRotateIfKeyIsNotSyncedYet() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_doesNotClearNextKeyIfSyncIsJustPending() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get())
+ .isEqualTo(NEXT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_doesNotRotateIfPermanentFailure() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get())
+ .isEqualTo(CURRENT_SECONDARY_KEY_ALIAS);
+ }
+
+ @Test
+ public void run_removesNextKeyIfPermanentFailure() throws Exception {
+ addNextSecondaryKeyToRecoveryController();
+ setNextKeyRecoveryStatus(RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE);
+ addCurrentSecondaryKeyToRecoveryController();
+ mCryptoSettings.setActiveSecondaryKeyAlias(CURRENT_SECONDARY_KEY_ALIAS);
+ mBackupServer.setActiveSecondaryKeyAlias(
+ CURRENT_SECONDARY_KEY_ALIAS, Collections.emptyMap());
+
+ mTask.run();
+
+ assertFalse(mCryptoSettings.getNextSecondaryKeyAlias().isPresent());
+ }
+
+ private void setNextKeyRecoveryStatus(int status) throws Exception {
+ mRecoveryController.setRecoveryStatus(NEXT_SECONDARY_KEY_ALIAS, status);
+ }
+
+ private void addCurrentSecondaryKeyToRecoveryController() throws Exception {
+ mRecoveryController.generateKey(CURRENT_SECONDARY_KEY_ALIAS);
+ }
+
+ private void addNextSecondaryKeyToRecoveryController() throws Exception {
+ mRecoveryController.generateKey(NEXT_SECONDARY_KEY_ALIAS);
+ }
+
+ private void addWrappedTertiaries() throws Exception {
+ TertiaryKeyStore tertiaryKeyStore =
+ TertiaryKeyStore.newInstance(
+ mApplication, getRecoverableKey(CURRENT_SECONDARY_KEY_ALIAS));
+
+ for (String packageName : mTertiaryKeysByPackageName.keySet()) {
+ tertiaryKeyStore.save(packageName, mTertiaryKeysByPackageName.get(packageName));
+ }
+ }
+
+ private RecoverableKeyStoreSecondaryKey getRecoverableKey(String alias) throws Exception {
+ return mRecoverableSecondaryKeyManager.get(alias).get();
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java
new file mode 100644
index 000000000000..332906033b6d
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServer.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 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.testing.fakes;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.client.UnexpectedActiveSecondaryOnServerException;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+
+/** Fake {@link CryptoBackupServer}, for tests. Stores tertiary keys in memory. */
+public class FakeCryptoBackupServer implements CryptoBackupServer {
+ @GuardedBy("this")
+ @Nullable
+ private String mActiveSecondaryKeyAlias;
+
+ // Secondary key alias -> (package name -> tertiary key)
+ @GuardedBy("this")
+ private Map<String, Map<String, WrappedKeyProto.WrappedKey>> mWrappedKeyStore = new HashMap<>();
+
+ @Override
+ public String uploadIncrementalBackup(
+ String packageName,
+ String oldDocId,
+ byte[] diffScript,
+ WrappedKeyProto.WrappedKey tertiaryKey) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public String uploadNonIncrementalBackup(
+ String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public synchronized void setActiveSecondaryKeyAlias(
+ String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
+ mActiveSecondaryKeyAlias = keyAlias;
+
+ mWrappedKeyStore.putIfAbsent(keyAlias, new HashMap<>());
+ Map<String, WrappedKeyProto.WrappedKey> keyStore = mWrappedKeyStore.get(keyAlias);
+
+ for (String packageName : tertiaryKeys.keySet()) {
+ keyStore.put(packageName, tertiaryKeys.get(packageName));
+ }
+ }
+
+ public synchronized Optional<String> getActiveSecondaryKeyAlias() {
+ return Optional.ofNullable(mActiveSecondaryKeyAlias);
+ }
+
+ public synchronized Map<String, WrappedKeyProto.WrappedKey> getAllTertiaryKeys(
+ String secondaryKeyAlias) throws UnexpectedActiveSecondaryOnServerException {
+ if (!secondaryKeyAlias.equals(mActiveSecondaryKeyAlias)) {
+ throw new UnexpectedActiveSecondaryOnServerException(
+ String.format(
+ Locale.US,
+ "Requested tertiary keys wrapped with %s but %s was active secondary.",
+ secondaryKeyAlias,
+ mActiveSecondaryKeyAlias));
+ }
+
+ if (!mWrappedKeyStore.containsKey(secondaryKeyAlias)) {
+ return Collections.emptyMap();
+ }
+ return new HashMap<>(mWrappedKeyStore.get(secondaryKeyAlias));
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java
new file mode 100644
index 000000000000..4cd8333b1a5e
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/fakes/FakeCryptoBackupServerTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.testing.fakes;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+
+import android.util.Pair;
+
+import com.android.server.backup.encryption.client.UnexpectedActiveSecondaryOnServerException;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class FakeCryptoBackupServerTest {
+ private static final String PACKAGE_NAME_1 = "package1";
+ private static final String PACKAGE_NAME_2 = "package2";
+ private static final String PACKAGE_NAME_3 = "package3";
+ private static final WrappedKeyProto.WrappedKey PACKAGE_KEY_1 = createWrappedKey("key1");
+ private static final WrappedKeyProto.WrappedKey PACKAGE_KEY_2 = createWrappedKey("key2");
+ private static final WrappedKeyProto.WrappedKey PACKAGE_KEY_3 = createWrappedKey("key3");
+
+ private FakeCryptoBackupServer mServer;
+
+ @Before
+ public void setUp() {
+ mServer = new FakeCryptoBackupServer();
+ }
+
+ @Test
+ public void getActiveSecondaryKeyAlias_isInitiallyAbsent() throws Exception {
+ assertFalse(mServer.getActiveSecondaryKeyAlias().isPresent());
+ }
+
+ @Test
+ public void setActiveSecondaryKeyAlias_setsTheKeyAlias() throws Exception {
+ String keyAlias = "test";
+ mServer.setActiveSecondaryKeyAlias(keyAlias, Collections.emptyMap());
+ assertThat(mServer.getActiveSecondaryKeyAlias().get()).isEqualTo(keyAlias);
+ }
+
+ @Test
+ public void getAllTertiaryKeys_returnsWrappedKeys() throws Exception {
+ Map<String, WrappedKeyProto.WrappedKey> entries =
+ createKeyMap(
+ new Pair<>(PACKAGE_NAME_1, PACKAGE_KEY_1),
+ new Pair<>(PACKAGE_NAME_2, PACKAGE_KEY_2));
+ String secondaryKeyAlias = "doge";
+ mServer.setActiveSecondaryKeyAlias(secondaryKeyAlias, entries);
+
+ assertThat(mServer.getAllTertiaryKeys(secondaryKeyAlias)).containsExactlyEntriesIn(entries);
+ }
+
+ @Test
+ public void addTertiaryKeys_updatesExistingSet() throws Exception {
+ String keyId = "karlin";
+ WrappedKeyProto.WrappedKey replacementKey = createWrappedKey("some replacement bytes");
+
+ mServer.setActiveSecondaryKeyAlias(
+ keyId,
+ createKeyMap(
+ new Pair<>(PACKAGE_NAME_1, PACKAGE_KEY_1),
+ new Pair<>(PACKAGE_NAME_2, PACKAGE_KEY_2)));
+
+ mServer.setActiveSecondaryKeyAlias(
+ keyId,
+ createKeyMap(
+ new Pair<>(PACKAGE_NAME_1, replacementKey),
+ new Pair<>(PACKAGE_NAME_3, PACKAGE_KEY_3)));
+
+ assertThat(mServer.getAllTertiaryKeys(keyId))
+ .containsExactlyEntriesIn(
+ createKeyMap(
+ new Pair<>(PACKAGE_NAME_1, replacementKey),
+ new Pair<>(PACKAGE_NAME_2, PACKAGE_KEY_2),
+ new Pair<>(PACKAGE_NAME_3, PACKAGE_KEY_3)));
+ }
+
+ @Test
+ public void getAllTertiaryKeys_throwsForUnknownSecondaryKeyAlias() throws Exception {
+ assertThrows(
+ UnexpectedActiveSecondaryOnServerException.class,
+ () -> mServer.getAllTertiaryKeys("unknown"));
+ }
+
+ @Test
+ public void uploadIncrementalBackup_throwsUnsupportedOperationException() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ mServer.uploadIncrementalBackup(
+ PACKAGE_NAME_1,
+ "docid",
+ new byte[0],
+ new WrappedKeyProto.WrappedKey()));
+ }
+
+ @Test
+ public void uploadNonIncrementalBackup_throwsUnsupportedOperationException() {
+ assertThrows(
+ UnsupportedOperationException.class,
+ () ->
+ mServer.uploadNonIncrementalBackup(
+ PACKAGE_NAME_1, new byte[0], new WrappedKeyProto.WrappedKey()));
+ }
+
+ private static WrappedKeyProto.WrappedKey createWrappedKey(String data) {
+ WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
+ wrappedKey.key = data.getBytes(Charset.forName("UTF-8"));
+ return wrappedKey;
+ }
+
+ private Map<String, WrappedKeyProto.WrappedKey> createKeyMap(
+ Pair<String, WrappedKeyProto.WrappedKey>... pairs) {
+ Map<String, WrappedKeyProto.WrappedKey> map = new HashMap<>();
+ for (Pair<String, WrappedKeyProto.WrappedKey> pair : pairs) {
+ map.put(pair.first, pair.second);
+ }
+ return map;
+ }
+}