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

Commit 96b5925f authored by Al Sutton's avatar Al Sutton
Browse files

Import SecondaryKeyRotationScheduler

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I53b54baaac744dea77d9e3c30fd0171430c2887f
parent 9f8672db
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import com.android.internal.annotations.VisibleForTesting;

import java.security.KeyStoreException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * State about encrypted backups that needs to be remembered.
@@ -51,6 +52,9 @@ public class CryptoSettings {
        SECONDARY_KEY_LAST_ROTATED_AT
    };

    private static final long DEFAULT_SECONDARY_KEY_ROTATION_PERIOD =
            TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS);

    private static final String KEY_ANCESTRAL_SECONDARY_KEY_VERSION =
            "ancestral_secondary_key_version";

@@ -202,6 +206,11 @@ public class CryptoSettings {
                .apply();
    }

    /** The number of milliseconds between secondary key rotation */
    public long backupSecondaryKeyRotationIntervalMs() {
        return DEFAULT_SECONDARY_KEY_ROTATION_PERIOD;
    }

    /** Deletes all crypto settings related to backup (as opposed to restore). */
    public void clearAllSettingsForBackup() {
        Editor sharedPrefsEditor = mSharedPreferences.edit();
+116 −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.keys;

import android.content.Context;
import android.util.Slog;

import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;

import java.io.File;
import java.time.Clock;
import java.util.Optional;

/**
 * Helps schedule rotations of secondary keys.
 *
 * <p>TODO(b/72028016) Replace with a job.
 */
public class SecondaryKeyRotationScheduler {

    private static final String TAG = "SecondaryKeyRotationScheduler";
    private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";

    private final Context mContext;
    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
    private final CryptoSettings mCryptoSettings;
    private final Clock mClock;

    public SecondaryKeyRotationScheduler(
            Context context,
            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
            CryptoSettings cryptoSettings,
            Clock clock) {
        mContext = context;
        mCryptoSettings = cryptoSettings;
        mClock = clock;
        mSecondaryKeyManager = secondaryKeyManager;
    }

    /**
     * Returns {@code true} if a sentinel file for forcing secondary key rotation is present. This
     * is only for testing purposes.
     */
    private boolean isForceRotationTestSentinelPresent() {
        File file = new File(mContext.getFilesDir(), SENTINEL_FILE_PATH);
        if (file.exists()) {
            file.delete();
            return true;
        }
        return false;
    }

    /** Start the key rotation task if it's time to do so */
    public void startRotationIfScheduled() {
        if (isForceRotationTestSentinelPresent()) {
            Slog.i(TAG, "Found force flag for secondary rotation. Starting now.");
            startRotation();
            return;
        }

        Optional<Long> maybeLastRotated = mCryptoSettings.getSecondaryLastRotated();
        if (!maybeLastRotated.isPresent()) {
            Slog.v(TAG, "No previous rotation, scheduling from now.");
            scheduleRotationFromNow();
            return;
        }

        long lastRotated = maybeLastRotated.get();
        long now = mClock.millis();

        if (lastRotated > now) {
            Slog.i(TAG, "Last rotation was in the future. Clock must have changed. Rotate now.");
            startRotation();
            return;
        }

        long millisSinceLastRotation = now - lastRotated;
        long rotationInterval = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();
        if (millisSinceLastRotation >= rotationInterval) {
            Slog.i(
                    TAG,
                    "Last rotation was more than "
                            + rotationInterval
                            + "ms ("
                            + millisSinceLastRotation
                            + "ms) in the past. Rotate now.");
            startRotation();
        }

        Slog.v(TAG, "No rotation required, last " + lastRotated + ".");
    }

    private void startRotation() {
        scheduleRotationFromNow();
        new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager).run();
    }

    private void scheduleRotationFromNow() {
        mCryptoSettings.setSecondaryLastRotated(mClock.millis());
    }
}
+104 −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.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.util.Slog;

import com.android.internal.util.Preconditions;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;

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

/**
 * Starts rotating to a new secondary key. Cannot complete until the screen is unlocked and the new
 * key is synced.
 */
public class StartSecondaryKeyRotationTask {
    private static final String TAG = "BE-StSecondaryKeyRotTsk";

    private final CryptoSettings mCryptoSettings;
    private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;

    public StartSecondaryKeyRotationTask(
            CryptoSettings cryptoSettings,
            RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager) {
        mCryptoSettings = Preconditions.checkNotNull(cryptoSettings);
        mSecondaryKeyManager = Preconditions.checkNotNull(secondaryKeyManager);
    }

