blob: 619438c7f6fe4941f59f53694c0835b1f2522521 [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.tasks;
import android.annotation.Nullable;
import android.app.backup.BackupDataInput;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.util.Optional;
// TODO(b/141975695): Create a base class for EncryptedKvBackupTask and EncryptedFullBackupTask.
/** Performs encrypted key value backup, handling rotating the tertiary key as necessary. */
public class EncryptedKvBackupTask {
private static final String TAG = "EncryptedKvBackupTask";
private final TertiaryKeyManager mTertiaryKeyManager;
private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
private final ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
private final ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
private final KvBackupEncrypter mKvBackupEncrypter;
private final EncryptedBackupTask mEncryptedBackupTask;
private final String mPackageName;
/** Constructs new instances of {@link EncryptedKvBackupTask}. */
public static class EncryptedKvBackupTaskFactory {
/**
* Creates a new instance.
*
* <p>Either initializes encrypted backup or loads an existing secondary key as necessary.
*
* @param cryptoSettings to load secondary key state from
* @param fileDescriptor to read the backup data from
*/
public EncryptedKvBackupTask newInstance(
Context context,
SecureRandom secureRandom,
CryptoBackupServer cryptoBackupServer,
CryptoSettings cryptoSettings,
RecoverableKeyStoreSecondaryKeyManager
.RecoverableKeyStoreSecondaryKeyManagerProvider
recoverableSecondaryKeyManagerProvider,
ParcelFileDescriptor fileDescriptor,
String packageName)
throws IOException, UnrecoverableKeyException, LockScreenRequiredException,
InternalRecoveryServiceException, InvalidKeyException {
RecoverableKeyStoreSecondaryKey secondaryKey =
new InitializeRecoverableSecondaryKeyTask(
context,
cryptoSettings,
recoverableSecondaryKeyManagerProvider.get(),
cryptoBackupServer)
.run();
KvBackupEncrypter backupEncrypter =
new KvBackupEncrypter(new BackupDataInput(fileDescriptor.getFileDescriptor()));
TertiaryKeyManager tertiaryKeyManager =
new TertiaryKeyManager(
context,
secureRandom,
TertiaryKeyRotationScheduler.getInstance(context),
secondaryKey,
packageName);
return new EncryptedKvBackupTask(
tertiaryKeyManager,
ProtoStore.createKeyValueListingStore(context),
secondaryKey,
ProtoStore.createChunkListingStore(context),
backupEncrypter,
new EncryptedBackupTask(
cryptoBackupServer, secureRandom, packageName, backupEncrypter),
packageName);
}
}
@VisibleForTesting
EncryptedKvBackupTask(
TertiaryKeyManager tertiaryKeyManager,
ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore,
RecoverableKeyStoreSecondaryKey secondaryKey,
ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore,
KvBackupEncrypter kvBackupEncrypter,
EncryptedBackupTask encryptedBackupTask,
String packageName) {
mTertiaryKeyManager = tertiaryKeyManager;
mSecondaryKey = secondaryKey;
mKeyValueListingStore = keyValueListingStore;
mChunkListingStore = chunkListingStore;
mKvBackupEncrypter = kvBackupEncrypter;
mEncryptedBackupTask = encryptedBackupTask;
mPackageName = packageName;
}
/**
* Reads backup data from the file descriptor provided in the construtor, encrypts it and
* uploads it to the server.
*
* <p>The {@code incremental} flag indicates if the backup data provided is incremental or a
* complete set. Incremental backup is not possible if no previous crypto state exists, or the
* tertiary key must be rotated in the next backup. If the caller requests incremental backup
* but it is not possible, then the backup will not start and this method will throw {@link
* NonIncrementalBackupRequiredException}.
*
* <p>TODO(b/70704456): Update return code to indicate that we require non-incremental backup.
*
* @param incremental {@code true} if the data provided is a diff from the previous backup,
* {@code false} if it is a complete set
* @throws NonIncrementalBackupRequiredException if the caller provides an incremental backup but the task
* requires non-incremental backup
*/
public void performBackup(boolean incremental)
throws GeneralSecurityException, IOException, NoSuchMethodException,
InstantiationException, IllegalAccessException, InvocationTargetException,
NonIncrementalBackupRequiredException {
if (mTertiaryKeyManager.wasKeyRotated()) {
Slog.d(TAG, "Tertiary key is new so clearing package state.");
deleteListings(mPackageName);
}
Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
oldListings = getListingsAndEnsureConsistency(mPackageName);
if (oldListings.isPresent() && !incremental) {
Slog.d(
TAG,
"Non-incremental backup requested but incremental state existed, clearing it");
deleteListings(mPackageName);
oldListings = Optional.empty();
}
if (!oldListings.isPresent() && incremental) {
// If we don't have any state then we require a non-incremental backup, but this backup
// is incremental.
throw new NonIncrementalBackupRequiredException();
}
if (oldListings.isPresent()) {
mKvBackupEncrypter.setOldKeyValueListing(oldListings.get().first);
}
ChunksMetadataProto.ChunkListing newChunkListing;
if (oldListings.isPresent()) {
Slog.v(TAG, "Old listings existed, performing incremental backup");
newChunkListing =
mEncryptedBackupTask.performIncrementalBackup(
mTertiaryKeyManager.getKey(),
mTertiaryKeyManager.getWrappedKey(),
oldListings.get().second);
} else {
Slog.v(TAG, "Old listings did not exist, performing non-incremental backup");
// kv backups don't use this salt because they don't involve content-defined chunking.
byte[] fingerprintMixerSalt = null;
newChunkListing =
mEncryptedBackupTask.performNonIncrementalBackup(
mTertiaryKeyManager.getKey(),
mTertiaryKeyManager.getWrappedKey(),
fingerprintMixerSalt);
}
Slog.v(TAG, "Backup and upload succeeded, saving new listings");
saveListings(mPackageName, mKvBackupEncrypter.getNewKeyValueListing(), newChunkListing);
}
private Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
getListingsAndEnsureConsistency(String packageName)
throws IOException, InvocationTargetException, NoSuchMethodException,
InstantiationException, IllegalAccessException {
Optional<KeyValueListingProto.KeyValueListing> keyValueListing =
mKeyValueListingStore.loadProto(packageName);
Optional<ChunksMetadataProto.ChunkListing> chunkListing =
mChunkListingStore.loadProto(packageName);
// Normally either both protos exist or neither exist, but we correct this just in case.
boolean bothPresent = keyValueListing.isPresent() && chunkListing.isPresent();
if (!bothPresent) {
Slog.d(
TAG,
"Both listing were not present, clearing state, key value="
+ keyValueListing.isPresent()
+ ", chunk="
+ chunkListing.isPresent());
deleteListings(packageName);
return Optional.empty();
}
return Optional.of(Pair.create(keyValueListing.get(), chunkListing.get()));
}
private void saveListings(
String packageName,
KeyValueListingProto.KeyValueListing keyValueListing,
ChunksMetadataProto.ChunkListing chunkListing) {
try {
mKeyValueListingStore.saveProto(packageName, keyValueListing);
mChunkListingStore.saveProto(packageName, chunkListing);
} catch (IOException e) {
// If a problem occurred while saving either listing then they may be inconsistent, so
// delete
// both.
Slog.w(TAG, "Unable to save listings, deleting both for consistency", e);
deleteListings(packageName);
}
}
private void deleteListings(String packageName) {
mKeyValueListingStore.deleteProto(packageName);
mChunkListingStore.deleteProto(packageName);
}
}