diff options
Diffstat (limited to 'packages/BackupEncryption')
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; + } +} |