blob: 21c4e07577da21d080e31cae3df8dda4bd9c1fea [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 android.platform.test.annotations.Presubmit;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.EncryptedChunk;
import com.android.server.backup.testing.CryptoTestUtils;
import com.android.server.backup.testing.RandomInputStream;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import javax.crypto.SecretKey;
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class BackupStreamEncrypterTest {
private static final int SALT_LENGTH = 32;
private static final int BITS_PER_BYTE = 8;
private static final int BYTES_PER_KILOBYTE = 1024;
private static final int BYTES_PER_MEGABYTE = 1024 * 1024;
private static final int MIN_CHUNK_SIZE = 2 * BYTES_PER_KILOBYTE;
private static final int AVERAGE_CHUNK_SIZE = 4 * BYTES_PER_KILOBYTE;
private static final int MAX_CHUNK_SIZE = 64 * BYTES_PER_KILOBYTE;
private static final int BACKUP_SIZE = 2 * BYTES_PER_MEGABYTE;
private static final int SMALL_BACKUP_SIZE = BYTES_PER_KILOBYTE;
// 16 bytes for the mac. iv is encoded in a separate field.
private static final int BYTES_OVERHEAD_PER_CHUNK = 16;
private static final int MESSAGE_DIGEST_SIZE_IN_BYTES = 256 / BITS_PER_BYTE;
private static final int RANDOM_SEED = 42;
private static final double TOLERANCE = 0.1;
private Random mRandom;
private SecretKey mSecretKey;
private byte[] mSalt;
@Before
public void setUp() throws Exception {
mSecretKey = CryptoTestUtils.generateAesKey();
mSalt = new byte[SALT_LENGTH];
// Make these tests deterministic
mRandom = new Random(RANDOM_SEED);
mRandom.nextBytes(mSalt);
}
@Test
public void testBackup_producesChunksOfTheGivenAverageSize() throws Exception {
BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
long totalSize = 0;
for (EncryptedChunk chunk : result.getNewChunks()) {
totalSize += chunk.encryptedBytes().length;
}
double meanSize = totalSize / result.getNewChunks().size();
double expectedChunkSize = AVERAGE_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK;
assertThat(Math.abs(meanSize - expectedChunkSize) / expectedChunkSize)
.isLessThan(TOLERANCE);
}
@Test
public void testBackup_producesNoChunksSmallerThanMinSize() throws Exception {
BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
List<EncryptedChunk> chunks = result.getNewChunks();
// Last chunk could be smaller, depending on the file size and how it is chunked
for (EncryptedChunk chunk : chunks.subList(0, chunks.size() - 2)) {
assertThat(chunk.encryptedBytes().length)
.isAtLeast(MIN_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK);
}
}
@Test
public void testBackup_producesNoChunksLargerThanMaxSize() throws Exception {
BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
List<EncryptedChunk> chunks = result.getNewChunks();
for (EncryptedChunk chunk : chunks) {
assertThat(chunk.encryptedBytes().length)
.isAtMost(MAX_CHUNK_SIZE + BYTES_OVERHEAD_PER_CHUNK);
}
}
@Test
public void testBackup_producesAFileOfTheExpectedSize() throws Exception {
BackupEncrypter.Result result = runBackup(BACKUP_SIZE);
HashMap<ChunkHash, EncryptedChunk> chunksBySha256 =
chunksIndexedByKey(result.getNewChunks());
int expectedSize = BACKUP_SIZE + result.getAllChunks().size() * BYTES_OVERHEAD_PER_CHUNK;
int size = 0;
for (ChunkHash byteString : result.getAllChunks()) {
size += chunksBySha256.get(byteString).encryptedBytes().length;
}
assertThat(size).isEqualTo(expectedSize);
}
@Test
public void testBackup_forSameFile_producesNoNewChunks() throws Exception {
byte[] backupData = getRandomData(BACKUP_SIZE);
BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
assertThat(incrementalResult.getNewChunks()).isEmpty();
}
@Test
public void testBackup_onlyUpdatesChangedChunks() throws Exception {
byte[] backupData = getRandomData(BACKUP_SIZE);
BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
// Let's update the 2nd and 5th chunk
backupData[positionOfChunk(result, 1)]++;
backupData[positionOfChunk(result, 4)]++;
BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
assertThat(incrementalResult.getNewChunks()).hasSize(2);
}
@Test
public void testBackup_doesNotIncludeUpdatedChunksInNewListing() throws Exception {
byte[] backupData = getRandomData(BACKUP_SIZE);
BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
// Let's update the 2nd and 5th chunk
backupData[positionOfChunk(result, 1)]++;
backupData[positionOfChunk(result, 4)]++;
BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
List<EncryptedChunk> newChunks = incrementalResult.getNewChunks();
List<ChunkHash> chunkListing = result.getAllChunks();
assertThat(newChunks).doesNotContain(chunkListing.get(1));
assertThat(newChunks).doesNotContain(chunkListing.get(4));
}
@Test
public void testBackup_includesUnchangedChunksInNewListing() throws Exception {
byte[] backupData = getRandomData(BACKUP_SIZE);
BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
// Let's update the 2nd and 5th chunk
backupData[positionOfChunk(result, 1)]++;
backupData[positionOfChunk(result, 4)]++;
BackupEncrypter.Result incrementalResult = runBackup(backupData, result.getAllChunks());
HashSet<ChunkHash> chunksPresentInIncremental =
new HashSet<>(incrementalResult.getAllChunks());
chunksPresentInIncremental.removeAll(result.getAllChunks());
assertThat(chunksPresentInIncremental).hasSize(2);
}
@Test
public void testBackup_forSameData_createsSameDigest() throws Exception {
byte[] backupData = getRandomData(SMALL_BACKUP_SIZE);
BackupEncrypter.Result result = runBackup(backupData, ImmutableList.of());
BackupEncrypter.Result result2 = runBackup(backupData, ImmutableList.of());
assertThat(result.getDigest()).isEqualTo(result2.getDigest());
}
@Test
public void testBackup_forDifferentData_createsDifferentDigest() throws Exception {
byte[] backup1Data = getRandomData(SMALL_BACKUP_SIZE);
byte[] backup2Data = getRandomData(SMALL_BACKUP_SIZE);
BackupEncrypter.Result result = runBackup(backup1Data, ImmutableList.of());
BackupEncrypter.Result result2 = runBackup(backup2Data, ImmutableList.of());
assertThat(result.getDigest()).isNotEqualTo(result2.getDigest());
}
@Test
public void testBackup_createsDigestOf32Bytes() throws Exception {
assertThat(runBackup(getRandomData(SMALL_BACKUP_SIZE), ImmutableList.of()).getDigest())
.hasLength(MESSAGE_DIGEST_SIZE_IN_BYTES);
}
private byte[] getRandomData(int size) throws Exception {
RandomInputStream randomInputStream = new RandomInputStream(mRandom, size);
byte[] backupData = new byte[size];
randomInputStream.read(backupData);
return backupData;
}
private BackupEncrypter.Result runBackup(int backupSize) throws Exception {
RandomInputStream dataStream = new RandomInputStream(mRandom, backupSize);
BackupStreamEncrypter task =
new BackupStreamEncrypter(
dataStream, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, AVERAGE_CHUNK_SIZE);
return task.backup(mSecretKey, mSalt, ImmutableSet.of());
}
private BackupEncrypter.Result runBackup(byte[] data, List<ChunkHash> existingChunks)
throws Exception {
ByteArrayInputStream dataStream = new ByteArrayInputStream(data);
BackupStreamEncrypter task =
new BackupStreamEncrypter(
dataStream, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, AVERAGE_CHUNK_SIZE);
return task.backup(mSecretKey, mSalt, ImmutableSet.copyOf(existingChunks));
}
/** Returns a {@link HashMap} of the chunks, indexed by the SHA-256 Mac key. */
private static HashMap<ChunkHash, EncryptedChunk> chunksIndexedByKey(
List<EncryptedChunk> chunks) {
HashMap<ChunkHash, EncryptedChunk> chunksByKey = new HashMap<>();
for (EncryptedChunk chunk : chunks) {
chunksByKey.put(chunk.key(), chunk);
}
return chunksByKey;
}
/**
* Returns the start position of the chunk in the plaintext backup data.
*
* @param result The result from a backup.
* @param index The index of the chunk in question.
* @return the start position.
*/
private static int positionOfChunk(BackupEncrypter.Result result, int index) {
HashMap<ChunkHash, EncryptedChunk> byKey = chunksIndexedByKey(result.getNewChunks());
List<ChunkHash> listing = result.getAllChunks();
int position = 0;
for (int i = 0; i < index - 1; i++) {
EncryptedChunk chunk = byKey.get(listing.get(i));
position += chunk.encryptedBytes().length - BYTES_OVERHEAD_PER_CHUNK;
}
return position;
}
}