Loading packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java 0 → 100644 +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.keys; import android.content.Context; import com.android.internal.annotations.VisibleForTesting; /** * Schedules tertiary key rotations in a staggered fashion. * * <p>Apps are due a key rotation after a certain number of backups. Rotations are then staggerered * over a period of time, through restricting the number of rotations allowed in a 24-hour window. * This will causes the apps to enter a staggered cycle of regular rotations. * * <p>Note: the methods in this class are not optimized to be super fast. They make blocking IO to * ensure that scheduler information is committed to disk, so that it is available after the user * turns their device off and on. This ought to be fine as * * <ul> * <li>It will be invoked before a backup, so should never be invoked on the UI thread * <li>It will be invoked before a backup, so the vast amount of time is spent on the backup, not * writing tiny amounts of data to disk. * </ul> */ public class TertiaryKeyRotationScheduler { /** Default number of key rotations allowed within 24 hours. */ private static final int KEY_ROTATION_LIMIT = 2; /** A new instance, using {@code context} to determine where to store state. */ public static TertiaryKeyRotationScheduler getInstance(Context context) { TertiaryKeyRotationWindowedCount windowedCount = TertiaryKeyRotationWindowedCount.getInstance(context); TertiaryKeyRotationTracker tracker = TertiaryKeyRotationTracker.getInstance(context); return new TertiaryKeyRotationScheduler(tracker, windowedCount, KEY_ROTATION_LIMIT); } private final TertiaryKeyRotationTracker mTracker; private final TertiaryKeyRotationWindowedCount mWindowedCount; private final int mMaximumRotationsPerWindow; /** * A new instance. * * @param tracker Tracks how many times each application has backed up. * @param windowedCount Tracks how many rotations have happened in the last 24 hours. * @param maximumRotationsPerWindow The maximum number of key rotations allowed per 24 hours. */ @VisibleForTesting TertiaryKeyRotationScheduler( TertiaryKeyRotationTracker tracker, TertiaryKeyRotationWindowedCount windowedCount, int maximumRotationsPerWindow) { mTracker = tracker; mWindowedCount = windowedCount; mMaximumRotationsPerWindow = maximumRotationsPerWindow; } /** * Returns {@code true} if the app with {@code packageName} is due having its key rotated. * * <p>This ought to be queried before backing up an app, to determine whether to do an * incremental backup or a full backup. (A full backup forces key rotation.) */ public boolean isKeyRotationDue(String packageName) { if (mWindowedCount.getCount() >= mMaximumRotationsPerWindow) { return false; } return mTracker.isKeyRotationDue(packageName); } /** * Records that a backup happened for the app with the given {@code packageName}. * * <p>Each backup brings the app closer to the point at which a key rotation is due. */ public void recordBackup(String packageName) { mTracker.recordBackup(packageName); } /** * Records a key rotation happened for the app with the given {@code packageName}. * * <p>This resets the countdown until the next key rotation is due. */ public void recordKeyRotation(String packageName) { mTracker.resetCountdown(packageName); mWindowedCount.record(); } } packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java +21 −7 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * 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. Loading @@ -16,6 +16,8 @@ package com.android.server.backup.encryption.keys; import static com.android.internal.util.Preconditions.checkArgument; import android.content.Context; import android.content.SharedPreferences; import android.util.Slog; Loading Loading @@ -46,15 +48,27 @@ public class TertiaryKeyRotationTracker { */ public static TertiaryKeyRotationTracker getInstance(Context context) { return new TertiaryKeyRotationTracker( context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)); context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE), MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION); } private final SharedPreferences mSharedPreferences; private final int mMaxBackupsTillRotation; /** New instance, storing data in {@code mSharedPreferences}. */ /** * New instance, storing data in {@code sharedPreferences} and initializing backup countdown to * {@code maxBackupsTillRotation}. */ @VisibleForTesting TertiaryKeyRotationTracker(SharedPreferences sharedPreferences) { TertiaryKeyRotationTracker(SharedPreferences sharedPreferences, int maxBackupsTillRotation) { checkArgument( maxBackupsTillRotation >= 0, String.format( Locale.US, "maxBackupsTillRotation should be non-negative but was %d", maxBackupsTillRotation)); mSharedPreferences = sharedPreferences; mMaxBackupsTillRotation = maxBackupsTillRotation; } /** Loading @@ -63,7 +77,7 @@ public class TertiaryKeyRotationTracker { * @param packageName The package name of the app. */ public boolean isKeyRotationDue(String packageName) { return getBackupsSinceRotation(packageName) >= MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION; return getBackupsSinceRotation(packageName) >= mMaxBackupsTillRotation; } /** Loading @@ -84,7 +98,7 @@ public class TertiaryKeyRotationTracker { packageName, Math.max( 0, MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION mMaxBackupsTillRotation - backupsSinceRotation))); } } Loading @@ -102,7 +116,7 @@ public class TertiaryKeyRotationTracker { public void markAllForRotation() { SharedPreferences.Editor editor = mSharedPreferences.edit(); for (String packageName : mSharedPreferences.getAll().keySet()) { editor.putInt(packageName, MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION); editor.putInt(packageName, mMaxBackupsTillRotation); } editor.apply(); } Loading packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java 0 → 100644 +132 −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.internal.annotations.VisibleForTesting; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.time.Clock; import java.util.ArrayList; import java.util.concurrent.TimeUnit; /** * Tracks (and commits to disk) how many key rotations have happened in the last 24 hours. This * allows us to limit (and therefore stagger) the number of key rotations in a given period of time. * * <p>Note to engineers thinking of replacing the below with fancier algorithms and data structures: * we expect the total size of this count at any time to be below however many rotations we allow in * the window, which is going to be in single digits. Any changes that mean we write to disk more * frequently, that the code is no longer resistant to clock changes, or that the code is more * difficult to understand are almost certainly not worthwhile. */ public class TertiaryKeyRotationWindowedCount { private static final String TAG = "TertiaryKeyRotCount"; private static final int WINDOW_IN_HOURS = 24; private static final String LOG_FILE_NAME = "tertiary_key_rotation_windowed_count"; private final Clock mClock; private final File mFile; private ArrayList<Long> mEvents; /** Returns a new instance, persisting state to the files dir of {@code context}. */ public static TertiaryKeyRotationWindowedCount getInstance(Context context) { File logFile = new File(context.getFilesDir(), LOG_FILE_NAME); return new TertiaryKeyRotationWindowedCount(logFile, Clock.systemDefaultZone()); } /** A new instance, committing state to {@code file}, and reading time from {@code clock}. */ @VisibleForTesting TertiaryKeyRotationWindowedCount(File file, Clock clock) { mFile = file; mClock = clock; mEvents = new ArrayList<>(); try { loadFromFile(); } catch (IOException e) { Slog.e(TAG, "Error reading " + LOG_FILE_NAME, e); } } /** Records a key rotation at the current time. */ public void record() { mEvents.add(mClock.millis()); compact(); try { saveToFile(); } catch (IOException e) { Slog.e(TAG, "Error saving " + LOG_FILE_NAME, e); } } /** Returns the number of key rotation that have been recorded in the window. */ public int getCount() { compact(); return mEvents.size(); } private void compact() { long minimumTimestamp = getMinimumTimestamp(); long now = mClock.millis(); ArrayList<Long> compacted = new ArrayList<>(); for (long event : mEvents) { if (event >= minimumTimestamp && event <= now) { compacted.add(event); } } mEvents = compacted; } private long getMinimumTimestamp() { return mClock.millis() - TimeUnit.HOURS.toMillis(WINDOW_IN_HOURS) + 1; } private void loadFromFile() throws IOException { if (!mFile.exists()) { return; } try (FileInputStream fis = new FileInputStream(mFile); DataInputStream dis = new DataInputStream(fis)) { while (true) { mEvents.add(dis.readLong()); } } catch (EOFException eof) { // expected } } private void saveToFile() throws IOException { // File size is maximum number of key rotations in window multiplied by 8 bytes, which is // why // we just overwrite it each time. We expect it will always be less than 100 bytes in size. try (FileOutputStream fos = new FileOutputStream(mFile); DataOutputStream dos = new DataOutputStream(fos)) { for (long event : mEvents) { dos.writeLong(event); } } } } packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java 0 → 100644 +200 −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 static org.robolectric.RuntimeEnvironment.application; import android.content.Context; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import java.io.File; import java.time.Clock; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; /** Tests for the tertiary key rotation scheduler */ @RunWith(RobolectricTestRunner.class) public final class TertiaryKeyRotationSchedulerTest { private static final int MAXIMUM_ROTATIONS_PER_WINDOW = 2; private static final int MAX_BACKUPS_TILL_ROTATION = 31; private static final String SHARED_PREFS_NAME = "tertiary_key_rotation_tracker"; private static final String PACKAGE_1 = "com.android.example1"; private static final String PACKAGE_2 = "com.android.example2"; @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private Clock mClock; private File mFile; private TertiaryKeyRotationScheduler mScheduler; /** Setup the scheduler for test */ @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mFile = temporaryFolder.newFile(); mScheduler = new TertiaryKeyRotationScheduler( new TertiaryKeyRotationTracker( application.getSharedPreferences( SHARED_PREFS_NAME, Context.MODE_PRIVATE), MAX_BACKUPS_TILL_ROTATION), new TertiaryKeyRotationWindowedCount(mFile, mClock), MAXIMUM_ROTATIONS_PER_WINDOW); } /** Test we don't trigger a rotation straight off */ @Test public void isKeyRotationDue_isFalseInitially() { assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test we don't prematurely trigger a rotation */ @Test public void isKeyRotationDue_isFalseAfterInsufficientBackups() { simulateBackups(MAX_BACKUPS_TILL_ROTATION - 1); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test we do trigger a backup */ @Test public void isKeyRotationDue_isTrueAfterEnoughBackups() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue(); } /** Test rotation will occur if the quota allows */ @Test public void isKeyRotationDue_isTrueIfRotationQuotaRemainsInWindow() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_2); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue(); } /** Test rotation is blocked if the quota has been exhausted */ @Test public void isKeyRotationDue_isFalseIfEnoughRotationsHaveHappenedInWindow() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_2); mScheduler.recordKeyRotation(PACKAGE_2); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test rotation is due after one window has passed */ @Test public void isKeyRotationDue_isTrueAfterAWholeWindowHasPassed() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_2); mScheduler.recordKeyRotation(PACKAGE_2); setTimeMillis(TimeUnit.HOURS.toMillis(24)); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue(); } /** Test the rotation state changes after a rotation */ @Test public void isKeyRotationDue_isFalseAfterRotation() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_1); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test the rate limiting for a given window */ @Test public void isKeyRotationDue_neverAllowsMoreThanInWindow() { List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION); // simulate backups of all apps each night for (int i = 0; i < 300; i++) { setTimeMillis(i * TimeUnit.HOURS.toMillis(24)); int rotationsThisNight = 0; for (String app : apps) { if (mScheduler.isKeyRotationDue(app)) { rotationsThisNight++; mScheduler.recordKeyRotation(app); } else { mScheduler.recordBackup(app); } } assertThat(rotationsThisNight).isAtMost(MAXIMUM_ROTATIONS_PER_WINDOW); } } /** Test that backups are staggered over the window */ @Test public void isKeyRotationDue_naturallyStaggersBackupsOverTime() { List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION); HashMap<String, ArrayList<Integer>> rotationDays = new HashMap<>(); for (String app : apps) { rotationDays.put(app, new ArrayList<>()); } // simulate backups of all apps each night for (int i = 0; i < 300; i++) { setTimeMillis(i * TimeUnit.HOURS.toMillis(24)); for (String app : apps) { if (mScheduler.isKeyRotationDue(app)) { rotationDays.get(app).add(i); mScheduler.recordKeyRotation(app); } else { mScheduler.recordBackup(app); } } } for (String app : apps) { List<Integer> days = rotationDays.get(app); for (int i = 1; i < days.size(); i++) { assertThat(days.get(i) - days.get(i - 1)).isEqualTo(MAX_BACKUPS_TILL_ROTATION + 1); } } } private ArrayList<String> makeTestApps(int n) { ArrayList<String> apps = new ArrayList<>(); for (int i = 0; i < n; i++) { apps.add(String.format(Locale.US, "com.android.app%d", i)); } return apps; } private void simulateBackups(int numberOfBackups) { while (numberOfBackups > 0) { mScheduler.recordBackup(PACKAGE_1); numberOfBackups--; } } private void setTimeMillis(long timeMillis) { when(mClock.millis()).thenReturn(timeMillis); } } packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java 0 → 100644 +131 −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 org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import java.io.File; import java.io.IOException; import java.time.Clock; import java.util.concurrent.TimeUnit; /** Tests for {@link TertiaryKeyRotationWindowedCount}. */ @RunWith(RobolectricTestRunner.class) public class TertiaryKeyRotationWindowedCountTest { private static final int TIMESTAMP_SIZE_IN_BYTES = 8; @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder(); @Mock private Clock mClock; private File mFile; private TertiaryKeyRotationWindowedCount mWindowedcount; /** Setup the windowed counter for testing */ @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); mFile = mTemporaryFolder.newFile(); mWindowedcount = new TertiaryKeyRotationWindowedCount(mFile, mClock); } /** Test handling bad files */ @Test public void constructor_doesNotFailForBadFile() throws IOException { new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock); } /** Test the count is 0 to start */ @Test public void getCount_isZeroInitially() { assertThat(mWindowedcount.getCount()).isEqualTo(0); } /** Test the count is correct for a time window */ @Test public void getCount_includesResultsInLastTwentyFourHours() { setTimeMillis(0); mWindowedcount.record(); setTimeMillis(TimeUnit.HOURS.toMillis(4)); mWindowedcount.record(); setTimeMillis(TimeUnit.HOURS.toMillis(23)); mWindowedcount.record(); mWindowedcount.record(); assertThat(mWindowedcount.getCount()).isEqualTo(4); } /** Test old results are ignored */ @Test public void getCount_ignoresResultsOlderThanTwentyFourHours() { setTimeMillis(0); mWindowedcount.record(); setTimeMillis(TimeUnit.HOURS.toMillis(24)); assertThat(mWindowedcount.getCount()).isEqualTo(0); } /** Test future events are removed if the clock moves backways (e.g. DST, TZ change) */ @Test public void getCount_removesFutureEventsIfClockHasChanged() { setTimeMillis(1000); mWindowedcount.record(); setTimeMillis(0); assertThat(mWindowedcount.getCount()).isEqualTo(0); } /** Check recording doesn't fail for a bad file */ @Test public void record_doesNotFailForBadFile() throws Exception { new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock).record(); } /** Checks the state is persisted */ @Test public void record_persistsStateToDisk() { setTimeMillis(0); mWindowedcount.record(); assertThat(new TertiaryKeyRotationWindowedCount(mFile, mClock).getCount()).isEqualTo(1); } /** Test the file doesn't contain unnecessary data */ @Test public void record_compactsFileToLast24Hours() { setTimeMillis(0); mWindowedcount.record(); assertThat(mFile.length()).isEqualTo(TIMESTAMP_SIZE_IN_BYTES); setTimeMillis(1); mWindowedcount.record(); assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES); setTimeMillis(TimeUnit.HOURS.toMillis(24)); mWindowedcount.record(); assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES); } private void setTimeMillis(long timeMillis) { when(mClock.millis()).thenReturn(timeMillis); } } Loading
packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationScheduler.java 0 → 100644 +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.keys; import android.content.Context; import com.android.internal.annotations.VisibleForTesting; /** * Schedules tertiary key rotations in a staggered fashion. * * <p>Apps are due a key rotation after a certain number of backups. Rotations are then staggerered * over a period of time, through restricting the number of rotations allowed in a 24-hour window. * This will causes the apps to enter a staggered cycle of regular rotations. * * <p>Note: the methods in this class are not optimized to be super fast. They make blocking IO to * ensure that scheduler information is committed to disk, so that it is available after the user * turns their device off and on. This ought to be fine as * * <ul> * <li>It will be invoked before a backup, so should never be invoked on the UI thread * <li>It will be invoked before a backup, so the vast amount of time is spent on the backup, not * writing tiny amounts of data to disk. * </ul> */ public class TertiaryKeyRotationScheduler { /** Default number of key rotations allowed within 24 hours. */ private static final int KEY_ROTATION_LIMIT = 2; /** A new instance, using {@code context} to determine where to store state. */ public static TertiaryKeyRotationScheduler getInstance(Context context) { TertiaryKeyRotationWindowedCount windowedCount = TertiaryKeyRotationWindowedCount.getInstance(context); TertiaryKeyRotationTracker tracker = TertiaryKeyRotationTracker.getInstance(context); return new TertiaryKeyRotationScheduler(tracker, windowedCount, KEY_ROTATION_LIMIT); } private final TertiaryKeyRotationTracker mTracker; private final TertiaryKeyRotationWindowedCount mWindowedCount; private final int mMaximumRotationsPerWindow; /** * A new instance. * * @param tracker Tracks how many times each application has backed up. * @param windowedCount Tracks how many rotations have happened in the last 24 hours. * @param maximumRotationsPerWindow The maximum number of key rotations allowed per 24 hours. */ @VisibleForTesting TertiaryKeyRotationScheduler( TertiaryKeyRotationTracker tracker, TertiaryKeyRotationWindowedCount windowedCount, int maximumRotationsPerWindow) { mTracker = tracker; mWindowedCount = windowedCount; mMaximumRotationsPerWindow = maximumRotationsPerWindow; } /** * Returns {@code true} if the app with {@code packageName} is due having its key rotated. * * <p>This ought to be queried before backing up an app, to determine whether to do an * incremental backup or a full backup. (A full backup forces key rotation.) */ public boolean isKeyRotationDue(String packageName) { if (mWindowedCount.getCount() >= mMaximumRotationsPerWindow) { return false; } return mTracker.isKeyRotationDue(packageName); } /** * Records that a backup happened for the app with the given {@code packageName}. * * <p>Each backup brings the app closer to the point at which a key rotation is due. */ public void recordBackup(String packageName) { mTracker.recordBackup(packageName); } /** * Records a key rotation happened for the app with the given {@code packageName}. * * <p>This resets the countdown until the next key rotation is due. */ public void recordKeyRotation(String packageName) { mTracker.resetCountdown(packageName); mWindowedCount.record(); } }
packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationTracker.java +21 −7 Original line number Diff line number Diff line /* * Copyright (C) 2018 The Android Open Source Project * 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. Loading @@ -16,6 +16,8 @@ package com.android.server.backup.encryption.keys; import static com.android.internal.util.Preconditions.checkArgument; import android.content.Context; import android.content.SharedPreferences; import android.util.Slog; Loading Loading @@ -46,15 +48,27 @@ public class TertiaryKeyRotationTracker { */ public static TertiaryKeyRotationTracker getInstance(Context context) { return new TertiaryKeyRotationTracker( context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)); context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE), MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION); } private final SharedPreferences mSharedPreferences; private final int mMaxBackupsTillRotation; /** New instance, storing data in {@code mSharedPreferences}. */ /** * New instance, storing data in {@code sharedPreferences} and initializing backup countdown to * {@code maxBackupsTillRotation}. */ @VisibleForTesting TertiaryKeyRotationTracker(SharedPreferences sharedPreferences) { TertiaryKeyRotationTracker(SharedPreferences sharedPreferences, int maxBackupsTillRotation) { checkArgument( maxBackupsTillRotation >= 0, String.format( Locale.US, "maxBackupsTillRotation should be non-negative but was %d", maxBackupsTillRotation)); mSharedPreferences = sharedPreferences; mMaxBackupsTillRotation = maxBackupsTillRotation; } /** Loading @@ -63,7 +77,7 @@ public class TertiaryKeyRotationTracker { * @param packageName The package name of the app. */ public boolean isKeyRotationDue(String packageName) { return getBackupsSinceRotation(packageName) >= MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION; return getBackupsSinceRotation(packageName) >= mMaxBackupsTillRotation; } /** Loading @@ -84,7 +98,7 @@ public class TertiaryKeyRotationTracker { packageName, Math.max( 0, MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION mMaxBackupsTillRotation - backupsSinceRotation))); } } Loading @@ -102,7 +116,7 @@ public class TertiaryKeyRotationTracker { public void markAllForRotation() { SharedPreferences.Editor editor = mSharedPreferences.edit(); for (String packageName : mSharedPreferences.getAll().keySet()) { editor.putInt(packageName, MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION); editor.putInt(packageName, mMaxBackupsTillRotation); } editor.apply(); } Loading
packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCount.java 0 → 100644 +132 −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.internal.annotations.VisibleForTesting; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.time.Clock; import java.util.ArrayList; import java.util.concurrent.TimeUnit; /** * Tracks (and commits to disk) how many key rotations have happened in the last 24 hours. This * allows us to limit (and therefore stagger) the number of key rotations in a given period of time. * * <p>Note to engineers thinking of replacing the below with fancier algorithms and data structures: * we expect the total size of this count at any time to be below however many rotations we allow in * the window, which is going to be in single digits. Any changes that mean we write to disk more * frequently, that the code is no longer resistant to clock changes, or that the code is more * difficult to understand are almost certainly not worthwhile. */ public class TertiaryKeyRotationWindowedCount { private static final String TAG = "TertiaryKeyRotCount"; private static final int WINDOW_IN_HOURS = 24; private static final String LOG_FILE_NAME = "tertiary_key_rotation_windowed_count"; private final Clock mClock; private final File mFile; private ArrayList<Long> mEvents; /** Returns a new instance, persisting state to the files dir of {@code context}. */ public static TertiaryKeyRotationWindowedCount getInstance(Context context) { File logFile = new File(context.getFilesDir(), LOG_FILE_NAME); return new TertiaryKeyRotationWindowedCount(logFile, Clock.systemDefaultZone()); } /** A new instance, committing state to {@code file}, and reading time from {@code clock}. */ @VisibleForTesting TertiaryKeyRotationWindowedCount(File file, Clock clock) { mFile = file; mClock = clock; mEvents = new ArrayList<>(); try { loadFromFile(); } catch (IOException e) { Slog.e(TAG, "Error reading " + LOG_FILE_NAME, e); } } /** Records a key rotation at the current time. */ public void record() { mEvents.add(mClock.millis()); compact(); try { saveToFile(); } catch (IOException e) { Slog.e(TAG, "Error saving " + LOG_FILE_NAME, e); } } /** Returns the number of key rotation that have been recorded in the window. */ public int getCount() { compact(); return mEvents.size(); } private void compact() { long minimumTimestamp = getMinimumTimestamp(); long now = mClock.millis(); ArrayList<Long> compacted = new ArrayList<>(); for (long event : mEvents) { if (event >= minimumTimestamp && event <= now) { compacted.add(event); } } mEvents = compacted; } private long getMinimumTimestamp() { return mClock.millis() - TimeUnit.HOURS.toMillis(WINDOW_IN_HOURS) + 1; } private void loadFromFile() throws IOException { if (!mFile.exists()) { return; } try (FileInputStream fis = new FileInputStream(mFile); DataInputStream dis = new DataInputStream(fis)) { while (true) { mEvents.add(dis.readLong()); } } catch (EOFException eof) { // expected } } private void saveToFile() throws IOException { // File size is maximum number of key rotations in window multiplied by 8 bytes, which is // why // we just overwrite it each time. We expect it will always be less than 100 bytes in size. try (FileOutputStream fos = new FileOutputStream(mFile); DataOutputStream dos = new DataOutputStream(fos)) { for (long event : mEvents) { dos.writeLong(event); } } } }
packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationSchedulerTest.java 0 → 100644 +200 −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 static org.robolectric.RuntimeEnvironment.application; import android.content.Context; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import java.io.File; import java.time.Clock; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.TimeUnit; /** Tests for the tertiary key rotation scheduler */ @RunWith(RobolectricTestRunner.class) public final class TertiaryKeyRotationSchedulerTest { private static final int MAXIMUM_ROTATIONS_PER_WINDOW = 2; private static final int MAX_BACKUPS_TILL_ROTATION = 31; private static final String SHARED_PREFS_NAME = "tertiary_key_rotation_tracker"; private static final String PACKAGE_1 = "com.android.example1"; private static final String PACKAGE_2 = "com.android.example2"; @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); @Mock private Clock mClock; private File mFile; private TertiaryKeyRotationScheduler mScheduler; /** Setup the scheduler for test */ @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mFile = temporaryFolder.newFile(); mScheduler = new TertiaryKeyRotationScheduler( new TertiaryKeyRotationTracker( application.getSharedPreferences( SHARED_PREFS_NAME, Context.MODE_PRIVATE), MAX_BACKUPS_TILL_ROTATION), new TertiaryKeyRotationWindowedCount(mFile, mClock), MAXIMUM_ROTATIONS_PER_WINDOW); } /** Test we don't trigger a rotation straight off */ @Test public void isKeyRotationDue_isFalseInitially() { assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test we don't prematurely trigger a rotation */ @Test public void isKeyRotationDue_isFalseAfterInsufficientBackups() { simulateBackups(MAX_BACKUPS_TILL_ROTATION - 1); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test we do trigger a backup */ @Test public void isKeyRotationDue_isTrueAfterEnoughBackups() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue(); } /** Test rotation will occur if the quota allows */ @Test public void isKeyRotationDue_isTrueIfRotationQuotaRemainsInWindow() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_2); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue(); } /** Test rotation is blocked if the quota has been exhausted */ @Test public void isKeyRotationDue_isFalseIfEnoughRotationsHaveHappenedInWindow() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_2); mScheduler.recordKeyRotation(PACKAGE_2); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test rotation is due after one window has passed */ @Test public void isKeyRotationDue_isTrueAfterAWholeWindowHasPassed() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_2); mScheduler.recordKeyRotation(PACKAGE_2); setTimeMillis(TimeUnit.HOURS.toMillis(24)); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isTrue(); } /** Test the rotation state changes after a rotation */ @Test public void isKeyRotationDue_isFalseAfterRotation() { simulateBackups(MAX_BACKUPS_TILL_ROTATION); mScheduler.recordKeyRotation(PACKAGE_1); assertThat(mScheduler.isKeyRotationDue(PACKAGE_1)).isFalse(); } /** Test the rate limiting for a given window */ @Test public void isKeyRotationDue_neverAllowsMoreThanInWindow() { List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION); // simulate backups of all apps each night for (int i = 0; i < 300; i++) { setTimeMillis(i * TimeUnit.HOURS.toMillis(24)); int rotationsThisNight = 0; for (String app : apps) { if (mScheduler.isKeyRotationDue(app)) { rotationsThisNight++; mScheduler.recordKeyRotation(app); } else { mScheduler.recordBackup(app); } } assertThat(rotationsThisNight).isAtMost(MAXIMUM_ROTATIONS_PER_WINDOW); } } /** Test that backups are staggered over the window */ @Test public void isKeyRotationDue_naturallyStaggersBackupsOverTime() { List<String> apps = makeTestApps(MAXIMUM_ROTATIONS_PER_WINDOW * MAX_BACKUPS_TILL_ROTATION); HashMap<String, ArrayList<Integer>> rotationDays = new HashMap<>(); for (String app : apps) { rotationDays.put(app, new ArrayList<>()); } // simulate backups of all apps each night for (int i = 0; i < 300; i++) { setTimeMillis(i * TimeUnit.HOURS.toMillis(24)); for (String app : apps) { if (mScheduler.isKeyRotationDue(app)) { rotationDays.get(app).add(i); mScheduler.recordKeyRotation(app); } else { mScheduler.recordBackup(app); } } } for (String app : apps) { List<Integer> days = rotationDays.get(app); for (int i = 1; i < days.size(); i++) { assertThat(days.get(i) - days.get(i - 1)).isEqualTo(MAX_BACKUPS_TILL_ROTATION + 1); } } } private ArrayList<String> makeTestApps(int n) { ArrayList<String> apps = new ArrayList<>(); for (int i = 0; i < n; i++) { apps.add(String.format(Locale.US, "com.android.app%d", i)); } return apps; } private void simulateBackups(int numberOfBackups) { while (numberOfBackups > 0) { mScheduler.recordBackup(PACKAGE_1); numberOfBackups--; } } private void setTimeMillis(long timeMillis) { when(mClock.millis()).thenReturn(timeMillis); } }
packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyRotationWindowedCountTest.java 0 → 100644 +131 −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 org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import java.io.File; import java.io.IOException; import java.time.Clock; import java.util.concurrent.TimeUnit; /** Tests for {@link TertiaryKeyRotationWindowedCount}. */ @RunWith(RobolectricTestRunner.class) public class TertiaryKeyRotationWindowedCountTest { private static final int TIMESTAMP_SIZE_IN_BYTES = 8; @Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder(); @Mock private Clock mClock; private File mFile; private TertiaryKeyRotationWindowedCount mWindowedcount; /** Setup the windowed counter for testing */ @Before public void setUp() throws IOException { MockitoAnnotations.initMocks(this); mFile = mTemporaryFolder.newFile(); mWindowedcount = new TertiaryKeyRotationWindowedCount(mFile, mClock); } /** Test handling bad files */ @Test public void constructor_doesNotFailForBadFile() throws IOException { new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock); } /** Test the count is 0 to start */ @Test public void getCount_isZeroInitially() { assertThat(mWindowedcount.getCount()).isEqualTo(0); } /** Test the count is correct for a time window */ @Test public void getCount_includesResultsInLastTwentyFourHours() { setTimeMillis(0); mWindowedcount.record(); setTimeMillis(TimeUnit.HOURS.toMillis(4)); mWindowedcount.record(); setTimeMillis(TimeUnit.HOURS.toMillis(23)); mWindowedcount.record(); mWindowedcount.record(); assertThat(mWindowedcount.getCount()).isEqualTo(4); } /** Test old results are ignored */ @Test public void getCount_ignoresResultsOlderThanTwentyFourHours() { setTimeMillis(0); mWindowedcount.record(); setTimeMillis(TimeUnit.HOURS.toMillis(24)); assertThat(mWindowedcount.getCount()).isEqualTo(0); } /** Test future events are removed if the clock moves backways (e.g. DST, TZ change) */ @Test public void getCount_removesFutureEventsIfClockHasChanged() { setTimeMillis(1000); mWindowedcount.record(); setTimeMillis(0); assertThat(mWindowedcount.getCount()).isEqualTo(0); } /** Check recording doesn't fail for a bad file */ @Test public void record_doesNotFailForBadFile() throws Exception { new TertiaryKeyRotationWindowedCount(mTemporaryFolder.newFolder(), mClock).record(); } /** Checks the state is persisted */ @Test public void record_persistsStateToDisk() { setTimeMillis(0); mWindowedcount.record(); assertThat(new TertiaryKeyRotationWindowedCount(mFile, mClock).getCount()).isEqualTo(1); } /** Test the file doesn't contain unnecessary data */ @Test public void record_compactsFileToLast24Hours() { setTimeMillis(0); mWindowedcount.record(); assertThat(mFile.length()).isEqualTo(TIMESTAMP_SIZE_IN_BYTES); setTimeMillis(1); mWindowedcount.record(); assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES); setTimeMillis(TimeUnit.HOURS.toMillis(24)); mWindowedcount.record(); assertThat(mFile.length()).isEqualTo(2 * TIMESTAMP_SIZE_IN_BYTES); } private void setTimeMillis(long timeMillis) { when(mClock.millis()).thenReturn(timeMillis); } }