blob: b90343ad4b3514cfcb31b32ffbc619d0a023662b [file] [log] [blame]
/*
* 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);
}
}
}
}