Move TertiaryKeyRotationScheduler and TertiaryKeyRotationWindowedCount
Migrate these classes into the framework to support encryption in the framework for backups. There's a modification to the RotationTracker class to re-introduce a test supporting constructor. The modification to the blueprint is needed to provide Truthy which is used in the tests of the migrated classes. Bug: 111386661 Test: make RunBackupEncryptionRoboTests Change-Id: Ibe96a9a52f638d5e87e1be46af5803672288f482
This commit is contained in:
@@ -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.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();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* 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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +98,7 @@ public class TertiaryKeyRotationTracker {
|
||||
packageName,
|
||||
Math.max(
|
||||
0,
|
||||
MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION
|
||||
mMaxBackupsTillRotation
|
||||
- backupsSinceRotation)));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user