summaryrefslogtreecommitdiff
path: root/packages/BackupEncryption/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/BackupEncryption/src')
-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
6 files changed, 554 insertions, 0 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.");
+ }
+}