summaryrefslogtreecommitdiff
path: root/packages/BackupEncryption
diff options
context:
space:
mode:
Diffstat (limited to 'packages/BackupEncryption')
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java233
-rw-r--r--packages/BackupEncryption/test/robolectric/Android.bp5
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/CryptoSettingsTest.java212
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);
+ }
+}