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

Commit 2397afe6 authored by Ruslan Tkhakokhov's avatar Ruslan Tkhakokhov Committed by Android (Google) Code Review
Browse files

Merge "Import InitializeRecoverableSecondaryKeyTask"

parents cb17c921 67638535
Loading
Loading
Loading
Loading
+157 −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.tasks;

import android.content.Context;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.security.keystore.recovery.RecoveryController;
import android.util.Slog;

import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;

import java.security.InvalidKeyException;
import java.security.UnrecoverableKeyException;
import java.util.Collections;
import java.util.Optional;

/**
 * Initializes the device for encrypted backup, through generating a secondary key, and setting its
 * alias in the settings.
 *
 * <p>If the device is already initialized, this is a no-op.
 */
public class InitializeRecoverableSecondaryKeyTask {
    private static final String TAG = "InitializeRecoverableSecondaryKeyTask";

    private final Context mContext;
    private final CryptoSettings mCryptoSettings;
    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
    private final CryptoBackupServer mBackupServer;

    /**
     * A new instance.
     *
     * @param cryptoSettings Settings to store the active key alias.
     * @param secondaryKeyManager Key manager to generate the new active secondary key.
     * @param backupServer Server with which to sync the active key alias.
     */
    public InitializeRecoverableSecondaryKeyTask(
            Context context,
            CryptoSettings cryptoSettings,
            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
            CryptoBackupServer backupServer) {
        mContext = context;
        mCryptoSettings = cryptoSettings;
        mSecondaryKeyManager = secondaryKeyManager;
        mBackupServer = backupServer;
    }

    /**
     * Initializes the device for encrypted backup, by generating a recoverable secondary key, then
     * sending that alias to the backup server and saving it in local settings.
     *
     * <p>If there is already an active secondary key then does nothing. If the active secondary key
     * is destroyed then throws {@link InvalidKeyException}.
     *
     * <p>If a key rotation is pending and able to finish (i.e., the new key has synced with the
     * remote trusted hardware module), then it completes the rotation before returning the key.
     *
     * @return The active secondary key.
     * @throws InvalidKeyException if the secondary key is in a bad state.
     */
    public RecoverableKeyStoreSecondaryKey run()
            throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
                    InternalRecoveryServiceException {
        // Complete any pending key rotations
        new RotateSecondaryKeyTask(
                        mContext,
                        mSecondaryKeyManager,
                        mBackupServer,
                        mCryptoSettings,
                        RecoveryController.getInstance(mContext))
                .run();

        return runInternal();
    }

    private RecoverableKeyStoreSecondaryKey runInternal()
            throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
                    InternalRecoveryServiceException {
        Optional<RecoverableKeyStoreSecondaryKey> maybeActiveKey = loadFromSetting();

        if (maybeActiveKey.isPresent()) {
            assertKeyNotDestroyed(maybeActiveKey.get());
            Slog.d(TAG, "Secondary key already initialized: " + maybeActiveKey.get().getAlias());
            return maybeActiveKey.get();
        }

        Slog.v(TAG, "Initializing for crypto: generating a secondary key.");
        RecoverableKeyStoreSecondaryKey key = mSecondaryKeyManager.generate();

        String alias = key.getAlias();
        Slog.i(TAG, "Generated new secondary key " + alias);

        // No tertiary keys yet as we are creating a brand new secondary (without rotation).
        mBackupServer.setActiveSecondaryKeyAlias(alias, /*tertiaryKeys=*/ Collections.emptyMap());
        Slog.v(TAG, "Successfully synced %s " + alias + " with server.");

        mCryptoSettings.initializeWithKeyAlias(alias);
        Slog.v(TAG, "Successfully saved " + alias + " as active secondary to disk.");

        return key;
    }

    private void assertKeyNotDestroyed(RecoverableKeyStoreSecondaryKey key)
            throws InvalidKeyException {
        if (key.getStatus(mContext) == RecoverableKeyStoreSecondaryKey.Status.DESTROYED) {
            throw new InvalidKeyException("Key destroyed: " + key.getAlias());
        }
    }

    private Optional<RecoverableKeyStoreSecondaryKey> loadFromSetting()
            throws InvalidKeyException, UnrecoverableKeyException,
                    InternalRecoveryServiceException {

        // TODO: b/141856950.
        if (!mCryptoSettings.getIsInitialized()) {
            return Optional.empty();
        }

        Optional<String> maybeAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
        if (!maybeAlias.isPresent()) {
            throw new InvalidKeyException(
                    "Settings said crypto was initialized, but there was no active secondary"
                            + " alias");
        }

        String alias = maybeAlias.get();

        Optional<RecoverableKeyStoreSecondaryKey> key;
        key = mSecondaryKeyManager.get(alias);

        if (!key.isPresent()) {
            throw new InvalidKeyException(
                    "Initialized with key but it was not in key store: " + alias);
        }

        return key;
    }
}
+165 −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.tasks;

