diff options
Diffstat (limited to 'packages/BackupEncryption/src')
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."); + } +} |