diff options
author | Al Sutton <alsutton@google.com> | 2019-10-09 08:49:49 +0100 |
---|---|---|
committer | Al Sutton <alsutton@google.com> | 2019-10-09 15:29:18 +0100 |
commit | 178a50249e9c555c604e6335f7ee5f4279561545 (patch) | |
tree | 677df2f1d48b10998a3d93bac3b0f03bba48d90d /packages/BackupEncryption/src | |
parent | 4238f749cc734fa34d1cdc9ab0544c8d15e4479c (diff) |
Import EncryptedFullBackupDataProcessor
Bug: 111386661
Test: make RunBackupEncryptionRoboIntegTests
Change-Id: I5b9f828663157df13e55f7ed7c8eceef99fa5899
Diffstat (limited to 'packages/BackupEncryption/src')
2 files changed, 238 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java index 91b292666756..66be25b53a62 100644 --- a/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/StreamUtils.java @@ -18,9 +18,13 @@ package com.android.server.backup.encryption; import java.io.Closeable; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; /** Utility methods for dealing with Streams */ public class StreamUtils { + private static final int MAX_COPY_BUFFER_SIZE = 1024; // 1k copy buffer size. + /** * Close a Closeable and silently ignore any IOExceptions. * @@ -33,4 +37,28 @@ public class StreamUtils { // Silently ignore } } + + /** + * Copy data from an InputStream to an OutputStream upto a given number of bytes. + * + * @param in The source InputStream + * @param out The destination OutputStream + * @param limit The maximum number of bytes to copy + * @throws IOException Thrown if there is a problem performing the copy. + */ + public static void copyStream(InputStream in, OutputStream out, int limit) throws IOException { + int bufferSize = Math.min(MAX_COPY_BUFFER_SIZE, limit); + byte[] buffer = new byte[bufferSize]; + + int copied = 0; + while (copied < limit) { + int maxReadSize = Math.min(bufferSize, limit - copied); + int read = in.read(buffer, 0, maxReadSize); + if (read < 0) { + return; // Reached the stream end before the limit + } + out.write(buffer, 0, read); + copied += read; + } + } } diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java new file mode 100644 index 000000000000..0baec8b0a450 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/EncryptedFullBackupDataProcessor.java @@ -0,0 +1,210 @@ +/* + * 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.checkNotNull; +import static com.android.internal.util.Preconditions.checkState; + +import android.annotation.Nullable; +import android.app.backup.BackupTransport; +import android.content.Context; +import android.util.Slog; + +import com.android.server.backup.encryption.FullBackupDataProcessor; +import com.android.server.backup.encryption.StreamUtils; +import com.android.server.backup.encryption.client.CryptoBackupServer; +import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.security.SecureRandom; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** + * Accepts backup data from a {@link InputStream} and passes it to the encrypted full data backup + * path. + */ +public class EncryptedFullBackupDataProcessor implements FullBackupDataProcessor { + + private static final String TAG = "EncryptedFullBackupDP"; + + private final Context mContext; + private final ExecutorService mExecutorService; + private final CryptoBackupServer mCryptoBackupServer; + private final SecureRandom mSecureRandom; + private final RecoverableKeyStoreSecondaryKey mSecondaryKey; + private final String mPackageName; + + @Nullable private InputStream mInputStream; + @Nullable private PipedOutputStream mOutputStream; + @Nullable private EncryptedFullBackupTask mBackupTask; + @Nullable private Future<Void> mBackupTaskFuture; + @Nullable private FullBackupCallbacks mFullBackupCallbacks; + + public EncryptedFullBackupDataProcessor( + Context context, + ExecutorService executorService, + CryptoBackupServer cryptoBackupServer, + SecureRandom secureRandom, + RecoverableKeyStoreSecondaryKey secondaryKey, + String packageName) { + mContext = checkNotNull(context); + mExecutorService = checkNotNull(executorService); + mCryptoBackupServer = checkNotNull(cryptoBackupServer); + mSecureRandom = checkNotNull(secureRandom); + mSecondaryKey = checkNotNull(secondaryKey); + mPackageName = checkNotNull(packageName); + } + + @Override + public boolean initiate(InputStream inputStream) throws IOException { + checkState(mBackupTask == null, "initiate() twice"); + + this.mInputStream = inputStream; + mOutputStream = new PipedOutputStream(); + + mBackupTask = + EncryptedFullBackupTask.newInstance( + mContext, + mCryptoBackupServer, + mSecureRandom, + mSecondaryKey, + mPackageName, + new PipedInputStream(mOutputStream)); + + return true; + } + + @Override + public void start() { + checkState(mBackupTask != null, "start() before initiate()"); + mBackupTaskFuture = mExecutorService.submit(mBackupTask); + } + + @Override + public int pushData(int numBytes) { + checkState( + mBackupTaskFuture != null && mInputStream != null && mOutputStream != null, + "pushData() before start()"); + + // If the upload has failed then stop without pushing any more bytes. + if (mBackupTaskFuture.isDone()) { + Optional<Exception> exception = getTaskException(); + Slog.e(TAG, "Encrypted upload failed", exception.orElse(null)); + if (exception.isPresent()) { + reportNetworkFailureIfNecessary(exception.get()); + + if (exception.get().getCause() instanceof SizeQuotaExceededException) { + return BackupTransport.TRANSPORT_QUOTA_EXCEEDED; + } + } + + return BackupTransport.TRANSPORT_ERROR; + } + + try { + StreamUtils.copyStream(mInputStream, mOutputStream, numBytes); + } catch (IOException e) { + Slog.e(TAG, "IOException when processing backup", e); + return BackupTransport.TRANSPORT_ERROR; + } + + return BackupTransport.TRANSPORT_OK; + } + + @Override + public void cancel() { + checkState(mBackupTaskFuture != null && mBackupTask != null, "cancel() before start()"); + mBackupTask.cancel(); + closeStreams(); + } + + @Override + public int finish() { + checkState(mBackupTaskFuture != null, "finish() before start()"); + + // getTaskException() waits for the task to finish. We must close the streams first, which + // causes the task to finish, otherwise it will block forever. + closeStreams(); + Optional<Exception> exception = getTaskException(); + + if (exception.isPresent()) { + Slog.e(TAG, "Exception during encrypted full backup", exception.get()); + reportNetworkFailureIfNecessary(exception.get()); + + if (exception.get().getCause() instanceof SizeQuotaExceededException) { + return BackupTransport.TRANSPORT_QUOTA_EXCEEDED; + } + return BackupTransport.TRANSPORT_ERROR; + + } else { + if (mFullBackupCallbacks != null) { + mFullBackupCallbacks.onSuccess(); + } + + return BackupTransport.TRANSPORT_OK; + } + } + + private void closeStreams() { + StreamUtils.closeQuietly(mInputStream); + StreamUtils.closeQuietly(mOutputStream); + } + + @Override + public void handleCheckSizeRejectionZeroBytes() { + cancel(); + } + + @Override + public void handleCheckSizeRejectionQuotaExceeded() { + cancel(); + } + + @Override + public void handleSendBytesQuotaExceeded() { + cancel(); + } + + @Override + public void attachCallbacks(FullBackupCallbacks fullBackupCallbacks) { + this.mFullBackupCallbacks = fullBackupCallbacks; + } + + private void reportNetworkFailureIfNecessary(Exception exception) { + if (!(exception.getCause() instanceof SizeQuotaExceededException) + && mFullBackupCallbacks != null) { + mFullBackupCallbacks.onTransferFailed(); + } + } + + private Optional<Exception> getTaskException() { + if (mBackupTaskFuture != null) { + try { + mBackupTaskFuture.get(); + } catch (InterruptedException | ExecutionException e) { + return Optional.of(e); + } + } + return Optional.empty(); + } +} |