    /** Begin the key rotation */
    public void run() {
        Slog.i(TAG, "Attempting to initiate a secondary key rotation.");

        Optional<String> maybeCurrentAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
        if (!maybeCurrentAlias.isPresent()) {
            Slog.w(TAG, "No active current alias. Cannot trigger a secondary rotation.");
            return;
        }
        String currentAlias = maybeCurrentAlias.get();

        Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
        if (maybeNextAlias.isPresent()) {
            String nextAlias = maybeNextAlias.get();
            if (nextAlias.equals(currentAlias)) {
                // Shouldn't be possible, but guard against accidentally deleting the active key.
                Slog.e(TAG, "Was already trying to rotate to what is already the active key.");
            } else {
                Slog.w(TAG, "Was already rotating to another key. Cancelling that.");
                try {
                    mSecondaryKeyManager.remove(nextAlias);
                } catch (Exception e) {
                    Slog.wtf(TAG, "Could not remove old key", e);
                }
            }
            mCryptoSettings.removeNextSecondaryKeyAlias();
        }

        RecoverableKeyStoreSecondaryKey newSecondaryKey;
        try {
            newSecondaryKey = mSecondaryKeyManager.generate();
        } catch (LockScreenRequiredException e) {
            Slog.e(TAG, "No lock screen is set - cannot generate a new key to rotate to.", e);
            return;
        } catch (InternalRecoveryServiceException e) {
            Slog.e(TAG, "Internal error in Recovery Controller, failed to rotate key.", e);
            return;
        } catch (UnrecoverableKeyException e) {
            Slog.e(TAG, "Failed to get key after generating, failed to rotate", e);
            return;
        }

        String alias = newSecondaryKey.getAlias();
        Slog.i(TAG, "Generated a new secondary key with alias '" + alias + "'.");
        try {
            mCryptoSettings.setNextSecondaryAlias(alias);
            Slog.i(TAG, "Successfully set '" + alias + "' as next key to rotate to");
        } catch (IllegalArgumentException e) {
            Slog.e(TAG, "Unexpected error setting next alias", e);
            try {
                mSecondaryKeyManager.remove(alias);
            } catch (Exception err) {
                Slog.wtf(TAG, "Failed to remove generated key after encountering error", err);
            }
        }
    }
}
+179 −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.keys;

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

import static org.mockito.Mockito.when;

import android.content.Context;
import android.platform.test.annotations.Presubmit;

import androidx.test.core.app.ApplicationProvider;

import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;

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.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;

import java.io.File;
import java.time.Clock;

@Config(shadows = SecondaryKeyRotationSchedulerTest.ShadowStartSecondaryKeyRotationTask.class)
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class SecondaryKeyRotationSchedulerTest {
    private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";

    @Mock private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
    @Mock private Clock mClock;

    private CryptoSettings mCryptoSettings;
    private SecondaryKeyRotationScheduler mScheduler;
    private long mRotationIntervalMillis;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        Context application = ApplicationProvider.getApplicationContext();

        mCryptoSettings = CryptoSettings.getInstanceForTesting(application);
        mRotationIntervalMillis = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();

        mScheduler =
                new SecondaryKeyRotationScheduler(
                        application, mSecondaryKeyManager, mCryptoSettings, mClock);
        ShadowStartSecondaryKeyRotationTask.reset();
    }

    @Test
    public void startRotationIfScheduled_rotatesIfRotationWasFarEnoughInThePast() {
        long lastRotated = 100009;
        mCryptoSettings.setSecondaryLastRotated(lastRotated);
        setNow(lastRotated + mRotationIntervalMillis);

        mScheduler.startRotationIfScheduled();

        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
    }

    @Test
    public void startRotationIfScheduled_setsNewRotationTimeIfRotationWasFarEnoughInThePast() {
        long lastRotated = 100009;
        long now = lastRotated + mRotationIntervalMillis;
        mCryptoSettings.setSecondaryLastRotated(lastRotated);
        setNow(now);

        mScheduler.startRotationIfScheduled();

        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
    }

    @Test
    public void startRotationIfScheduled_rotatesIfClockHasChanged() {
        long lastRotated = 100009;
        mCryptoSettings.setSecondaryLastRotated(lastRotated);
        setNow(lastRotated - 1);

        mScheduler.startRotationIfScheduled();

        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
    }

    @Test
    public void startRotationIfScheduled_rotatesIfSentinelFileIsPresent() throws Exception {
        File file = new File(RuntimeEnvironment.application.getFilesDir(), SENTINEL_FILE_PATH);
        file.createNewFile();

        mScheduler.startRotationIfScheduled();

        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isTrue();
    }

    @Test
    public void startRotationIfScheduled_setsNextRotationIfClockHasChanged() {
        long lastRotated = 100009;
        long now = lastRotated - 1;
        mCryptoSettings.setSecondaryLastRotated(lastRotated);
        setNow(now);

        mScheduler.startRotationIfScheduled();

        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
    }

    @Test
    public void startRotationIfScheduled_doesNothingIfRotationWasRecentEnough() {
        long lastRotated = 100009;
        mCryptoSettings.setSecondaryLastRotated(lastRotated);
        setNow(lastRotated + mRotationIntervalMillis - 1);

        mScheduler.startRotationIfScheduled();

        assertThat(ShadowStartSecondaryKeyRotationTask.sRan).isFalse();
    }

    @Test
    public void startRotationIfScheduled_doesNotSetRotationTimeIfRotationWasRecentEnough() {
        long lastRotated = 100009;
        mCryptoSettings.setSecondaryLastRotated(lastRotated);
        setNow(lastRotated + mRotationIntervalMillis - 1);

        mScheduler.startRotationIfScheduled();

        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(lastRotated);
    }

    @Test
    public void startRotationIfScheduled_setsLastRotatedToNowIfNeverRotated() {
        long now = 13295436;
        setNow(now);

        mScheduler.startRotationIfScheduled();

        assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(now);
    }

    private void setNow(long timestamp) {
        when(mClock.millis()).thenReturn(timestamp);
    }

    @Implements(StartSecondaryKeyRotationTask.class)
    public static class ShadowStartSecondaryKeyRotationTask {
        private static boolean sRan = false;

        @Implementation
        public void run() {
            sRan = true;
        }

        @Resetter
        public static void reset() {
            sRan = false;
        }
    }
}
+113 −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 com.google.common.truth.Truth.assertThat;