import static android.security.keystore.recovery.RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;

import static com.google.common.truth.Truth.assertThat;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
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.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import com.android.server.testing.fakes.FakeCryptoBackupServer;
import com.android.server.testing.shadows.ShadowRecoveryController;

import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.util.Optional;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@Config(shadows = {ShadowRecoveryController.class})
@RunWith(RobolectricTestRunner.class)
public class InitializeRecoverableSecondaryKeyTaskTest {
    @Mock private CryptoSettings mMockCryptoSettings;

    private Application mApplication;
    private InitializeRecoverableSecondaryKeyTask mTask;
    private CryptoSettings mCryptoSettings;
    private FakeCryptoBackupServer mFakeCryptoBackupServer;
    private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        ShadowRecoveryController.reset();

        mApplication = ApplicationProvider.getApplicationContext();
        mFakeCryptoBackupServer = new FakeCryptoBackupServer();
        mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
        mSecondaryKeyManager =
                new RecoverableKeyStoreSecondaryKeyManager(
                        RecoveryController.getInstance(mApplication), new SecureRandom());

        mTask =
                new InitializeRecoverableSecondaryKeyTask(
                        mApplication, mCryptoSettings, mSecondaryKeyManager, mFakeCryptoBackupServer);
    }

    @Test
    public void testRun_generatesNewKeyInRecoveryController() throws Exception {
        RecoverableKeyStoreSecondaryKey key = mTask.run();

        assertThat(RecoveryController.getInstance(mApplication).getAliases())
                .contains(key.getAlias());
    }

    @Test
    public void testRun_setsAliasOnServer() throws Exception {
        RecoverableKeyStoreSecondaryKey key = mTask.run();

        assertThat(mFakeCryptoBackupServer.getActiveSecondaryKeyAlias().get())
                .isEqualTo(key.getAlias());
    }

    @Test
    public void testRun_setsAliasInSettings() throws Exception {
        RecoverableKeyStoreSecondaryKey key = mTask.run();

        assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(key.getAlias());
    }

    @Test
    public void testRun_initializesSettings() throws Exception {
        mTask.run();

        assertThat(mCryptoSettings.getIsInitialized()).isTrue();
    }

    @Test
    public void testRun_initializeSettingsFails_throws() throws Exception {
        useMockCryptoSettings();
        doThrow(IllegalArgumentException.class)
                .when(mMockCryptoSettings)
                .initializeWithKeyAlias(any());


        assertThrows(IllegalArgumentException.class, () -> mTask.run());
    }

    @Test
    public void testRun_doesNotGenerateANewKeyIfOneIsAvailable() throws Exception {
        RecoverableKeyStoreSecondaryKey key1 = mTask.run();
        RecoverableKeyStoreSecondaryKey key2 = mTask.run();

        assertThat(key1.getAlias()).isEqualTo(key2.getAlias());
        assertThat(key2.getSecretKey()).isEqualTo(key2.getSecretKey());
    }

    @Test
    public void testRun_existingKeyButDestroyed_throws() throws Exception {
        RecoverableKeyStoreSecondaryKey key = mTask.run();
        ShadowRecoveryController.setRecoveryStatus(
                key.getAlias(), RECOVERY_STATUS_PERMANENT_FAILURE);

        assertThrows(InvalidKeyException.class, () -> mTask.run());
    }

    @Test
    public void testRun_settingsInitializedButNotSecondaryKeyAlias_throws() {
        useMockCryptoSettings();
        when(mMockCryptoSettings.getIsInitialized()).thenReturn(true);
        when(mMockCryptoSettings.getActiveSecondaryKeyAlias()).thenReturn(Optional.empty());

        assertThrows(InvalidKeyException.class, () -> mTask.run());
    }

    @Test
    public void testRun_keyAliasSetButNotInStore_throws() {
        useMockCryptoSettings();
        when(mMockCryptoSettings.getIsInitialized()).thenReturn(true);
        when(mMockCryptoSettings.getActiveSecondaryKeyAlias())
                .thenReturn(Optional.of("missingAlias"));

        assertThrows(InvalidKeyException.class, () -> mTask.run());
    }

    private void useMockCryptoSettings() {
        mTask =
                new InitializeRecoverableSecondaryKeyTask(
                        mApplication,
                        mMockCryptoSettings,
                        mSecondaryKeyManager,
                        mFakeCryptoBackupServer);
    }
}