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