Import EncryptedFull???Task
Bug: 111386661 Test: make RunBackupEncryptionRoboTests Change-Id: I135f561a2590206f7131eb14df3f91211fef3daa
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user