diff options
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; + } } |