diff options
Diffstat (limited to 'packages/BackupEncryption')
3 files changed, 450 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java new file mode 100644 index 000000000000..2010620f76ed --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java @@ -0,0 +1,233 @@ +/* + * 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; + +import static com.android.internal.util.Preconditions.checkNotNull; +import static com.android.internal.util.Preconditions.checkState; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.security.keystore.recovery.InternalRecoveryServiceException; +import android.security.keystore.recovery.RecoveryController; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; + +import java.security.KeyStoreException; +import java.util.Optional; + +/** + * State about encrypted backups that needs to be remembered. + */ +public class CryptoSettings { + + private static final String TAG = "CryptoSettings"; + + private static final String SHARED_PREFERENCES_NAME = "crypto_settings"; + + private static final String KEY_IS_INITIALIZED = "isInitialized"; + private static final String KEY_ACTIVE_SECONDARY_ALIAS = "activeSecondary"; + private static final String KEY_NEXT_SECONDARY_ALIAS = "nextSecondary"; + private static final String SECONDARY_KEY_LAST_ROTATED_AT = "secondaryKeyLastRotatedAt"; + private static final String[] SETTINGS_FOR_BACKUP = { + KEY_IS_INITIALIZED, + KEY_ACTIVE_SECONDARY_ALIAS, + KEY_NEXT_SECONDARY_ALIAS, + SECONDARY_KEY_LAST_ROTATED_AT + }; + + private static final String KEY_ANCESTRAL_SECONDARY_KEY_VERSION = + "ancestral_secondary_key_version"; + + private final SharedPreferences mSharedPreferences; + private final Context mContext; + + /** + * A new instance. + * + * @param context For looking up the {@link SharedPreferences}, for storing state. + * @return The instance. + */ + public static CryptoSettings getInstance(Context context) { + // We need single process mode because CryptoSettings may be used from several processes + // simultaneously. + SharedPreferences sharedPreferences = + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + return new CryptoSettings(sharedPreferences, context); + } + + /** + * A new instance using {@link SharedPreferences} in the default mode. + * + * <p>This will not work across multiple processes but will work in tests. + */ + @VisibleForTesting + public static CryptoSettings getInstanceForTesting(Context context) { + SharedPreferences sharedPreferences = + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + return new CryptoSettings(sharedPreferences, context); + } + + private CryptoSettings(SharedPreferences sharedPreferences, Context context) { + mSharedPreferences = checkNotNull(sharedPreferences); + mContext = checkNotNull(context); + } + + /** + * The alias of the current active secondary key. This should be used to retrieve the key from + * AndroidKeyStore. + */ + public Optional<String> getActiveSecondaryKeyAlias() { + return getStringInSharedPrefs(KEY_ACTIVE_SECONDARY_ALIAS); + } + + /** + * The alias of the secondary key to which the client is rotating. The rotation is not + * immediate, which is why this setting is needed. Once the next key is created, it can take up + * to 72 hours potentially (or longer if the user has no network) for the next key to be synced + * with the keystore. Only after that has happened does the client attempt to re-wrap all + * tertiary keys and commit the rotation. + */ + public Optional<String> getNextSecondaryKeyAlias() { + return getStringInSharedPrefs(KEY_NEXT_SECONDARY_ALIAS); + } + + /** + * If the settings have been initialized. + */ + public boolean getIsInitialized() { + return mSharedPreferences.getBoolean(KEY_IS_INITIALIZED, false); + } + + /** + * Sets the alias of the currently active secondary key. + * + * @param activeAlias The alias, as in AndroidKeyStore. + * @throws IllegalArgumentException if the alias is not in the user's keystore. + */ + public void setActiveSecondaryKeyAlias(String activeAlias) throws IllegalArgumentException { + assertIsValidAlias(activeAlias); + mSharedPreferences.edit().putString(KEY_ACTIVE_SECONDARY_ALIAS, activeAlias).apply(); + } + + /** + * Sets the alias of the secondary key to which the client is rotating. + * + * @param nextAlias The alias, as in AndroidKeyStore. + * @throws KeyStoreException if unable to check whether alias is valid in the keystore. + * @throws IllegalArgumentException if the alias is not in the user's keystore. + */ + public void setNextSecondaryAlias(String nextAlias) throws IllegalArgumentException { + assertIsValidAlias(nextAlias); + mSharedPreferences.edit().putString(KEY_NEXT_SECONDARY_ALIAS, nextAlias).apply(); + } + + /** + * Unsets the alias of the key to which the client is rotating. This is generally performed once + * a rotation is complete. + */ + public void removeNextSecondaryKeyAlias() { + mSharedPreferences.edit().remove(KEY_NEXT_SECONDARY_ALIAS).apply(); + } + + /** + * Sets the timestamp of when the secondary key was last rotated. + * + * @param timestamp The timestamp to set. + */ + public void setSecondaryLastRotated(long timestamp) { + mSharedPreferences.edit().putLong(SECONDARY_KEY_LAST_ROTATED_AT, timestamp).apply(); + } + + /** + * Returns a timestamp of when the secondary key was last rotated. + * + * @return The timestamp. + */ + public Optional<Long> getSecondaryLastRotated() { + if (!mSharedPreferences.contains(SECONDARY_KEY_LAST_ROTATED_AT)) { + return Optional.empty(); + } + return Optional.of(mSharedPreferences.getLong(SECONDARY_KEY_LAST_ROTATED_AT, -1)); + } + + /** + * Sets the settings to have been initialized. (Otherwise loading should try to initialize + * again.) + */ + private void setIsInitialized() { + mSharedPreferences.edit().putBoolean(KEY_IS_INITIALIZED, true).apply(); + } + + /** + * Initializes with the given key alias. + * + * @param alias The secondary key alias to be set as active. + * @throws IllegalArgumentException if the alias does not reference a valid key. + * @throws IllegalStateException if attempting to initialize an already initialized settings. + */ + public void initializeWithKeyAlias(String alias) throws IllegalArgumentException { + checkState( + !getIsInitialized(), "Attempting to initialize an already initialized settings."); + setActiveSecondaryKeyAlias(alias); + setIsInitialized(); + } + + /** Returns the secondary key version of the encrypted backup set to restore from (if set). */ + public Optional<String> getAncestralSecondaryKeyVersion() { + return Optional.ofNullable( + mSharedPreferences.getString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, null)); + } + + /** Sets the secondary key version of the encrypted backup set to restore from. */ + public void setAncestralSecondaryKeyVersion(String ancestralSecondaryKeyVersion) { + mSharedPreferences + .edit() + .putString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, ancestralSecondaryKeyVersion) + .apply(); + } + + /** Deletes all crypto settings related to backup (as opposed to restore). */ + public void clearAllSettingsForBackup() { + Editor sharedPrefsEditor = mSharedPreferences.edit(); + for (String backupSettingKey : SETTINGS_FOR_BACKUP) { + sharedPrefsEditor.remove(backupSettingKey); + } + sharedPrefsEditor.apply(); + + Slog.d(TAG, "Cleared crypto settings for backup"); + } + + /** + * Throws {@link IllegalArgumentException} if the alias does not refer to a key that is in + * the {@link RecoveryController}. + */ + private void assertIsValidAlias(String alias) throws IllegalArgumentException { + try { + if (!RecoveryController.getInstance(mContext).getAliases().contains(alias)) { + throw new IllegalArgumentException(alias + " is not in RecoveryController"); + } + } catch (InternalRecoveryServiceException e) { + throw new IllegalArgumentException("Problem accessing recovery service", e); + } + } + + private Optional<String> getStringInSharedPrefs(String key) { + return Optional.ofNullable(mSharedPreferences.getString(key, null)); + } +} diff --git a/packages/BackupEncryption/test/robolectric/Android.bp b/packages/BackupEncryption/test/robolectric/Android.bp index f84be6d4f3f4..4e42ce7366f0 100644 --- a/packages/BackupEncryption/test/robolectric/Android.bp +++ b/packages/BackupEncryption/test/robolectric/Android.bp @@ -25,6 +25,11 @@ android_robolectric_test { "testng", "truth-prebuilt", ], + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "androidx.test.rules", + ], instrumentation_for: "BackupEncryption", } diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java new file mode 100644 index 000000000000..979b3d5dc13a --- /dev/null +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java @@ -0,0 +1,212 @@ +/* + * 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; + +import static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; + +import android.app.Application; +import android.security.keystore.recovery.RecoveryController; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.server.testing.shadows.ShadowRecoveryController; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Optional; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowRecoveryController.class) +public class CryptoSettingsTest { + + private static final String TEST_KEY_ALIAS = + "com.android.server.backup.encryption/keystore/08120c326b928ff34c73b9c58581da63"; + + private CryptoSettings mCryptoSettings; + private Application mApplication; + + @Before + public void setUp() { + ShadowRecoveryController.reset(); + + mApplication = ApplicationProvider.getApplicationContext(); + mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication); + } + + @Test + public void getActiveSecondaryAlias_isInitiallyAbsent() { + assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse(); + } + + @Test + public void getActiveSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS); + assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS); + } + + @Test + public void getNextSecondaryAlias_isInitiallyAbsent() { + assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse(); + } + + @Test + public void getNextSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS); + assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS); + } + + @Test + public void isInitialized_isInitiallyFalse() { + assertThat(mCryptoSettings.getIsInitialized()).isFalse(); + } + + @Test + public void setActiveSecondaryAlias_throwsIfKeyIsNotInRecoveryController() { + assertThrows( + IllegalArgumentException.class, + () -> mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS)); + } + + @Test + public void setNextSecondaryAlias_inRecoveryController_setsAlias() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + + mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS); + + assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS); + } + + @Test + public void setNextSecondaryAlias_throwsIfKeyIsNotInRecoveryController() { + assertThrows( + IllegalArgumentException.class, + () -> mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS)); + } + + @Test + public void removeNextSecondaryAlias_removesIt() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS); + + mCryptoSettings.removeNextSecondaryKeyAlias(); + + assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse(); + } + + @Test + public void initializeWithKeyAlias_setsAsInitialized() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS); + assertThat(mCryptoSettings.getIsInitialized()).isTrue(); + } + + @Test + public void initializeWithKeyAlias_setsActiveAlias() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS); + assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS); + } + + @Test + public void initializeWithKeyAlias_throwsIfKeyIsNotInRecoveryController() { + assertThrows( + IllegalArgumentException.class, + () -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS)); + } + + @Test + public void initializeWithKeyAlias_throwsIfAlreadyInitialized() throws Exception { + setAliasIsInRecoveryController(TEST_KEY_ALIAS); + mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS); + + assertThrows( + IllegalStateException.class, + () -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS)); + } + + @Test + public void getSecondaryLastRotated_returnsEmptyInitially() { + assertThat(mCryptoSettings.getSecondaryLastRotated()).isEqualTo(Optional.empty()); + } + + @Test + public void getSecondaryLastRotated_returnsTimestampAfterItIsSet() { + long timestamp = 1000001; + + mCryptoSettings.setSecondaryLastRotated(timestamp); + + assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(timestamp); + } + + @Test + public void getAncestralSecondaryKeyVersion_notSet_returnsOptionalAbsent() { + assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().isPresent()).isFalse(); + } + + @Test + public void getAncestralSecondaryKeyVersion_isSet_returnsSetValue() { + String secondaryKeyVersion = "some_secondary_key"; + mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion); + + assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get()) + .isEqualTo(secondaryKeyVersion); + } + + @Test + public void getAncestralSecondaryKeyVersion_isSetMultipleTimes_returnsLastSetValue() { + String secondaryKeyVersion1 = "some_secondary_key"; + String secondaryKeyVersion2 = "another_secondary_key"; + mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion1); + mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion2); + + assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get()) + .isEqualTo(secondaryKeyVersion2); + } + + @Test + public void clearAllSettingsForBackup_clearsStateForBackup() throws Exception { + String key1 = "key1"; + String key2 = "key2"; + String ancestralKey = "ancestral_key"; + setAliasIsInRecoveryController(key1); + setAliasIsInRecoveryController(key2); + mCryptoSettings.setActiveSecondaryKeyAlias(key1); + mCryptoSettings.setNextSecondaryAlias(key2); + mCryptoSettings.setSecondaryLastRotated(100001); + mCryptoSettings.setAncestralSecondaryKeyVersion(ancestralKey); + + mCryptoSettings.clearAllSettingsForBackup(); + + assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse(); + assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse(); + assertThat(mCryptoSettings.getSecondaryLastRotated().isPresent()).isFalse(); + assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get()).isEqualTo(ancestralKey); + } + + private void setAliasIsInRecoveryController(String alias) throws Exception { + RecoveryController recoveryController = RecoveryController.getInstance(mApplication); + recoveryController.generateKey(alias); + } +} |