diff options
Diffstat (limited to 'packages/BackupEncryption')
5 files changed, 521 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java index 2010620f76ed..033f1b10118c 100644 --- a/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/CryptoSettings.java @@ -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(); diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java new file mode 100644 index 000000000000..91b57cf69795 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationScheduler.java @@ -0,0 +1,116 @@ +/* + * 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()); + } +} diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java new file mode 100644 index 000000000000..77cfded32173 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTask.java @@ -0,0 +1,104 @@ +/* + * 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); + } + } + } +} diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java new file mode 100644 index 000000000000..c31d19d8568c --- /dev/null +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/SecondaryKeyRotationSchedulerTest.java @@ -0,0 +1,179 @@ +/* + * 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; + } + } +} diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java new file mode 100644 index 000000000000..4ac4fa8d06c9 --- /dev/null +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/StartSecondaryKeyRotationTaskTest.java @@ -0,0 +1,113 @@ +/* + * 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()); + } +} |