summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Ruslan Tkhakokhov <rthakohov@google.com> 2019-10-03 16:54:29 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2019-10-03 16:54:29 +0000
commita3b3988934b74c0f77bb937e94a06edc59108f56 (patch)
tree790ba0c750eb5c9ee082ad3e42bac44b88a71296
parent6b44087b1a8387eddd53493573709d1d81ac32cb (diff)
parenta8382e9b95a89c76ff35a72385a41bb36898493f (diff)
Merge changes Ifd189613,Id603dabc,Id9cd0a57
* changes: Add integration test for encrypted KV B&R Import EncryptedKvRestoreTask Import EncryptedKvBackupTask
-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/EncryptedKvRestoreTask.java139
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/NonIncrementalBackupRequiredException.java25
-rw-r--r--packages/BackupEncryption/test/robolectric-integration/Android.bp1
-rw-r--r--packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java134
-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/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java185
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java12
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java66
9 files changed, 1147 insertions, 15 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/EncryptedKvRestoreTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java
new file mode 100644
index 000000000000..12b44590ebe6
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTask.java
@@ -0,0 +1,139 @@
+/*
+ * 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.android.internal.util.Preconditions.checkArgument;
+
+import android.app.backup.BackupDataOutput;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.FullRestoreDownloader;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
+import com.android.server.backup.encryption.keys.RestoreKeyFetcher;
+import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
+import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
+import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
+
+import java.io.File;
+import java.io.IOException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.KeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+
+/**
+ * Performs a key value restore by downloading the backup set, decrypting it and writing it to the
+ * file provided by backup manager.
+ */
+public class EncryptedKvRestoreTask {
+ private static final String ENCRYPTED_FILE_NAME = "encrypted_kv";
+
+ private final File mTemporaryFolder;
+ private final ChunkHasher mChunkHasher;
+ private final FullRestoreToFileTask mFullRestoreToFileTask;
+ private final BackupFileDecryptorTask mBackupFileDecryptorTask;
+
+ /** Constructs new instances of the task. */
+ public static class EncryptedKvRestoreTaskFactory {
+ /**
+ * Constructs a new instance.
+ *
+ * <p>Fetches the appropriate secondary key and uses this to unwrap the tertiary key. Stores
+ * temporary files in {@link Context#getFilesDir()}.
+ */
+ public EncryptedKvRestoreTask newInstance(
+ Context context,
+ RecoverableKeyStoreSecondaryKeyManager
+ .RecoverableKeyStoreSecondaryKeyManagerProvider
+ recoverableSecondaryKeyManagerProvider,
+ FullRestoreDownloader fullRestoreDownloader,
+ String secondaryKeyAlias,
+ WrappedKeyProto.WrappedKey wrappedTertiaryKey)
+ throws EncryptedRestoreException, NoSuchAlgorithmException, NoSuchPaddingException,
+ KeyException, InvalidAlgorithmParameterException {
+ SecretKey tertiaryKey =
+ RestoreKeyFetcher.unwrapTertiaryKey(
+ recoverableSecondaryKeyManagerProvider,
+ secondaryKeyAlias,
+ wrappedTertiaryKey);
+
+ return new EncryptedKvRestoreTask(
+ context.getFilesDir(),
+ new ChunkHasher(tertiaryKey),
+ new FullRestoreToFileTask(fullRestoreDownloader),
+ new BackupFileDecryptorTask(tertiaryKey));
+ }
+ }
+
+ @VisibleForTesting
+ EncryptedKvRestoreTask(
+ File temporaryFolder,
+ ChunkHasher chunkHasher,
+ FullRestoreToFileTask fullRestoreToFileTask,
+ BackupFileDecryptorTask backupFileDecryptorTask) {
+ checkArgument(
+ temporaryFolder.isDirectory(), "Temporary folder must be an existing directory");
+
+ mTemporaryFolder = temporaryFolder;
+ mChunkHasher = chunkHasher;
+ mFullRestoreToFileTask = fullRestoreToFileTask;
+ mBackupFileDecryptorTask = backupFileDecryptorTask;
+ }
+
+ /**
+ * Runs the restore, writing the pairs in lexicographical order to the given file descriptor.
+ *
+ * <p>This will block for the duration of the restore.
+ *
+ * @throws EncryptedRestoreException if there is a problem decrypting or verifying the backup
+ */
+ public void getRestoreData(ParcelFileDescriptor output)
+ throws IOException, EncryptedRestoreException, BadPaddingException,
+ InvalidAlgorithmParameterException, NoSuchAlgorithmException,
+ IllegalBlockSizeException, ShortBufferException, InvalidKeyException {
+ File encryptedFile = new File(mTemporaryFolder, ENCRYPTED_FILE_NAME);
+ try {
+ downloadDecryptAndWriteBackup(encryptedFile, output);
+ } finally {
+ encryptedFile.delete();
+ }
+ }
+
+ private void downloadDecryptAndWriteBackup(File encryptedFile, ParcelFileDescriptor output)
+ throws EncryptedRestoreException, IOException, BadPaddingException, InvalidKeyException,
+ NoSuchAlgorithmException, IllegalBlockSizeException, ShortBufferException,
+ InvalidAlgorithmParameterException {
+ mFullRestoreToFileTask.restoreToFile(encryptedFile);
+ DecryptedChunkKvOutput decryptedChunkKvOutput = new DecryptedChunkKvOutput(mChunkHasher);
+ mBackupFileDecryptorTask.decryptFile(encryptedFile, decryptedChunkKvOutput);
+
+ BackupDataOutput backupDataOutput = new BackupDataOutput(output.getFileDescriptor());
+ for (KeyValuePairProto.KeyValuePair pair : decryptedChunkKvOutput.getPairs()) {
+ backupDataOutput.writeEntityHeader(pair.key, pair.value.length);
+ backupDataOutput.writeEntityData(pair.value, pair.value.length);
+ }
+ }
+}
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-integration/Android.bp b/packages/BackupEncryption/test/robolectric-integration/Android.bp
index f696278ab967..67365df4b28f 100644
--- a/packages/BackupEncryption/test/robolectric-integration/Android.bp
+++ b/packages/BackupEncryption/test/robolectric-integration/Android.bp
@@ -23,6 +23,7 @@ android_robolectric_test {
"platform-test-annotations",
"testng",
"truth-prebuilt",
+ "BackupEncryptionRoboTests",
],
static_libs: [
"androidx.test.core",
diff --git a/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java b/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java
index 8ec68fdf822d..a432d91828cf 100644
--- a/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java
+++ b/packages/BackupEncryption/test/robolectric-integration/src/com/android/server/backup/encryption/RoundTripTest.java
@@ -19,21 +19,35 @@ package com.android.server.backup.encryption;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.security.keystore.recovery.InternalRecoveryServiceException;
+import android.security.keystore.recovery.RecoveryController;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.backup.encryption.client.CryptoBackupServer;
+import com.android.server.backup.encryption.keys.KeyWrapUtils;
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.WrappedKeyProto;
import com.android.server.backup.encryption.tasks.EncryptedFullBackupTask;
import com.android.server.backup.encryption.tasks.EncryptedFullRestoreTask;
+import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
+import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
+import com.android.server.testing.shadows.DataEntity;
+import com.android.server.testing.shadows.ShadowBackupDataInput;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+import com.android.server.testing.shadows.ShadowRecoveryController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -42,15 +56,29 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
+import java.util.Optional;
import java.util.Map;
+import java.util.Set;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
+@Config(
+ shadows = {
+ ShadowBackupDataInput.class,
+ ShadowBackupDataOutput.class,
+ ShadowRecoveryController.class
+ })
@RunWith(RobolectricTestRunner.class)
public class RoundTripTest {
+ private static final DataEntity[] KEY_VALUE_DATA = {
+ new DataEntity("test_key_1", "test_value_1"),
+ new DataEntity("test_key_2", "test_value_2"),
+ new DataEntity("test_key_3", "test_value_3")
+ };
+
/** Amount of data we want to round trip in this test */
private static final int TEST_DATA_SIZE = 1024 * 1024; // 1MB
@@ -59,6 +87,7 @@ public class RoundTripTest {
/** Key parameters used for the secondary encryption key */
private static final String KEY_ALGORITHM = "AES";
+
private static final int KEY_SIZE_BITS = 256;
/** Package name for our test package */
@@ -77,25 +106,82 @@ public class RoundTripTest {
private RecoverableKeyStoreSecondaryKey mSecondaryKey;
/** Source of random material which is considered non-predictable in its' generation */
- private SecureRandom mSecureRandom = new SecureRandom();
+ private final SecureRandom mSecureRandom = new SecureRandom();
+
+ private RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider
+ mSecondaryKeyManagerProvider;
+ private DummyServer mDummyServer;
+ private RecoveryController mRecoveryController;
+
+ @Mock private ParcelFileDescriptor mParcelFileDescriptor;
@Before
- public void setUp() throws NoSuchAlgorithmException {
+ public void setUp() throws NoSuchAlgorithmException, InternalRecoveryServiceException {
+ MockitoAnnotations.initMocks(this);
+
+ ShadowBackupDataInput.reset();
+ ShadowBackupDataOutput.reset();
+
mContext = ApplicationProvider.getApplicationContext();
mSecondaryKey = new RecoverableKeyStoreSecondaryKey(TEST_KEY_ALIAS, generateAesKey());
+ mDummyServer = new DummyServer();
+ mSecondaryKeyManagerProvider =
+ () ->
+ new RecoverableKeyStoreSecondaryKeyManager(
+ RecoveryController.getInstance(mContext), mSecureRandom);
+
fillBuffer(mOriginalData);
}
@Test
- public void testRoundTrip() throws Exception {
- byte[] backupData = performBackup(mOriginalData);
+ public void testFull_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
+ byte[] backupData = performFullBackup(mOriginalData);
assertThat(backupData).isNotEqualTo(mOriginalData);
- byte[] restoredData = performRestore(backupData);
+ byte[] restoredData = performFullRestore(backupData);
assertThat(restoredData).isEqualTo(mOriginalData);
}
- /** Perform a backup and return the backed-up representation of the data */
- private byte[] performBackup(byte[] backupData) throws Exception {
+ @Test
+ public void testKeyValue_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
+ byte[] backupData = performNonIncrementalKeyValueBackup(KEY_VALUE_DATA);
+
+ // Get the secondary key used to do backup.
+ Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
+ mSecondaryKeyManagerProvider.get().get(mDummyServer.mSecondaryKeyAlias);
+ assertThat(secondaryKey.isPresent()).isTrue();
+
+ Set<DataEntity> restoredData = performKeyValueRestore(backupData, secondaryKey.get());
+
+ assertThat(restoredData).containsExactly(KEY_VALUE_DATA).inOrder();
+ }
+
+ /** Perform a key/value backup and return the backed-up representation of the data */
+ private byte[] performNonIncrementalKeyValueBackup(DataEntity[] backupData)
+ throws Exception {
+ // Populate test key/value data.
+ for (DataEntity entity : backupData) {
+ ShadowBackupDataInput.addEntity(entity);
+ }
+
+ EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
+ new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
+ EncryptedKvBackupTask backupTask =
+ backupTaskFactory.newInstance(
+ mContext,
+ mSecureRandom,
+ mDummyServer,
+ CryptoSettings.getInstance(mContext),
+ mSecondaryKeyManagerProvider,
+ mParcelFileDescriptor,
+ TEST_PACKAGE_NAME);
+
+ backupTask.performBackup(/* incremental */ false);
+
+ return mDummyServer.mStoredData;
+ }
+
+ /** Perform a full backup and return the backed-up representation of the data */
+ private byte[] performFullBackup(byte[] backupData) throws Exception {
DummyServer dummyServer = new DummyServer();
EncryptedFullBackupTask backupTask =
EncryptedFullBackupTask.newInstance(
@@ -109,8 +195,24 @@ public class RoundTripTest {
return dummyServer.mStoredData;
}
- /** Perform a restore and resturn the bytes obtained from the restore process */
- private byte[] performRestore(byte[] backupData)
+ private Set<DataEntity> performKeyValueRestore(
+ byte[] backupData, RecoverableKeyStoreSecondaryKey secondaryKey) throws Exception {
+ EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
+ new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
+ EncryptedKvRestoreTask restoreTask =
+ restoreTaskFactory.newInstance(
+ mContext,
+ mSecondaryKeyManagerProvider,
+ new FakeFullRestoreDownloader(backupData),
+ secondaryKey.getAlias(),
+ KeyWrapUtils.wrap(
+ secondaryKey.getSecretKey(), getTertiaryKey(secondaryKey)));
+ restoreTask.getRestoreData(mParcelFileDescriptor);
+ return ShadowBackupDataOutput.getEntities();
+ }
+
+ /** Perform a full restore and return the bytes obtained from the restore process */
+ private byte[] performFullRestore(byte[] backupData)
throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
InvalidAlgorithmParameterException, InvalidKeyException,
IllegalBlockSizeException {
@@ -118,7 +220,9 @@ public class RoundTripTest {
EncryptedFullRestoreTask restoreTask =
EncryptedFullRestoreTask.newInstance(
- mContext, new FakeFullRestoreDownloader(backupData), getTertiaryKey());
+ mContext,
+ new FakeFullRestoreDownloader(backupData),
+ getTertiaryKey(mSecondaryKey));
byte[] buffer = new byte[READ_BUFFER_SIZE];
int bytesRead = restoreTask.readNextChunk(buffer);
@@ -131,7 +235,7 @@ public class RoundTripTest {
}
/** Get the tertiary key for our test package from the key manager */
- private SecretKey getTertiaryKey()
+ private SecretKey getTertiaryKey(RecoverableKeyStoreSecondaryKey secondaryKey)
throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, IOException, NoSuchPaddingException,
InvalidKeyException {
@@ -140,7 +244,7 @@ public class RoundTripTest {
mContext,
mSecureRandom,
TertiaryKeyRotationScheduler.getInstance(mContext),
- mSecondaryKey,
+ secondaryKey,
TEST_PACKAGE_NAME);
return tertiaryKeyManager.getKey();
}
@@ -162,13 +266,13 @@ public class RoundTripTest {
}
/**
- * Dummy backup data endpoint. This stores the data so we can use it
- * in subsequent test steps.
+ * Dummy backup data endpoint. This stores the data so we can use it in subsequent test steps.
*/
private static class DummyServer implements CryptoBackupServer {
private static final String DUMMY_DOC_ID = "DummyDoc";
byte[] mStoredData = null;
+ String mSecondaryKeyAlias;
@Override
public String uploadIncrementalBackup(
@@ -190,7 +294,7 @@ public class RoundTripTest {
@Override
public void setActiveSecondaryKeyAlias(
String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
- throw new RuntimeException("Not Implemented");
+ mSecondaryKeyAlias = keyAlias;
}
}
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/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java
new file mode 100644
index 000000000000..6666d95d9a2d
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedKvRestoreTaskTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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 org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertThrows;
+
+import android.os.ParcelFileDescriptor;
+
+import com.android.server.backup.encryption.chunk.ChunkHash;
+import com.android.server.backup.encryption.chunking.ChunkHasher;
+import com.android.server.backup.testing.CryptoTestUtils;
+import com.android.server.testing.shadows.DataEntity;
+import com.android.server.testing.shadows.ShadowBackupDataOutput;
+
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+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.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+
+@Config(shadows = {ShadowBackupDataOutput.class})
+@RunWith(RobolectricTestRunner.class)
+public class EncryptedKvRestoreTaskTest {
+ private static final String TEST_KEY_1 = "test_key_1";
+ private static final String TEST_KEY_2 = "test_key_2";
+ private static final String TEST_KEY_3 = "test_key_3";
+ private static final byte[] TEST_VALUE_1 = {1, 2, 3};
+ private static final byte[] TEST_VALUE_2 = {4, 5, 6};
+ private static final byte[] TEST_VALUE_3 = {20, 25, 30, 35};
+
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private File temporaryDirectory;
+
+ @Mock private ParcelFileDescriptor mParcelFileDescriptor;
+ @Mock private ChunkHasher mChunkHasher;
+ @Mock private FullRestoreToFileTask mFullRestoreToFileTask;
+ @Mock private BackupFileDecryptorTask mBackupFileDecryptorTask;
+
+ private EncryptedKvRestoreTask task;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mChunkHasher.computeHash(any()))
+ .thenAnswer(invocation -> fakeHash(invocation.getArgument(0)));
+ doAnswer(invocation -> writeTestPairsToFile(invocation.getArgument(0)))
+ .when(mFullRestoreToFileTask)
+ .restoreToFile(any());
+ doAnswer(
+ invocation ->
+ readPairsFromFile(
+ invocation.getArgument(0), invocation.getArgument(1)))
+ .when(mBackupFileDecryptorTask)
+ .decryptFile(any(), any());
+
+ temporaryDirectory = temporaryFolder.newFolder();
+ task =
+ new EncryptedKvRestoreTask(
+ temporaryDirectory,
+ mChunkHasher,
+ mFullRestoreToFileTask,
+ mBackupFileDecryptorTask);
+ }
+
+ @Test
+ public void testGetRestoreData_writesPairsToOutputInOrder() throws Exception {
+ task.getRestoreData(mParcelFileDescriptor);
+
+ assertThat(ShadowBackupDataOutput.getEntities())
+ .containsExactly(
+ new DataEntity(TEST_KEY_1, TEST_VALUE_1),
+ new DataEntity(TEST_KEY_2, TEST_VALUE_2),
+ new DataEntity(TEST_KEY_3, TEST_VALUE_3))
+ .inOrder();
+ }
+
+ @Test
+ public void testGetRestoreData_exceptionDuringDecryption_throws() throws Exception {
+ doThrow(IOException.class).when(mBackupFileDecryptorTask).decryptFile(any(), any());
+ assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+ }
+
+ @Test
+ public void testGetRestoreData_exceptionDuringDownload_throws() throws Exception {
+ doThrow(IOException.class).when(mFullRestoreToFileTask).restoreToFile(any());
+ assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+ }
+
+ @Test
+ public void testGetRestoreData_exceptionDuringDecryption_deletesTemporaryFiles() throws Exception {
+ doThrow(InvalidKeyException.class).when(mBackupFileDecryptorTask).decryptFile(any(), any());
+ assertThrows(InvalidKeyException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+ assertThat(temporaryDirectory.listFiles()).isEmpty();
+ }
+
+ @Test
+ public void testGetRestoreData_exceptionDuringDownload_deletesTemporaryFiles() throws Exception {
+ doThrow(IOException.class).when(mFullRestoreToFileTask).restoreToFile(any());
+ assertThrows(IOException.class, () -> task.getRestoreData(mParcelFileDescriptor));
+ assertThat(temporaryDirectory.listFiles()).isEmpty();
+ }
+
+ private static Void writeTestPairsToFile(File file) throws IOException {
+ // Write the pairs out of order to check the task sorts them.
+ Set<byte[]> pairs =
+ new HashSet<>(
+ Arrays.asList(
+ createPair(TEST_KEY_1, TEST_VALUE_1),
+ createPair(TEST_KEY_3, TEST_VALUE_3),
+ createPair(TEST_KEY_2, TEST_VALUE_2)));
+
+ try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file))) {
+ oos.writeObject(pairs);
+ }
+ return null;
+ }
+
+ private static Void readPairsFromFile(File file, DecryptedChunkOutput decryptedChunkOutput)
+ throws IOException, ClassNotFoundException, InvalidKeyException,
+ NoSuchAlgorithmException {
+ try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
+ DecryptedChunkOutput output = decryptedChunkOutput.open()) {
+ Set<byte[]> pairs = readPairs(ois);
+ for (byte[] pair : pairs) {
+ output.processChunk(pair, pair.length);
+ }
+ }
+
+ return null;
+ }
+
+ private static byte[] createPair(String key, byte[] value) {
+ return MessageNano.toByteArray(CryptoTestUtils.newPair(key, value));
+ }
+
+ @SuppressWarnings("unchecked") // deserialization.
+ private static Set<byte[]> readPairs(ObjectInputStream ois)
+ throws IOException, ClassNotFoundException {
+ return (Set<byte[]>) ois.readObject();
+ }
+
+ private static ChunkHash fakeHash(byte[] data) {
+ return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES));
+ }
+}
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;
+ }
}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
new file mode 100644
index 000000000000..2302e555fb44
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/shadows/ShadowBackupDataOutput.java
@@ -0,0 +1,66 @@
+/*
+ * 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.testing.shadows;
+
+import android.app.backup.BackupDataOutput;
+
+import java.io.FileDescriptor;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.Assert;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+/** Shadow for BackupDataOutput. */
+@Implements(BackupDataOutput.class)
+public class ShadowBackupDataOutput {
+ private static final List<DataEntity> ENTRIES = new ArrayList<>();
+
+ private String mCurrentKey;
+ private int mDataSize;
+
+ public static void reset() {
+ ENTRIES.clear();
+ }
+
+ public static Set<DataEntity> getEntities() {
+ return new LinkedHashSet<>(ENTRIES);
+ }
+
+ public void __constructor__(FileDescriptor fd) {}
+
+ public void __constructor__(FileDescriptor fd, long quota) {}
+
+ public void __constructor__(FileDescriptor fd, long quota, int transportFlags) {}
+
+ @Implementation
+ public int writeEntityHeader(String key, int size) {
+ mCurrentKey = key;
+ mDataSize = size;
+ return 0;
+ }
+
+ @Implementation
+ public int writeEntityData(byte[] data, int size) {
+ Assert.assertEquals("ShadowBackupDataOutput expects size = mDataSize", size, mDataSize);
+ ENTRIES.add(new DataEntity(mCurrentKey, data, mDataSize));
+ return 0;
+ }
+}