blob: c02b103f1d3389f24986d5e21aaeb31476c2e6bb [file] [log] [blame]
/*
* Copyright (C) 2017 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.locksettings.recoverablekeystore.storage;
import android.annotation.Nullable;
import android.os.Environment;
import android.security.keystore.recovery.KeyChainSnapshot;
import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.locksettings.recoverablekeystore.serialization
.KeyChainSnapshotDeserializer;
import com.android.server.locksettings.recoverablekeystore.serialization
.KeyChainSnapshotParserException;
import com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSerializer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.util.Locale;
/**
* Storage for recovery snapshots. Stores snapshots in memory, backed by disk storage.
*
* <p>Recovery snapshots are generated after a successful screen unlock. They are only generated if
* the recoverable keystore has been mutated since the previous snapshot. This class stores only the
* latest snapshot for each recovery agent.
*
* <p>This class is thread-safe. It is used both on the service thread and the
* {@link com.android.server.locksettings.recoverablekeystore.KeySyncTask} thread.
*/
public class RecoverySnapshotStorage {
private static final String TAG = "RecoverySnapshotStorage";
private static final String ROOT_PATH = "system";
private static final String STORAGE_PATH = "recoverablekeystore/snapshots/";
@GuardedBy("this")
private final SparseArray<KeyChainSnapshot> mSnapshotByUid = new SparseArray<>();
private final File rootDirectory;
/**
* A new instance, storing snapshots in /data/system/recoverablekeystore/snapshots.
*
* <p>NOTE: calling this multiple times DOES NOT return the same instance, so will NOT be backed
* by the same in-memory store.
*/
public static RecoverySnapshotStorage newInstance() {
return new RecoverySnapshotStorage(
new File(Environment.getDataDirectory(), ROOT_PATH));
}
@VisibleForTesting
public RecoverySnapshotStorage(File rootDirectory) {
this.rootDirectory = rootDirectory;
}
/**
* Sets the latest {@code snapshot} for the recovery agent {@code uid}.
*/
public synchronized void put(int uid, KeyChainSnapshot snapshot) {
mSnapshotByUid.put(uid, snapshot);
try {
writeToDisk(uid, snapshot);
} catch (IOException | CertificateEncodingException e) {
Log.e(TAG,
String.format(Locale.US, "Error persisting snapshot for %d to disk", uid),
e);
}
}
/**
* Returns the latest snapshot for the recovery agent {@code uid}, or null if none exists.
*/
@Nullable
public synchronized KeyChainSnapshot get(int uid) {
KeyChainSnapshot snapshot = mSnapshotByUid.get(uid);
if (snapshot != null) {
return snapshot;
}
try {
return readFromDisk(uid);
} catch (IOException | KeyChainSnapshotParserException e) {
Log.e(TAG, String.format(Locale.US, "Error reading snapshot for %d from disk", uid), e);
return null;
}
}
/**
* Removes any (if any) snapshot associated with recovery agent {@code uid}.
*/
public synchronized void remove(int uid) {
mSnapshotByUid.remove(uid);
getSnapshotFile(uid).delete();
}
/**
* Writes the snapshot for recovery agent {@code uid} to disk.
*
* @throws IOException if an IO error occurs writing to disk.
*/
private void writeToDisk(int uid, KeyChainSnapshot snapshot)
throws IOException, CertificateEncodingException {
File snapshotFile = getSnapshotFile(uid);
try (
FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)
) {
KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
} catch (IOException | CertificateEncodingException e) {
// If we fail to write the latest snapshot, we should delete any older snapshot that
// happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
snapshotFile.delete();
throw e;
}
}
/**
* Reads the last snapshot for recovery agent {@code uid} from disk.
*
* @return The snapshot, or null if none existed.
* @throws IOException if an IO error occurs reading from disk.
*/
@Nullable
private KeyChainSnapshot readFromDisk(int uid)
throws IOException, KeyChainSnapshotParserException {
File snapshotFile = getSnapshotFile(uid);
try (
FileInputStream fileInputStream = new FileInputStream(snapshotFile)
) {
return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
} catch (IOException | KeyChainSnapshotParserException e) {
// If we fail to read the latest snapshot, we should delete it in case it is in some way
// corrupted. We can regenerate snapshots anyway.
snapshotFile.delete();
throw e;
}
}
private File getSnapshotFile(int uid) {
File folder = getStorageFolder();
String fileName = getSnapshotFileName(uid);
return new File(folder, fileName);
}
private String getSnapshotFileName(int uid) {
return String.format(Locale.US, "%d.xml", uid);
}
private File getStorageFolder() {
File folder = new File(rootDirectory, STORAGE_PATH);
folder.mkdirs();
return folder;
}
}