Import EncryptedKvBackupTask
Bug: 111386661 Test: atest EncryptedKvBackupTaskTest Change-Id: Id9cd0a57c3ed33f18d68e7fad60035e8a956a1de
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user