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

Commit a1148841 authored by Al Sutton's avatar Al Sutton Committed by Android (Google) Code Review
Browse files

Merge "Migrate CryptoSettings"

parents 1f6a8f43 53b1c12e
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);
    }
}