Import EncryptedKvRestoreTask

Bug: 111386661
Test: atest EncryptedKvRestoreTaskTest
Change-Id: Id603dabce098ef05471a76095d0cd25e95a681a5
This commit is contained in:
Ruslan Tkhakokhov
2019-10-01 12:29:58 +01:00
parent f45db5e35a
commit 7058c19587
3 changed files with 390 additions and 0 deletions

View File

@@ -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);
}
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}