blob: a432d91828cf7231c436cbb80533fca9bb9bfe62 [file] [log] [blame]
/*
* 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;
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;
import java.io.IOException;
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
/** Buffer size used when reading data from the restore task */
private static final int READ_BUFFER_SIZE = 1024; // 1024 byte buffer.
/** 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 */
private static final String TEST_PACKAGE_NAME = "com.android.backup.test";
/** The name we use to refer to our secondary key */
private static final String TEST_KEY_ALIAS = "test/backup/KEY_ALIAS";
/** Original data used for comparison after round trip */
private final byte[] mOriginalData = new byte[TEST_DATA_SIZE];
/** App context, used to store the key data and chunk listings */
private Context mContext;
/** The secondary key we're using for the test */
private RecoverableKeyStoreSecondaryKey mSecondaryKey;
/** Source of random material which is considered non-predictable in its' generation */
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, 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 testFull_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
byte[] backupData = performFullBackup(mOriginalData);
assertThat(backupData).isNotEqualTo(mOriginalData);
byte[] restoredData = performFullRestore(backupData);
assertThat(restoredData).isEqualTo(mOriginalData);
}
@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(
mContext,
dummyServer,
mSecureRandom,
mSecondaryKey,
TEST_PACKAGE_NAME,
new ByteArrayInputStream(backupData));
backupTask.call();
return dummyServer.mStoredData;
}
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 {
ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
EncryptedFullRestoreTask restoreTask =
EncryptedFullRestoreTask.newInstance(
mContext,
new FakeFullRestoreDownloader(backupData),
getTertiaryKey(mSecondaryKey));
byte[] buffer = new byte[READ_BUFFER_SIZE];
int bytesRead = restoreTask.readNextChunk(buffer);
while (bytesRead != -1) {
decryptedOutput.write(buffer, 0, bytesRead);
bytesRead = restoreTask.readNextChunk(buffer);
}
return decryptedOutput.toByteArray();
}
/** Get the tertiary key for our test package from the key manager */
private SecretKey getTertiaryKey(RecoverableKeyStoreSecondaryKey secondaryKey)
throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, IOException, NoSuchPaddingException,
InvalidKeyException {
TertiaryKeyManager tertiaryKeyManager =
new TertiaryKeyManager(
mContext,
mSecureRandom,
TertiaryKeyRotationScheduler.getInstance(mContext),
secondaryKey,
TEST_PACKAGE_NAME);
return tertiaryKeyManager.getKey();
}
/** Fill a buffer with data in a predictable way */
private void fillBuffer(byte[] buffer) {
byte loopingCounter = 0;
for (int i = 0; i < buffer.length; i++) {
buffer[i] = loopingCounter;
loopingCounter++;
}
}
/** Generate a new, random, AES key */
public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
keyGenerator.init(KEY_SIZE_BITS);
return keyGenerator.generateKey();
}
/**
* 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(
String packageName,
String oldDocId,
byte[] diffScript,
WrappedKeyProto.WrappedKey tertiaryKey) {
throw new RuntimeException("Not Implemented");
}
@Override
public String uploadNonIncrementalBackup(
String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
assertThat(packageName).isEqualTo(TEST_PACKAGE_NAME);
mStoredData = data;
return DUMMY_DOC_ID;
}
@Override
public void setActiveSecondaryKeyAlias(
String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
mSecondaryKeyAlias = keyAlias;
}
}
/** Fake package wrapper which returns data from a byte array. */
private static class FakeFullRestoreDownloader extends FullRestoreDownloader {
private final ByteArrayInputStream mData;
FakeFullRestoreDownloader(byte[] data) {
// We override all methods of the superclass, so it does not require any collaborators.
super();
mData = new ByteArrayInputStream(data);
}
@Override
public int readNextChunk(byte[] buffer) throws IOException {
return mData.read(buffer);
}
@Override
public void finish(FinishType finishType) {
// Do nothing.
}
}
}