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 0000000000000..12b44590ebe61 --- /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. + * + *
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. + * + *
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/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 0000000000000..6666d95d9a2d6
--- /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