Migrate CryptoSettings
Bug: 111386661 Test: atest BackupEncryptionRoboTests Change-Id: I67df369594cebfa8f7a67076c990f5493c0e2189
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ android_robolectric_test {
|
||||
"testng",
|
||||
"truth-prebuilt",
|
||||
],
|
||||
static_libs: [
|
||||
"androidx.test.core",
|
||||
"androidx.test.runner",
|
||||
"androidx.test.rules",
|
||||
],
|
||||
instrumentation_for: "BackupEncryption",
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user