diff options
Diffstat (limited to 'packages/BackupEncryption/src')
2 files changed, 269 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java new file mode 100644 index 000000000000..619438c7f6fe --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java @@ -0,0 +1,244 @@ +/* + * 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); + } +} diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java new file mode 100644 index 000000000000..a3eda7d1270f --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java @@ -0,0 +1,25 @@ +/* + * 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; + +// TODO(141840878): Update documentation. +/** + * Exception thrown when the framework provides an incremental backup but the transport requires a + * non-incremental backup. + */ +public class NonIncrementalBackupRequiredException extends Exception {} |