Import EncryptedFull???Task

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I135f561a2590206f7131eb14df3f91211fef3daa
This commit is contained in:
Al Sutton
2019-09-23 14:08:24 +01:00
parent ea5c362d63
commit 836548b7b9
9 changed files with 921 additions and 1 deletions

View File

@@ -0,0 +1,109 @@
/*
* 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 android.app.backup.BackupTransport;
import java.io.IOException;
import java.io.InputStream;
/** Accepts the full backup data stream and sends it to the server. */
public interface FullBackupDataProcessor {
/**
* Prepares the upload.
*
* <p>After this, call {@link #start()} to establish the connection.
*
* @param inputStream to read the backup data from, calling {@link #finish} or {@link #cancel}
* will close the stream
* @return {@code true} if the connection was set up successfully, otherwise {@code false}
*/
boolean initiate(InputStream inputStream) throws IOException;
/**
* Starts the upload, establishing the connection to the server.
*
* <p>After this, call {@link #pushData(int)} to request that the processor reads data from the
* socket, and uploads it to the server.
*
* <p>After this you must call one of {@link #cancel()}, {@link #finish()}, {@link
* #handleCheckSizeRejectionZeroBytes()}, {@link #handleCheckSizeRejectionQuotaExceeded()} or
* {@link #handleSendBytesQuotaExceeded()} to close the upload.
*/
void start();
/**
* Requests that the processor read {@code numBytes} from the input stream passed in {@link
* #initiate(InputStream)} and upload them to the server.
*
* @return {@link BackupTransport#TRANSPORT_OK} if the upload succeeds, or {@link
* BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
* quota, or {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
*/
int pushData(int numBytes);
/** Cancels the upload and tears down the connection. */
void cancel();
/**
* Finish the upload and tear down the connection.
*
* <p>Call this after there is no more data to push with {@link #pushData(int)}.
*
* @return One of {@link BackupTransport#TRANSPORT_OK} if the app upload succeeds, {@link
* BackupTransport#TRANSPORT_QUOTA_EXCEEDED} if the upload exceeded the server-side app size
* quota, {@link BackupTransport#TRANSPORT_ERROR} for server 500s, or {@link
* BackupTransport#TRANSPORT_PACKAGE_REJECTED} for other errors.
*/
int finish();
/**
* Notifies the processor that the current upload should be terminated because the estimated
* size is zero.
*/
void handleCheckSizeRejectionZeroBytes();
/**
* Notifies the processor that the current upload should be terminated because the estimated
* size exceeds the quota.
*/
void handleCheckSizeRejectionQuotaExceeded();
/**
* Notifies this class that the current upload should be terminated because the quota was
* exceeded during upload.
*/
void handleSendBytesQuotaExceeded();
/**
* Attaches {@link FullBackupCallbacks} which the processor will notify when the backup
* succeeds.
*/
void attachCallbacks(FullBackupCallbacks fullBackupCallbacks);
/**
* Implemented by the caller of the processor to receive notification of when the backup
* succeeds.
*/
interface FullBackupCallbacks {
/** The processor calls this to indicate that the current backup has succeeded. */
void onSuccess();
/** The processor calls this if the upload failed for a non-transient reason. */
void onTransferFailed();
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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 java.io.IOException;
/**
* Retrieves the data during a full restore, decrypting it if necessary.
*
* <p>Use {@link FullRestoreDataProcessorFactory} to construct the encrypted or unencrypted
* processor as appropriate during restore.
*/
public interface FullRestoreDataProcessor {
/** Return value of {@link #readNextChunk} when there is no more data to download. */
int END_OF_STREAM = -1;
/**
* Reads the next chunk of restore data and writes it to the given buffer.
*
* <p>Where necessary, will open the connection to the server and/or decrypt the backup file.
*
* <p>The implementation may retry various errors. If the retries fail it will throw the
* relevant exception.
*
* @return the number of bytes read, or {@link #END_OF_STREAM} if there is no more data
* @throws IOException when downloading from the network or writing to disk
*/
int readNextChunk(byte[] buffer) throws IOException;
/**
* Closes the connection to the server, deletes any temporary files and optionally sends a log
* with the given finish type.
*
* @param finishType one of {@link FullRestoreDownloader.FinishType}
*/
void finish(FullRestoreDownloader.FinishType finishType);
}

View File

@@ -0,0 +1,36 @@
/*
* 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 java.io.Closeable;
import java.io.IOException;
/** Utility methods for dealing with Streams */
public class StreamUtils {
/**
* Close a Closeable and silently ignore any IOExceptions.
*
* @param closeable The closeable to close
*/
public static void closeQuietly(Closeable closeable) {
try {
closeable.close();
} catch (IOException ioe) {
// Silently ignore
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* 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 android.content.Context;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.StreamUtils;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkListing;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.Callable;
import javax.crypto.SecretKey;
/**
* Task which reads a stream of plaintext full backup data, chunks it, encrypts it and uploads it to
* the server.
*
* <p>Once the backup completes or fails, closes the input stream.
*/
public class EncryptedFullBackupTask implements Callable<Void> {
private static final String TAG = "EncryptedFullBackupTask";
private static final int MIN_CHUNK_SIZE_BYTES = 2 * 1024;
private static final int MAX_CHUNK_SIZE_BYTES = 64 * 1024;
private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * 1024;
// TODO(b/69350270): Remove this hard-coded salt and related logic once we feel confident that
// incremental backup has happened at least once for all existing packages/users since we moved
// to
// using a randomly generated salt.
//
// The hard-coded fingerprint mixer salt was used for a short time period before replaced by one
// that is randomly generated on initial non-incremental backup and stored in ChunkListing to be
// reused for succeeding incremental backups. If an old ChunkListing does not have a
// fingerprint_mixer_salt, we assume that it was last backed up before a randomly generated salt
// is used so we use the hardcoded salt and set ChunkListing#fingerprint_mixer_salt to this
// value.
// Eventually all backup ChunkListings will have this field set and then we can remove the
// default
// value in the code.
static final byte[] DEFAULT_FINGERPRINT_MIXER_SALT =
Arrays.copyOf(new byte[] {20, 23}, FingerprintMixer.SALT_LENGTH_BYTES);
private final ProtoStore<ChunkListing> mChunkListingStore;
private final TertiaryKeyManager mTertiaryKeyManager;
private final InputStream mInputStream;
private final EncryptedBackupTask mTask;
private final String mPackageName;
private final SecureRandom mSecureRandom;
/** Creates a new instance with the default min, max and average chunk sizes. */
public static EncryptedFullBackupTask newInstance(
Context context,
CryptoBackupServer cryptoBackupServer,
SecureRandom secureRandom,
RecoverableKeyStoreSecondaryKey secondaryKey,
String packageName,
InputStream inputStream)
throws IOException {
EncryptedBackupTask encryptedBackupTask =
new EncryptedBackupTask(
cryptoBackupServer,
secureRandom,
packageName,
new BackupStreamEncrypter(
inputStream,
MIN_CHUNK_SIZE_BYTES,
MAX_CHUNK_SIZE_BYTES,
AVERAGE_CHUNK_SIZE_BYTES));
TertiaryKeyManager tertiaryKeyManager =
new TertiaryKeyManager(
context,
secureRandom,
TertiaryKeyRotationScheduler.getInstance(context),
secondaryKey,
packageName);
return new EncryptedFullBackupTask(
ProtoStore.createChunkListingStore(context),
tertiaryKeyManager,
encryptedBackupTask,
inputStream,
packageName,
new SecureRandom());
}
@VisibleForTesting
EncryptedFullBackupTask(
ProtoStore<ChunkListing> chunkListingStore,
TertiaryKeyManager tertiaryKeyManager,
EncryptedBackupTask task,
InputStream inputStream,
String packageName,
SecureRandom secureRandom) {
mChunkListingStore = chunkListingStore;
mTertiaryKeyManager = tertiaryKeyManager;
mInputStream = inputStream;
mTask = task;
mPackageName = packageName;
mSecureRandom = secureRandom;
}
@Override
public Void call() throws Exception {
try {
Optional<ChunkListing> maybeOldChunkListing =
mChunkListingStore.loadProto(mPackageName);
if (maybeOldChunkListing.isPresent()) {
Slog.i(TAG, "Found previous chunk listing for " + mPackageName);
}
// If the key has been rotated then we must re-encrypt all of the backup data.
if (mTertiaryKeyManager.wasKeyRotated()) {
Slog.i(
TAG,
"Key was rotated or newly generated for "
+ mPackageName
+ ", so performing a full backup.");
maybeOldChunkListing = Optional.empty();
mChunkListingStore.deleteProto(mPackageName);
}
SecretKey tertiaryKey = mTertiaryKeyManager.getKey();
WrappedKeyProto.WrappedKey wrappedTertiaryKey = mTertiaryKeyManager.getWrappedKey();
ChunkListing newChunkListing;
if (!maybeOldChunkListing.isPresent()) {
byte[] fingerprintMixerSalt = new byte[FingerprintMixer.SALT_LENGTH_BYTES];
mSecureRandom.nextBytes(fingerprintMixerSalt);
newChunkListing =
mTask.performNonIncrementalBackup(
tertiaryKey, wrappedTertiaryKey, fingerprintMixerSalt);
} else {
ChunkListing oldChunkListing = maybeOldChunkListing.get();
if (oldChunkListing.fingerprintMixerSalt == null
|| oldChunkListing.fingerprintMixerSalt.length == 0) {
oldChunkListing.fingerprintMixerSalt = DEFAULT_FINGERPRINT_MIXER_SALT;
}
newChunkListing =
mTask.performIncrementalBackup(
tertiaryKey, wrappedTertiaryKey, oldChunkListing);
}
mChunkListingStore.saveProto(mPackageName, newChunkListing);
Slog.v(TAG, "Saved chunk listing for " + mPackageName);
} catch (IOException e) {
Slog.e(TAG, "Storage exception, wiping state");
mChunkListingStore.deleteProto(mPackageName);
throw e;
} finally {
StreamUtils.closeQuietly(mInputStream);
}
return null;
}
/**
* Signals to the task that the backup has been cancelled. If the upload has not yet started
* then the task will not upload any data to the server or save the new chunk listing.
*
* <p>You must then terminate the input stream.
*/
public void cancel() {
mTask.cancel();
}
}

View File

@@ -0,0 +1,137 @@
/*
* 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.annotation.Nullable;
import android.content.Context;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.FullRestoreDataProcessor;
import com.android.server.backup.encryption.FullRestoreDownloader;
import com.android.server.backup.encryption.StreamUtils;
import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
/** Downloads the encrypted backup file, decrypts it and passes the data to backup manager. */
public class EncryptedFullRestoreTask implements FullRestoreDataProcessor {
private static final String DEFAULT_TEMPORARY_FOLDER = "encrypted_restore_temp";
private static final String ENCRYPTED_FILE_NAME = "encrypted_restore";
private static final String DECRYPTED_FILE_NAME = "decrypted_restore";
private final FullRestoreToFileTask mFullRestoreToFileTask;
private final BackupFileDecryptorTask mBackupFileDecryptorTask;
private final File mEncryptedFile;
private final File mDecryptedFile;
@Nullable private InputStream mDecryptedFileInputStream;
/**
* Creates a new task which stores temporary files in the files directory.
*
* @param fullRestoreDownloader which will download the backup file
* @param tertiaryKey which the backup file is encrypted with
*/
public static EncryptedFullRestoreTask newInstance(
Context context, FullRestoreDownloader fullRestoreDownloader, SecretKey tertiaryKey)
throws NoSuchAlgorithmException, NoSuchPaddingException {
File temporaryFolder = new File(context.getFilesDir(), DEFAULT_TEMPORARY_FOLDER);
temporaryFolder.mkdirs();
return new EncryptedFullRestoreTask(
temporaryFolder, fullRestoreDownloader, new BackupFileDecryptorTask(tertiaryKey));
}
@VisibleForTesting
EncryptedFullRestoreTask(
File temporaryFolder,
FullRestoreDownloader fullRestoreDownloader,
BackupFileDecryptorTask backupFileDecryptorTask) {
checkArgument(temporaryFolder.isDirectory(), "Temporary folder must be existing directory");
mEncryptedFile = new File(temporaryFolder, ENCRYPTED_FILE_NAME);
mDecryptedFile = new File(temporaryFolder, DECRYPTED_FILE_NAME);
mFullRestoreToFileTask = new FullRestoreToFileTask(fullRestoreDownloader);
mBackupFileDecryptorTask = backupFileDecryptorTask;
}
/**
* Reads the next decrypted bytes into the given buffer.
*
* <p>During the first call this method will download the backup file from the server, decrypt
* it and save it to disk. It will then read the bytes from the file on disk.
*
* <p>Once this method has read all the bytes of the file, the caller must call {@link #finish}
* to clean up.
*
* @return the number of bytes read, or {@code -1} on reaching the end of the file
*/
@Override
public int readNextChunk(byte[] buffer) throws IOException {
if (mDecryptedFileInputStream == null) {
try {
mDecryptedFileInputStream = downloadAndDecryptBackup();
} catch (BadPaddingException
| InvalidKeyException
| NoSuchAlgorithmException
| IllegalBlockSizeException
| ShortBufferException
| EncryptedRestoreException
| InvalidAlgorithmParameterException e) {
throw new IOException("Encryption issue", e);
}
}
return mDecryptedFileInputStream.read(buffer);
}
private InputStream downloadAndDecryptBackup()
throws IOException, BadPaddingException, InvalidKeyException, NoSuchAlgorithmException,
IllegalBlockSizeException, ShortBufferException, EncryptedRestoreException,
InvalidAlgorithmParameterException {
mFullRestoreToFileTask.restoreToFile(mEncryptedFile);
mBackupFileDecryptorTask.decryptFile(
mEncryptedFile, new DecryptedChunkFileOutput(mDecryptedFile));
mEncryptedFile.delete();
return new BufferedInputStream(new FileInputStream(mDecryptedFile));
}
/** Cleans up temporary files. */
@Override
public void finish(FullRestoreDownloader.FinishType unusedFinishType) {
// The download is finished and log sent during RestoreToFileTask#restoreToFile(), so we
// don't need to do either of those things here.
StreamUtils.closeQuietly(mDecryptedFileInputStream);
mEncryptedFile.delete();
mDecryptedFile.delete();
}
}

View File

@@ -0,0 +1,24 @@
/*
* 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;
/** Exception thrown when aa backup has exceeded the space allowed for that user */
public class SizeQuotaExceededException extends RuntimeException {
public SizeQuotaExceededException() {
super("Backup size quota exceeded.");
}
}

View File

@@ -0,0 +1,234 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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 java.util.stream.Collectors.toList;
import com.android.server.backup.encryption.FullRestoreDownloader;
import com.google.common.io.Files;
import com.google.common.primitives.Bytes;
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 java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
@RunWith(RobolectricTestRunner.class)
public class EncryptedFullRestoreTaskTest {
private static final int TEST_BUFFER_SIZE = 10;
private static final byte[] TEST_ENCRYPTED_DATA = {1, 2, 3, 4, 5, 6};
private static final byte[] TEST_DECRYPTED_DATA = fakeDecrypt(TEST_ENCRYPTED_DATA);
@Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Mock private BackupFileDecryptorTask mDecryptorTask;
private File mFolder;
private FakeFullRestoreDownloader mFullRestorePackageWrapper;
private EncryptedFullRestoreTask mTask;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mFolder = temporaryFolder.newFolder();
mFullRestorePackageWrapper = new FakeFullRestoreDownloader(TEST_ENCRYPTED_DATA);
doAnswer(
invocation -> {
File source = invocation.getArgument(0);
DecryptedChunkOutput target = invocation.getArgument(1);
byte[] decrypted = fakeDecrypt(Files.toByteArray(source));
target.open();
target.processChunk(decrypted, decrypted.length);
target.close();
return null;
})
.when(mDecryptorTask)
.decryptFile(any(), any());
mTask = new EncryptedFullRestoreTask(mFolder, mFullRestorePackageWrapper, mDecryptorTask);
}
@Test
public void readNextChunk_downloadsAndDecryptsBackup() throws Exception {
ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
byte[] buffer = new byte[TEST_BUFFER_SIZE];
int bytesRead = mTask.readNextChunk(buffer);
while (bytesRead != -1) {
decryptedOutput.write(buffer, 0, bytesRead);
bytesRead = mTask.readNextChunk(buffer);
}
assertThat(decryptedOutput.toByteArray()).isEqualTo(TEST_DECRYPTED_DATA);
}
@Test
public void finish_deletesTemporaryFiles() throws Exception {
mTask.readNextChunk(new byte[10]);
mTask.finish(FullRestoreDownloader.FinishType.UNKNOWN_FINISH);
assertThat(mFolder.listFiles()).isEmpty();
}
/** 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) {
// Nothing to do.
}
}
/** Fake decrypts a byte array by subtracting 1 from each byte. */
private static byte[] fakeDecrypt(byte[] input) {
return Bytes.toArray(Bytes.asList(input).stream().map(b -> b + 1).collect(toList()));
}
}

View File

@@ -78,7 +78,10 @@ public class CryptoTestUtils {
int orderingType,
ChunksMetadataProto.Chunk... chunks) {
ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
chunkListing.fingerprintMixerSalt = Arrays.copyOf(fingerprintSalt, fingerprintSalt.length);
chunkListing.fingerprintMixerSalt =
fingerprintSalt == null
? null
: Arrays.copyOf(fingerprintSalt, fingerprintSalt.length);
chunkListing.cipherType = cipherType;
chunkListing.chunkOrderingType = orderingType;
chunkListing.chunks = chunks;