summaryrefslogtreecommitdiff
path: root/packages/BackupEncryption
diff options
context:
space:
mode:
Diffstat (limited to 'packages/BackupEncryption')
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java109
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java51
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java36
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java197
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java137
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java24
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java234
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java129
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java5
9 files changed, 921 insertions, 1 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java
new file mode 100644
index 000000000000..f3ab2bde086a
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullBackupDataProcessor.java
@@ -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();
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java
new file mode 100644
index 000000000000..e4c40491bc2f
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/FullRestoreDataProcessor.java
@@ -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);
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java
new file mode 100644
index 000000000000..91b292666756
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java
@@ -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
+ }
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java
new file mode 100644
index 000000000000..a938d715a307
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTask.java
@@ -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();
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java
new file mode 100644
index 000000000000..04381af561b2
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTask.java
@@ -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();
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java
new file mode 100644
index 000000000000..515db86b6687
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/SizeQuotaExceededException.java
@@ -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.");
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java
new file mode 100644
index 000000000000..096b2da10c98
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupTaskTest.java
@@ -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();
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java
new file mode 100644
index 000000000000..0affacd114bf
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/EncryptedFullRestoreTaskTest.java
@@ -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()));
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
index 5cff53f817d4..5dfd5ee8ad53 100644
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
@@ -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;