import android.platform.test.annotations.Presubmit;
import android.security.keystore.recovery.RecoveryController;

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.shadows.ShadowRecoveryController;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;

import java.security.SecureRandom;

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowRecoveryController.class})
@Presubmit
public class StartSecondaryKeyRotationTaskTest {

    private CryptoSettings mCryptoSettings;
    private RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
    private StartSecondaryKeyRotationTask mStartSecondaryKeyRotationTask;

    @Before
    public void setUp() throws Exception {
        mSecondaryKeyManager =
                new RecoverableKeyStoreSecondaryKeyManager(
                        RecoveryController.getInstance(RuntimeEnvironment.application),
                        new SecureRandom());
        mCryptoSettings = CryptoSettings.getInstanceForTesting(RuntimeEnvironment.application);
        mStartSecondaryKeyRotationTask =
                new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager);

        ShadowRecoveryController.reset();
    }

    @Test
    public void run_doesNothingIfNoActiveSecondaryExists() {
        mStartSecondaryKeyRotationTask.run();

        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
    }

    @Test
    public void run_doesNotRemoveExistingNextSecondaryKeyIfItIsAlreadyActive() throws Exception {
        generateAnActiveKey();
        String activeAlias = mCryptoSettings.getActiveSecondaryKeyAlias().get();
        mCryptoSettings.setNextSecondaryAlias(activeAlias);

        mStartSecondaryKeyRotationTask.run();

        assertThat(mSecondaryKeyManager.get(activeAlias).isPresent()).isTrue();
    }

    @Test
    public void run_doesRemoveExistingNextSecondaryKeyIfItIsNotYetActive() throws Exception {
        generateAnActiveKey();
        RecoverableKeyStoreSecondaryKey nextKey = mSecondaryKeyManager.generate();
        String nextAlias = nextKey.getAlias();
        mCryptoSettings.setNextSecondaryAlias(nextAlias);

        mStartSecondaryKeyRotationTask.run();

        assertThat(mSecondaryKeyManager.get(nextAlias).isPresent()).isFalse();
    }

    @Test
    public void run_generatesANewNextSecondaryKey() throws Exception {
        generateAnActiveKey();

        mStartSecondaryKeyRotationTask.run();

        assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isTrue();
    }

    @Test
    public void run_generatesANewKeyThatExistsInKeyStore() throws Exception {
        generateAnActiveKey();

        mStartSecondaryKeyRotationTask.run();

        String nextAlias = mCryptoSettings.getNextSecondaryKeyAlias().get();
        assertThat(mSecondaryKeyManager.get(nextAlias).isPresent()).isTrue();
    }

    private void generateAnActiveKey() throws Exception {
        RecoverableKeyStoreSecondaryKey secondaryKey = mSecondaryKeyManager.generate();
        mCryptoSettings.setActiveSecondaryKeyAlias(secondaryKey.getAlias());
    }
}