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

Commit 873cdae0 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Import SecondaryKeyRotationScheduler"

parents 4266d333 96b5925f
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());
    }
}