Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 53b1c12e authored by Al Sutton's avatar Al Sutton
Browse files

Migrate CryptoSettings

Bug: 111386661
Test: atest BackupEncryptionRoboTests
Change-Id: I67df369594cebfa8f7a67076c990f5493c0e2189
parent d046fb26
Loading
Loading
Loading
Loading
+233 −0
Original line number Diff line number Diff line
/*
 * 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));
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -25,6 +25,11 @@ android_robolectric_test {
        "testng",
        "truth-prebuilt",
    ],
    static_libs: [
        "androidx.test.core",
        "androidx.test.runner",
        "androidx.test.rules",
    ],
    instrumentation_for: "BackupEncryption",
}

+212 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}