blob: 096b2da10c9820534d73113f9cc7d5a593a8425a [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 org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertThrows;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto.WrappedKey;
import com.android.server.backup.testing.CryptoTestUtils;
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.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Optional;
import javax.crypto.SecretKey;
@Config(shadows = {EncryptedBackupTaskTest.ShadowBackupFileBuilder.class})
@RunWith(RobolectricTestRunner.class)
public class EncryptedFullBackupTaskTest {
private static final String TEST_PACKAGE_NAME = "com.example.package";
private static final byte[] TEST_EXISTING_FINGERPRINT_MIXER_SALT =
Arrays.copyOf(new byte[] {11}, ChunkHash.HASH_LENGTH_BYTES);
private static final byte[] TEST_GENERATED_FINGERPRINT_MIXER_SALT =
Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
private static final ChunkHash TEST_CHUNK_HASH_1 =
new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
private static final ChunkHash TEST_CHUNK_HASH_2 =
new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
private static final int TEST_CHUNK_LENGTH_1 = 20;
private static final int TEST_CHUNK_LENGTH_2 = 40;
@Mock private ProtoStore<ChunkListing> mChunkListingStore;
@Mock private TertiaryKeyManager mTertiaryKeyManager;
@Mock private InputStream mInputStream;
@Mock private EncryptedBackupTask mEncryptedBackupTask;
@Mock private SecretKey mTertiaryKey;
@Mock private SecureRandom mSecureRandom;
private EncryptedFullBackupTask mTask;
private ChunkListing mOldChunkListing;
private ChunkListing mNewChunkListing;
private WrappedKey mWrappedTertiaryKey;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mWrappedTertiaryKey = new WrappedKey();
when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);
mOldChunkListing =
CryptoTestUtils.newChunkListing(
/* docId */ null,
TEST_EXISTING_FINGERPRINT_MIXER_SALT,
ChunksMetadataProto.AES_256_GCM,
ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
CryptoTestUtils.newChunk(TEST_CHUNK_HASH_1.getHash(), TEST_CHUNK_LENGTH_1));
mNewChunkListing =
CryptoTestUtils.newChunkListing(
/* docId */ null,
/* fingerprintSalt */ null,
ChunksMetadataProto.AES_256_GCM,
ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
CryptoTestUtils.newChunk(TEST_CHUNK_HASH_1.getHash(), TEST_CHUNK_LENGTH_1),
CryptoTestUtils.newChunk(TEST_CHUNK_HASH_2.getHash(), TEST_CHUNK_LENGTH_2));
when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
.thenReturn(mNewChunkListing);
when(mEncryptedBackupTask.performIncrementalBackup(any(), any(), any()))
.thenReturn(mNewChunkListing);
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
doAnswer(invocation -> {
byte[] byteArray = (byte[]) invocation.getArguments()[0];
System.arraycopy(
TEST_GENERATED_FINGERPRINT_MIXER_SALT,
/* srcPos */ 0,
byteArray,
/* destPos */ 0,
FingerprintMixer.SALT_LENGTH_BYTES);
return null;
})
.when(mSecureRandom)
.nextBytes(any(byte[].class));
mTask =
new EncryptedFullBackupTask(
mChunkListingStore,
mTertiaryKeyManager,
mEncryptedBackupTask,
mInputStream,
TEST_PACKAGE_NAME,
mSecureRandom);
}
@Test
public void call_existingChunkListingButTertiaryKeyRotated_performsNonIncrementalBackup()
throws Exception {
when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
.thenReturn(Optional.of(mOldChunkListing));
mTask.call();
verify(mEncryptedBackupTask)
.performNonIncrementalBackup(
eq(mTertiaryKey),
eq(mWrappedTertiaryKey),
eq(TEST_GENERATED_FINGERPRINT_MIXER_SALT));
}
@Test
public void call_noExistingChunkListing_performsNonIncrementalBackup() throws Exception {
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
mTask.call();
verify(mEncryptedBackupTask)
.performNonIncrementalBackup(
eq(mTertiaryKey),
eq(mWrappedTertiaryKey),
eq(TEST_GENERATED_FINGERPRINT_MIXER_SALT));
}
@Test
public void call_existingChunkListing_performsIncrementalBackup() throws Exception {
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
.thenReturn(Optional.of(mOldChunkListing));
mTask.call();
verify(mEncryptedBackupTask)
.performIncrementalBackup(
eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(mOldChunkListing));
}
@Test
public void
call_existingChunkListingWithNoFingerprintMixerSalt_doesntSetSaltBeforeIncBackup()
throws Exception {
mOldChunkListing.fingerprintMixerSalt = new byte[0];
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
.thenReturn(Optional.of(mOldChunkListing));
mTask.call();
verify(mEncryptedBackupTask)
.performIncrementalBackup(
eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(mOldChunkListing));
}
@Test
public void call_noExistingChunkListing_storesNewChunkListing() throws Exception {
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
mTask.call();
verify(mChunkListingStore).saveProto(TEST_PACKAGE_NAME, mNewChunkListing);
}
@Test
public void call_existingChunkListing_storesNewChunkListing() throws Exception {
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
.thenReturn(Optional.of(mOldChunkListing));
mTask.call();
verify(mChunkListingStore).saveProto(TEST_PACKAGE_NAME, mNewChunkListing);
}
@Test
public void call_exceptionDuringBackup_doesNotSaveNewChunkListing() throws Exception {
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME)).thenReturn(Optional.empty());
when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
.thenThrow(GeneralSecurityException.class);
assertThrows(Exception.class, () -> mTask.call());
assertThat(mChunkListingStore.loadProto(TEST_PACKAGE_NAME).isPresent()).isFalse();
}
@Test
public void call_incrementalThrowsPermanentException_clearsState() throws Exception {
when(mChunkListingStore.loadProto(TEST_PACKAGE_NAME))
.thenReturn(Optional.of(mOldChunkListing));
when(mEncryptedBackupTask.performIncrementalBackup(any(), any(), any()))
.thenThrow(IOException.class);
assertThrows(IOException.class, () -> mTask.call());
verify(mChunkListingStore).deleteProto(TEST_PACKAGE_NAME);
}
@Test
public void call_closesInputStream() throws Exception {
mTask.call();
verify(mInputStream).close();
}
@Test
public void cancel_cancelsTask() throws Exception {
mTask.cancel();
verify(mEncryptedBackupTask).cancel();
}
}