summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTask.java244
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java25
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java356
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java12
4 files changed, 637 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 {}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java
new file mode 100644
index 000000000000..fa4fef50ac1a
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvBackupTaskTest.java
@@ -0,0 +1,356 @@
+/*
+ * 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 static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertThrows;
+
+import android.app.Application;
+import android.util.Pair;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ProtoStore;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
+import com.android.server.backup.encryption.keys.TertiaryKeyManager;
+import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
+import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
+import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+import com.android.server.backup.testing.CryptoTestUtils;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.crypto.SecretKey;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class EncryptedKvBackupTaskTest {
+ private static final boolean INCREMENTAL = true;
+ private static final boolean NON_INCREMENTAL = false;
+
+ private static final String TEST_PACKAGE_1 = "com.example.app1";
+ private static final String TEST_KEY_1 = "key_1";
+ private static final String TEST_KEY_2 = "key_2";
+ private static final ChunkHash TEST_HASH_1 =
+ new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
+ private static final ChunkHash TEST_HASH_2 =
+ new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
+ private static final int TEST_LENGTH_1 = 200;
+ private static final int TEST_LENGTH_2 = 300;
+
+ @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
+
+ @Captor private ArgumentCaptor<ChunksMetadataProto.ChunkListing> mChunkListingCaptor;
+
+ @Mock private TertiaryKeyManager mTertiaryKeyManager;
+ @Mock private RecoverableKeyStoreSecondaryKey mSecondaryKey;
+ @Mock private ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
+ @Mock private ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
+ @Mock private KvBackupEncrypter mKvBackupEncrypter;
+ @Mock private EncryptedBackupTask mEncryptedBackupTask;
+ @Mock private SecretKey mTertiaryKey;
+
+ private WrappedKeyProto.WrappedKey mWrappedTertiaryKey;
+ private KeyValueListingProto.KeyValueListing mNewKeyValueListing;
+ private ChunksMetadataProto.ChunkListing mNewChunkListing;
+ private EncryptedKvBackupTask mTask;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ Application application = ApplicationProvider.getApplicationContext();
+ mKeyValueListingStore = ProtoStore.createKeyValueListingStore(application);
+ mChunkListingStore = ProtoStore.createChunkListingStore(application);
+
+ mWrappedTertiaryKey = new WrappedKeyProto.WrappedKey();
+
+ when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(false);
+ when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
+ when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
+
+ mNewKeyValueListing =
+ createKeyValueListing(
+ CryptoTestUtils.mapOf(
+ new Pair<>(TEST_KEY_1, TEST_HASH_1),
+ new Pair<>(TEST_KEY_2, TEST_HASH_2)));
+ mNewChunkListing =
+ createChunkListing(
+ CryptoTestUtils.mapOf(
+ new Pair<>(TEST_HASH_1, TEST_LENGTH_1),
+ new Pair<>(TEST_HASH_2, TEST_LENGTH_2)));
+ when(mKvBackupEncrypter.getNewKeyValueListing()).thenReturn(mNewKeyValueListing);
+ when(mEncryptedBackupTask.performIncrementalBackup(
+ eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
+ .thenReturn(mNewChunkListing);
+ when(mEncryptedBackupTask.performNonIncrementalBackup(
+ eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
+ .thenReturn(mNewChunkListing);
+
+ mTask =
+ new EncryptedKvBackupTask(
+ mTertiaryKeyManager,
+ mKeyValueListingStore,
+ mSecondaryKey,
+ mChunkListingStore,
+ mKvBackupEncrypter,
+ mEncryptedBackupTask,
+ TEST_PACKAGE_1);
+ }
+
+ @Test
+ public void testPerformBackup_rotationRequired_deletesListings() throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+ mChunkListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+ when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+ // Throw an IOException so it aborts before saving the new listings.
+ when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+ .thenThrow(IOException.class);
+
+ assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+ assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ }
+
+ @Test
+ public void testPerformBackup_rotationRequiredButIncremental_throws() throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+ mChunkListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+ when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+
+ assertThrows(NonIncrementalBackupRequiredException.class,
+ () -> mTask.performBackup(INCREMENTAL));
+ }
+
+ @Test
+ public void testPerformBackup_rotationRequiredAndNonIncremental_performsNonIncrementalBackup()
+ throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+ mChunkListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+ when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
+
+ mTask.performBackup(NON_INCREMENTAL);
+
+ verify(mEncryptedBackupTask)
+ .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), any());
+ }
+
+ @Test
+ public void testPerformBackup_existingStateButNonIncremental_deletesListings() throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+ mChunkListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+ // Throw an IOException so it aborts before saving the new listings.
+ when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+ .thenThrow(IOException.class);
+
+ assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+ assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ }
+
+ @Test
+ public void testPerformBackup_keyValueListingMissing_deletesChunkListingAndPerformsNonIncremental()
+ throws Exception {
+ mChunkListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+ // Throw an IOException so it aborts before saving the new listings.
+ when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+ .thenThrow(IOException.class);
+
+ assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+ verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
+ assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ }
+
+ @Test
+ public void testPerformBackup_chunkListingMissing_deletesKeyValueListingAndPerformsNonIncremental()
+ throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+
+ // Throw an IOException so it aborts before saving the new listings.
+ when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
+ .thenThrow(IOException.class);
+
+ assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
+
+ verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
+ assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
+ }
+
+ @Test
+ public void testPerformBackup_existingStateAndIncremental_performsIncrementalBackup()
+ throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+ ChunksMetadataProto.ChunkListing oldChunkListing =
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1)));
+ mChunkListingStore.saveProto(TEST_PACKAGE_1, oldChunkListing);
+
+ mTask.performBackup(INCREMENTAL);
+
+ verify(mEncryptedBackupTask)
+ .performIncrementalBackup(
+ eq(mTertiaryKey), eq(mWrappedTertiaryKey), mChunkListingCaptor.capture());
+ assertChunkListingsEqual(mChunkListingCaptor.getValue(), oldChunkListing);
+ }
+
+ @Test
+ public void testPerformBackup_noExistingStateAndNonIncremental_performsNonIncrementalBackup()
+ throws Exception {
+ mTask.performBackup(NON_INCREMENTAL);
+
+ verify(mEncryptedBackupTask)
+ .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(null));
+ }
+
+ @Test
+ public void testPerformBackup_incremental_savesNewListings() throws Exception {
+ mKeyValueListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
+ mChunkListingStore.saveProto(
+ TEST_PACKAGE_1,
+ createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
+
+ mTask.performBackup(INCREMENTAL);
+
+ KeyValueListingProto.KeyValueListing actualKeyValueListing =
+ mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
+ ChunksMetadataProto.ChunkListing actualChunkListing =
+ mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
+ assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
+ assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
+ }
+
+ @Test
+ public void testPerformBackup_nonIncremental_savesNewListings() throws Exception {
+ mTask.performBackup(NON_INCREMENTAL);
+
+ KeyValueListingProto.KeyValueListing actualKeyValueListing =
+ mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
+ ChunksMetadataProto.ChunkListing actualChunkListing =
+ mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
+ assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
+ assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
+ }
+
+ private static KeyValueListingProto.KeyValueListing createKeyValueListing(
+ Map<String, ChunkHash> pairs) {
+ return new KeyValueListingBuilder().addAll(pairs).build();
+ }
+
+ private static ChunksMetadataProto.ChunkListing createChunkListing(
+ Map<ChunkHash, Integer> chunks) {
+ ChunksMetadataProto.Chunk[] listingChunks = new ChunksMetadataProto.Chunk[chunks.size()];
+ int chunksAdded = 0;
+ for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
+ listingChunks[chunksAdded] = CryptoTestUtils.newChunk(entry.getKey(), entry.getValue());
+ chunksAdded++;
+ }
+ return CryptoTestUtils.newChunkListingWithoutDocId(
+ /* fingerprintSalt */ new byte[0],
+ ChunksMetadataProto.AES_256_GCM,
+ ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
+ listingChunks);
+ }
+
+ private static void assertKeyValueListingsEqual(
+ KeyValueListingProto.KeyValueListing actual,
+ KeyValueListingProto.KeyValueListing expected) {
+ KeyValueListingProto.KeyValueEntry[] actualEntries = actual.entries;
+ KeyValueListingProto.KeyValueEntry[] expectedEntries = expected.entries;
+ assertThat(actualEntries.length).isEqualTo(expectedEntries.length);
+ for (int i = 0; i < actualEntries.length; i++) {
+ assertWithMessage("entry " + i)
+ .that(actualEntries[i].key)
+ .isEqualTo(expectedEntries[i].key);
+ assertWithMessage("entry " + i)
+ .that(actualEntries[i].hash)
+ .isEqualTo(expectedEntries[i].hash);
+ }
+ }
+
+ private static void assertChunkListingsEqual(
+ ChunksMetadataProto.ChunkListing actual, ChunksMetadataProto.ChunkListing expected) {
+ ChunksMetadataProto.Chunk[] actualChunks = actual.chunks;
+ ChunksMetadataProto.Chunk[] expectedChunks = expected.chunks;
+ assertThat(actualChunks.length).isEqualTo(expectedChunks.length);
+ for (int i = 0; i < actualChunks.length; i++) {
+ assertWithMessage("chunk " + i)
+ .that(actualChunks[i].hash)
+ .isEqualTo(expectedChunks[i].hash);
+ assertWithMessage("chunk " + i)
+ .that(actualChunks[i].length)
+ .isEqualTo(expectedChunks[i].length);
+ }
+ assertThat(actual.cipherType).isEqualTo(expected.cipherType);
+ assertThat(actual.documentId)
+ .isEqualTo(expected.documentId == null ? "" : expected.documentId);
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
index 5dfd5ee8ad53..b0c02ba637e0 100644
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
@@ -16,6 +16,8 @@
package com.android.server.backup.testing;
+import android.util.Pair;
+
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
@@ -23,6 +25,8 @@ import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
import java.nio.charset.Charset;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
import java.util.Random;
import javax.crypto.KeyGenerator;
@@ -162,4 +166,12 @@ public class CryptoTestUtils {
clone.checksum = Arrays.copyOf(original.checksum, original.checksum.length);
return clone;
}
+
+ public static <K, V> Map<K, V> mapOf(Pair<K, V>... pairs) {
+ Map<K, V> map = new HashMap<>();
+ for (Pair<K, V> pair : pairs) {
+ map.put(pair.first, pair.second);
+ }
+ return map;
+ }
}