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

Commit 67638535 authored by Ruslan Tkhakokhov's avatar Ruslan Tkhakokhov
Browse files

Import InitializeRecoverableSecondaryKeyTask

Bug: 111386661
Test: atest InitializeRecoverableSecondaryKeyTaskTest
Change-Id: I95a3acddfc1e28f6addd71af312815d9c3cb1ed6
parent 87778b86
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);
    }
}