/* * 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 mKeyValueListingStore; private final ProtoStore 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. * *

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 keyValueListingStore, RecoverableKeyStoreSecondaryKey secondaryKey, ProtoStore 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. * *

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}. * *

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> 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> getListingsAndEnsureConsistency(String packageName) throws IOException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { Optional keyValueListing = mKeyValueListingStore.loadProto(packageName); Optional 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); } }