blob: fa4fef50ac1ae66363a49fab9174426117cafec0 [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.tasks;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertThrows;
import android.app.Application;
import android.util.Pair;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import com.android.server.backup.testing.CryptoTestUtils;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;
import javax.crypto.SecretKey;
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.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public class EncryptedKvBackupTaskTest {
private static final boolean INCREMENTAL = true;
private static final boolean NON_INCREMENTAL = false;
private static final String TEST_PACKAGE_1 = "com.example.app1";
private static final String TEST_KEY_1 = "key_1";
private static final String TEST_KEY_2 = "key_2";
private static final ChunkHash TEST_HASH_1 =
new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
private static final ChunkHash TEST_HASH_2 =
new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
private static final int TEST_LENGTH_1 = 200;
private static final int TEST_LENGTH_2 = 300;
@Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
@Captor private ArgumentCaptor<ChunksMetadataProto.ChunkListing> mChunkListingCaptor;
@Mock private TertiaryKeyManager mTertiaryKeyManager;
@Mock private RecoverableKeyStoreSecondaryKey mSecondaryKey;
@Mock private ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
@Mock private ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
@Mock private KvBackupEncrypter mKvBackupEncrypter;
@Mock private EncryptedBackupTask mEncryptedBackupTask;
@Mock private SecretKey mTertiaryKey;
private WrappedKeyProto.WrappedKey mWrappedTertiaryKey;
private KeyValueListingProto.KeyValueListing mNewKeyValueListing;
private ChunksMetadataProto.ChunkListing mNewChunkListing;
private EncryptedKvBackupTask mTask;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
Application application = ApplicationProvider.getApplicationContext();
mKeyValueListingStore = ProtoStore.createKeyValueListingStore(application);
mChunkListingStore = ProtoStore.createChunkListingStore(application);
mWrappedTertiaryKey = new WrappedKeyProto.WrappedKey();
when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(false);
when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
mNewKeyValueListing =
createKeyValueListing(
CryptoTestUtils.mapOf(
new Pair<>(TEST_KEY_1, TEST_HASH_1),
new Pair<>(TEST_KEY_2, TEST_HASH_2)));
mNewChunkListing =
createChunkListing(
CryptoTestUtils.mapOf(
new Pair<>(TEST_HASH_1, TEST_LENGTH_1),
new Pair<>(TEST_HASH_2, TEST_LENGTH_2)));
when(mKvBackupEncrypter.getNewKeyValueListing()).thenReturn(mNewKeyValueListing);
when(mEncryptedBackupTask.performIncrementalBackup(
eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
.thenReturn(mNewChunkListing);
when(mEncryptedBackupTask.performNonIncrementalBackup(
eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
.thenReturn(mNewChunkListing);
mTask =
new EncryptedKvBackupTask(
mTertiaryKeyManager,
mKeyValueListingStore,
mSecondaryKey,
mChunkListingStore,
mKvBackupEncrypter,
mEncryptedBackupTask,
TEST_PACKAGE_1);
}
@Test
public void testPerformBackup_rotationRequired_deletesListings() throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
mChunkListingStore.saveProto(
TEST_PACKAGE_1,
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
// Throw an IOException so it aborts before saving the new listings.
when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
.thenThrow(IOException.class);
assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
}
@Test
public void testPerformBackup_rotationRequiredButIncremental_throws() throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
mChunkListingStore.saveProto(
TEST_PACKAGE_1,
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
assertThrows(NonIncrementalBackupRequiredException.class,
() -> mTask.performBackup(INCREMENTAL));
}
@Test
public void testPerformBackup_rotationRequiredAndNonIncremental_performsNonIncrementalBackup()
throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
mChunkListingStore.saveProto(
TEST_PACKAGE_1,
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
mTask.performBackup(NON_INCREMENTAL);
verify(mEncryptedBackupTask)
.performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), any());
}
@Test
public void testPerformBackup_existingStateButNonIncremental_deletesListings() throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
mChunkListingStore.saveProto(
TEST_PACKAGE_1,
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
// Throw an IOException so it aborts before saving the new listings.
when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
.thenThrow(IOException.class);
assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
}
@Test
public void testPerformBackup_keyValueListingMissing_deletesChunkListingAndPerformsNonIncremental()
throws Exception {
mChunkListingStore.saveProto(
TEST_PACKAGE_1,
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
// Throw an IOException so it aborts before saving the new listings.
when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
.thenThrow(IOException.class);
assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
}
@Test
public void testPerformBackup_chunkListingMissing_deletesKeyValueListingAndPerformsNonIncremental()
throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
// Throw an IOException so it aborts before saving the new listings.
when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
.thenThrow(IOException.class);
assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));
verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
}
@Test
public void testPerformBackup_existingStateAndIncremental_performsIncrementalBackup()
throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
ChunksMetadataProto.ChunkListing oldChunkListing =
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1)));
mChunkListingStore.saveProto(TEST_PACKAGE_1, oldChunkListing);
mTask.performBackup(INCREMENTAL);
verify(mEncryptedBackupTask)
.performIncrementalBackup(
eq(mTertiaryKey), eq(mWrappedTertiaryKey), mChunkListingCaptor.capture());
assertChunkListingsEqual(mChunkListingCaptor.getValue(), oldChunkListing);
}
@Test
public void testPerformBackup_noExistingStateAndNonIncremental_performsNonIncrementalBackup()
throws Exception {
mTask.performBackup(NON_INCREMENTAL);
verify(mEncryptedBackupTask)
.performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(null));
}
@Test
public void testPerformBackup_incremental_savesNewListings() throws Exception {
mKeyValueListingStore.saveProto(
TEST_PACKAGE_1,
createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
mChunkListingStore.saveProto(
TEST_PACKAGE_1,
createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));
mTask.performBackup(INCREMENTAL);
KeyValueListingProto.KeyValueListing actualKeyValueListing =
mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
ChunksMetadataProto.ChunkListing actualChunkListing =
mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
}
@Test
public void testPerformBackup_nonIncremental_savesNewListings() throws Exception {
mTask.performBackup(NON_INCREMENTAL);
KeyValueListingProto.KeyValueListing actualKeyValueListing =
mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
ChunksMetadataProto.ChunkListing actualChunkListing =
mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
}
private static KeyValueListingProto.KeyValueListing createKeyValueListing(
Map<String, ChunkHash> pairs) {
return new KeyValueListingBuilder().addAll(pairs).build();
}
private static ChunksMetadataProto.ChunkListing createChunkListing(
Map<ChunkHash, Integer> chunks) {
ChunksMetadataProto.Chunk[] listingChunks = new ChunksMetadataProto.Chunk[chunks.size()];
int chunksAdded = 0;
for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
listingChunks[chunksAdded] = CryptoTestUtils.newChunk(entry.getKey(), entry.getValue());
chunksAdded++;
}
return CryptoTestUtils.newChunkListingWithoutDocId(
/* fingerprintSalt */ new byte[0],
ChunksMetadataProto.AES_256_GCM,
ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
listingChunks);
}
private static void assertKeyValueListingsEqual(
KeyValueListingProto.KeyValueListing actual,
KeyValueListingProto.KeyValueListing expected) {
KeyValueListingProto.KeyValueEntry[] actualEntries = actual.entries;
KeyValueListingProto.KeyValueEntry[] expectedEntries = expected.entries;
assertThat(actualEntries.length).isEqualTo(expectedEntries.length);
for (int i = 0; i < actualEntries.length; i++) {
assertWithMessage("entry " + i)
.that(actualEntries[i].key)
.isEqualTo(expectedEntries[i].key);
assertWithMessage("entry " + i)
.that(actualEntries[i].hash)
.isEqualTo(expectedEntries[i].hash);
}
}
private static void assertChunkListingsEqual(
ChunksMetadataProto.ChunkListing actual, ChunksMetadataProto.ChunkListing expected) {
ChunksMetadataProto.Chunk[] actualChunks = actual.chunks;
ChunksMetadataProto.Chunk[] expectedChunks = expected.chunks;
assertThat(actualChunks.length).isEqualTo(expectedChunks.length);
for (int i = 0; i < actualChunks.length; i++) {
assertWithMessage("chunk " + i)
.that(actualChunks[i].hash)
.isEqualTo(expectedChunks[i].hash);
assertWithMessage("chunk " + i)
.that(actualChunks[i].length)
.isEqualTo(expectedChunks[i].length);
}
assertThat(actual.cipherType).isEqualTo(expected.cipherType);
assertThat(actual.documentId)
.isEqualTo(expected.documentId == null ? "" : expected.documentId);
}
}