diff options
3 files changed, 390 insertions, 0 deletions
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/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/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; + } +} |