Delete backup encryption code

Previously there was a plan to move the backup encryption logic to the
framework. However this plan was abandoned and encryption logic lives in
GMS now. This code is unused and out of date.

I'm removing this now because it blocks ag/19967045.

Test: m
  atest BackupFrameworksServicesRoboTests
  atest CtsBackupHostTestCases
Bug: 210083465

Change-Id: Ib5dc37898fde5c8ef9ea792931404a1915eda8aa
This commit is contained in:
Sarp Misoglu
2022-09-30 15:40:57 +01:00
parent 3510d130bb
commit 2d8b102ef1
160 changed files with 0 additions and 19249 deletions

View File

@@ -1,41 +0,0 @@
//
// 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_app {
name: "BackupEncryption",
defaults: ["platform_app_defaults"],
srcs: ["src/**/*.java"],
static_libs: ["backup-encryption-protos", "backuplib"],
optimize: { enabled: false },
platform_apis: true,
certificate: "platform",
privileged: true,
}
java_library {
name: "backup-encryption-protos",
proto: { type: "nano" },
srcs: ["proto/**/*.proto"],
}

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (c) 2016 Google Inc.
*
* 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.
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.server.backup.encryption"
android:sharedUserId="android.uid.system" >
<application android:allowBackup="false" >
<!-- This service does not need to be exported because it shares uid with the system server
which is the only client. -->
<service android:name=".BackupEncryptionService"
android:exported="false">
<intent-filter>
<action android:name="android.encryption.BACKUP_ENCRYPTION" />
</intent-filter>
</service>
</application>
</manifest>

View File

@@ -1 +0,0 @@
include /services/backup/OWNERS

View File

@@ -1 +0,0 @@
-keep class com.android.server.backup.encryption

View File

@@ -1,136 +0,0 @@
/*
* Copyright (C) 2018 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
*/
syntax = "proto2";
package android_backup_crypto;
option java_package = "com.android.server.backup.encryption.protos";
option java_outer_classname = "ChunksMetadataProto";
// Cipher type with which the chunks are encrypted. For now we only support AES/GCM/NoPadding, but
// this is for backwards-compatibility in case we need to change the default Cipher in the future.
enum CipherType {
UNKNOWN_CIPHER_TYPE = 0;
// Chunk is prefixed with a 12-byte nonce. The tag length is 16 bytes.
AES_256_GCM = 1;
}
// Checksum type with which the plaintext is verified.
enum ChecksumType {
UNKNOWN_CHECKSUM_TYPE = 0;
SHA_256 = 1;
}
enum ChunkOrderingType {
CHUNK_ORDERING_TYPE_UNSPECIFIED = 0;
// The chunk ordering contains a list of the start position of each chunk in the encrypted file,
// ordered as in the plaintext file. This allows us to recreate the original plaintext file
// during decryption. We use this mode for full backups where the order of the data in the file
// is important.
EXPLICIT_STARTS = 1;
// The chunk ordering does not contain any start positions, and instead each encrypted chunk in
// the backup file is prefixed with its length. This allows us to decrypt each chunk but does
// not give any information about the order. However, we use this mode for key value backups
// where the order does not matter.
INLINE_LENGTHS = 2;
}
// Chunk entry (for local state)
message Chunk {
// SHA-256 MAC of the plaintext of the chunk
optional bytes hash = 1;
// Number of bytes in encrypted chunk
optional int32 length = 2;
}
// List of the chunks in the blob, along with the length of each chunk. From this is it possible to
// extract individual chunks. (i.e., start position is equal to the sum of the lengths of all
// preceding chunks.)
//
// This is local state stored on the device. It is never sent to the backup server. See
// ChunkOrdering for how the device restores the chunks in the correct order.
// Next tag : 6
message ChunkListing {
repeated Chunk chunks = 1;
// Cipher algorithm with which the chunks are encrypted.
optional CipherType cipher_type = 2;
// Defines the type of chunk order used to encode the backup file on the server, so that we can
// consistently use the same type between backups. If unspecified this backup file was created
// before INLINE_LENGTHS was supported, thus assume it is EXPLICIT_STARTS.
optional ChunkOrderingType chunk_ordering_type = 5;
// The document ID returned from Scotty server after uploading the blob associated with this
// listing. This needs to be sent when uploading new diff scripts.
optional string document_id = 3;
// Fingerprint mixer salt used for content defined chunking. This is randomly generated for each
// package during the initial non-incremental backup and reused for incremental backups.
optional bytes fingerprint_mixer_salt = 4;
}
// Ordering information about plaintext and checksum. This is used on restore to reconstruct the
// blob in its correct order. (The chunk order is randomized so as to give the server less
// information about which parts of the backup are changing over time.) This proto is encrypted
// before being uploaded to the server, with a key unknown to the server.
message ChunkOrdering {
// For backups where ChunksMetadata#chunk_ordering_type = EXPLICIT STARTS:
// Ordered start positions of chunks. i.e., the file is the chunk starting at this position,
// followed by the chunk starting at this position, followed by ... etc. You can compute the
// lengths of the chunks by sorting this list then looking at the start position of the next
// chunk after the chunk you care about. This is guaranteed to work as all chunks are
// represented in this list.
//
// For backups where ChunksMetadata#chunk_ordering_type = INLINE_LENGTHS:
// This field is unused. See ChunkOrderingType#INLINE_LENGTHS.
repeated int32 starts = 1 [packed = true];
// Checksum of plaintext content. (i.e., in correct order.)
//
// Each chunk also has a MAC, as generated by GCM, so this is NOT Mac-then-Encrypt, which has
// security implications. This is an additional checksum to verify that once the chunks have
// been reordered, that the file matches the expected plaintext. This prevents the device
// restoring garbage data in case of a mismatch between the ChunkOrdering and the backup blob.
optional bytes checksum = 2;
}
// Additional metadata about a backup blob that needs to be synced to the server. This is used on
// restore to reconstruct the blob in its correct order. (The chunk order is randomized so as to
// give the server less information about which parts of the backup are changing over time.) This
// data structure is only ever uploaded to the server encrypted with a key unknown to the server.
// Next tag : 6
message ChunksMetadata {
// Cipher algorithm with which the chunk listing and chunks are encrypted.
optional CipherType cipher_type = 1;
// Defines the type of chunk order this metadata contains. If unspecified this backup file was
// created before INLINE_LENGTHS was supported, thus assume it is EXPLICIT_STARTS.
optional ChunkOrderingType chunk_ordering_type = 5
[default = CHUNK_ORDERING_TYPE_UNSPECIFIED];
// Encrypted bytes of ChunkOrdering
optional bytes chunk_ordering = 2;
// The type of algorithm used for the checksum of the plaintext. (See ChunkOrdering.) This is
// for forwards compatibility in case we change the algorithm in the future. For now, always
// SHA-256.
optional ChecksumType checksum_type = 3;
// This used to be the plaintext tertiary key. No longer used.
reserved 4;
}

View File

@@ -1,40 +0,0 @@
/*
* 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
*/
syntax = "proto2";
package android_backup_crypto;
option java_package = "com.android.server.backup.encryption.protos";
option java_outer_classname = "KeyValueListingProto";
// An entry of a key-value pair.
message KeyValueEntry {
// Plaintext key of the key-value pair.
optional string key = 1;
// SHA-256 MAC of the plaintext of the chunk containing the pair
optional bytes hash = 2;
}
// Describes the key/value pairs currently in the backup blob, mapping from the
// plaintext key to the hash of the chunk containing the pair.
//
// This is local state stored on the device. It is never sent to the
// backup server. See ChunkOrdering for how the device restores the
// key-value pairs in the correct order.
message KeyValueListing {
repeated KeyValueEntry entries = 1;
}

View File

@@ -1,31 +0,0 @@
/*
* 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
*/
syntax = "proto2";
package android_backup_crypto;
option java_package = "com.android.server.backup.encryption.protos";
option java_outer_classname = "KeyValuePairProto";
// Serialized form of a key-value pair, when it is to be encrypted in a blob.
// The backup blob for a key-value database consists of repeated encrypted
// key-value pairs like this, in a randomized order. See ChunkOrdering for how
// these are then reconstructed during a restore.
message KeyValuePair {
optional string key = 1;
optional bytes value = 2;
}

View File

@@ -1,52 +0,0 @@
syntax = "proto2";
package android_backup_crypto;
option java_package = "com.android.server.backup.encryption.protos";
option java_outer_classname = "WrappedKeyProto";
// Metadata associated with a tertiary key.
message KeyMetadata {
// Type of Cipher algorithm the key is used for.
enum Type {
UNKNOWN = 0;
// No padding. Uses 12-byte nonce. Tag length 16 bytes.
AES_256_GCM = 1;
}
// What kind of Cipher algorithm the key is used for. We assume at the moment
// that this will always be AES_256_GCM and throw if this is not the case.
// Provided here for forwards compatibility in case at some point we need to
// change Cipher algorithm.
optional Type type = 1;
}
// An encrypted tertiary key.
message WrappedKey {
// The Cipher with which the key was encrypted.
enum WrapAlgorithm {
UNKNOWN = 0;
// No padding. Uses 16-byte nonce (see nonce field). Tag length 16 bytes.
// The nonce is 16-bytes as this is wrapped with a key in AndroidKeyStore.
// AndroidKeyStore requires that it generates the IV, and it generates a
// 16-byte IV for you. You CANNOT provide your own IV.
AES_256_GCM = 1;
}
// Cipher algorithm used to wrap the key. We assume at the moment that this
// is always AES_256_GC and throw if this is not the case. Provided here for
// forwards compatibility if at some point we need to change Cipher algorithm.
optional WrapAlgorithm wrap_algorithm = 1;
// The nonce used to initialize the Cipher in AES/256/GCM mode.
optional bytes nonce = 2;
// The encrypted bytes of the key material.
optional bytes key = 3;
// Associated key metadata.
optional KeyMetadata metadata = 4;
// Deprecated field; Do not use
reserved 5;
}

View File

@@ -1,242 +0,0 @@
/*
* 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 static com.android.internal.util.Preconditions.checkState;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.RecoveryController;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.security.KeyStoreException;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* State about encrypted backups that needs to be remembered.
*/
public class CryptoSettings {
private static final String TAG = "CryptoSettings";
private static final String SHARED_PREFERENCES_NAME = "crypto_settings";
private static final String KEY_IS_INITIALIZED = "isInitialized";
private static final String KEY_ACTIVE_SECONDARY_ALIAS = "activeSecondary";
private static final String KEY_NEXT_SECONDARY_ALIAS = "nextSecondary";
private static final String SECONDARY_KEY_LAST_ROTATED_AT = "secondaryKeyLastRotatedAt";
private static final String[] SETTINGS_FOR_BACKUP = {
KEY_IS_INITIALIZED,
KEY_ACTIVE_SECONDARY_ALIAS,
KEY_NEXT_SECONDARY_ALIAS,
SECONDARY_KEY_LAST_ROTATED_AT
};
private static final long DEFAULT_SECONDARY_KEY_ROTATION_PERIOD =
TimeUnit.MILLISECONDS.convert(31, TimeUnit.DAYS);
private static final String KEY_ANCESTRAL_SECONDARY_KEY_VERSION =
"ancestral_secondary_key_version";
private final SharedPreferences mSharedPreferences;
private final Context mContext;
/**
* A new instance.
*
* @param context For looking up the {@link SharedPreferences}, for storing state.
* @return The instance.
*/
public static CryptoSettings getInstance(Context context) {
// We need single process mode because CryptoSettings may be used from several processes
// simultaneously.
SharedPreferences sharedPreferences =
context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
return new CryptoSettings(sharedPreferences, context);
}
/**
* A new instance using {@link SharedPreferences} in the default mode.
*
* <p>This will not work across multiple processes but will work in tests.
*/
@VisibleForTesting
public static CryptoSettings getInstanceForTesting(Context context) {
SharedPreferences sharedPreferences =
context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
return new CryptoSettings(sharedPreferences, context);
}
private CryptoSettings(SharedPreferences sharedPreferences, Context context) {
mSharedPreferences = Objects.requireNonNull(sharedPreferences);
mContext = Objects.requireNonNull(context);
}
/**
* The alias of the current active secondary key. This should be used to retrieve the key from
* AndroidKeyStore.
*/
public Optional<String> getActiveSecondaryKeyAlias() {
return getStringInSharedPrefs(KEY_ACTIVE_SECONDARY_ALIAS);
}
/**
* The alias of the secondary key to which the client is rotating. The rotation is not
* immediate, which is why this setting is needed. Once the next key is created, it can take up
* to 72 hours potentially (or longer if the user has no network) for the next key to be synced
* with the keystore. Only after that has happened does the client attempt to re-wrap all
* tertiary keys and commit the rotation.
*/
public Optional<String> getNextSecondaryKeyAlias() {
return getStringInSharedPrefs(KEY_NEXT_SECONDARY_ALIAS);
}
/**
* If the settings have been initialized.
*/
public boolean getIsInitialized() {
return mSharedPreferences.getBoolean(KEY_IS_INITIALIZED, false);
}
/**
* Sets the alias of the currently active secondary key.
*
* @param activeAlias The alias, as in AndroidKeyStore.
* @throws IllegalArgumentException if the alias is not in the user's keystore.
*/
public void setActiveSecondaryKeyAlias(String activeAlias) throws IllegalArgumentException {
assertIsValidAlias(activeAlias);
mSharedPreferences.edit().putString(KEY_ACTIVE_SECONDARY_ALIAS, activeAlias).apply();
}
/**
* Sets the alias of the secondary key to which the client is rotating.
*
* @param nextAlias The alias, as in AndroidKeyStore.
* @throws KeyStoreException if unable to check whether alias is valid in the keystore.
* @throws IllegalArgumentException if the alias is not in the user's keystore.
*/
public void setNextSecondaryAlias(String nextAlias) throws IllegalArgumentException {
assertIsValidAlias(nextAlias);
mSharedPreferences.edit().putString(KEY_NEXT_SECONDARY_ALIAS, nextAlias).apply();
}
/**
* Unsets the alias of the key to which the client is rotating. This is generally performed once
* a rotation is complete.
*/
public void removeNextSecondaryKeyAlias() {
mSharedPreferences.edit().remove(KEY_NEXT_SECONDARY_ALIAS).apply();
}
/**
* Sets the timestamp of when the secondary key was last rotated.
*
* @param timestamp The timestamp to set.
*/
public void setSecondaryLastRotated(long timestamp) {
mSharedPreferences.edit().putLong(SECONDARY_KEY_LAST_ROTATED_AT, timestamp).apply();
}
/**
* Returns a timestamp of when the secondary key was last rotated.
*
* @return The timestamp.
*/
public Optional<Long> getSecondaryLastRotated() {
if (!mSharedPreferences.contains(SECONDARY_KEY_LAST_ROTATED_AT)) {
return Optional.empty();
}
return Optional.of(mSharedPreferences.getLong(SECONDARY_KEY_LAST_ROTATED_AT, -1));
}
/**
* Sets the settings to have been initialized. (Otherwise loading should try to initialize
* again.)
*/
private void setIsInitialized() {
mSharedPreferences.edit().putBoolean(KEY_IS_INITIALIZED, true).apply();
}
/**
* Initializes with the given key alias.
*
* @param alias The secondary key alias to be set as active.
* @throws IllegalArgumentException if the alias does not reference a valid key.
* @throws IllegalStateException if attempting to initialize an already initialized settings.
*/
public void initializeWithKeyAlias(String alias) throws IllegalArgumentException {
checkState(
!getIsInitialized(), "Attempting to initialize an already initialized settings.");
setActiveSecondaryKeyAlias(alias);
setIsInitialized();
}
/** Returns the secondary key version of the encrypted backup set to restore from (if set). */
public Optional<String> getAncestralSecondaryKeyVersion() {
return Optional.ofNullable(
mSharedPreferences.getString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, null));
}
/** Sets the secondary key version of the encrypted backup set to restore from. */
public void setAncestralSecondaryKeyVersion(String ancestralSecondaryKeyVersion) {
mSharedPreferences
.edit()
.putString(KEY_ANCESTRAL_SECONDARY_KEY_VERSION, ancestralSecondaryKeyVersion)
.apply();
}
/** The number of milliseconds between secondary key rotation */
public long backupSecondaryKeyRotationIntervalMs() {
return DEFAULT_SECONDARY_KEY_ROTATION_PERIOD;
}
/** Deletes all crypto settings related to backup (as opposed to restore). */
public void clearAllSettingsForBackup() {
Editor sharedPrefsEditor = mSharedPreferences.edit();
for (String backupSettingKey : SETTINGS_FOR_BACKUP) {
sharedPrefsEditor.remove(backupSettingKey);
}
sharedPrefsEditor.apply();
Slog.d(TAG, "Cleared crypto settings for backup");
}
/**
* Throws {@link IllegalArgumentException} if the alias does not refer to a key that is in
* the {@link RecoveryController}.
*/
private void assertIsValidAlias(String alias) throws IllegalArgumentException {
try {
if (!RecoveryController.getInstance(mContext).getAliases().contains(alias)) {
throw new IllegalArgumentException(alias + " is not in RecoveryController");
}
} catch (InternalRecoveryServiceException e) {
throw new IllegalArgumentException("Problem accessing recovery service", e);
}
}
private Optional<String> getStringInSharedPrefs(String key) {
return Optional.ofNullable(mSharedPreferences.getString(key, null));
}
}

View File

@@ -1,81 +0,0 @@
/*
* 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.content.Context;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.RecoveryController;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
class EncryptionKeyHelper {
private static SecureRandom sSecureRandom = new SecureRandom();
private final Context mContext;
private final RecoverableKeyStoreSecondaryKeyManager
.RecoverableKeyStoreSecondaryKeyManagerProvider
mSecondaryKeyManagerProvider;
EncryptionKeyHelper(Context context) {
mContext = context;
mSecondaryKeyManagerProvider =
() ->
new RecoverableKeyStoreSecondaryKeyManager(
RecoveryController.getInstance(mContext), sSecureRandom);
}
RecoverableKeyStoreSecondaryKeyManager
.RecoverableKeyStoreSecondaryKeyManagerProvider getKeyManagerProvider() {
return mSecondaryKeyManagerProvider;
}
RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
throws UnrecoverableKeyException, InternalRecoveryServiceException {
String keyAlias = CryptoSettings.getInstance(mContext).getActiveSecondaryKeyAlias().get();
return mSecondaryKeyManagerProvider.get().get(keyAlias).get();
}
SecretKey getTertiaryKey(
String packageName,
RecoverableKeyStoreSecondaryKey secondaryKey)
throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, IOException, NoSuchPaddingException,
InvalidKeyException {
TertiaryKeyManager tertiaryKeyManager =
new TertiaryKeyManager(
mContext,
sSecureRandom,
TertiaryKeyRotationScheduler.getInstance(mContext),
secondaryKey,
packageName);
return tertiaryKeyManager.getKey();
}
}

View File

@@ -1,109 +0,0 @@
/*
* 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();
}
}

View File

@@ -1,51 +0,0 @@
/*
* 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);
}

View File

@@ -1,47 +0,0 @@
/*
* 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;
/** Interface for classes which will provide backup data */
public abstract class FullRestoreDownloader {
/** Enum to provide information on why a download finished */
public enum FinishType {
UNKNOWN_FINISH(0),
// Finish the downloading and successfully write data to Android OS.
FINISHED(1),
// Download failed with any kind of exception.
TRANSFER_FAILURE(2),
// Download failed due to auth failure on the device.
AUTH_FAILURE(3),
// Aborted by Android Framework.
FRAMEWORK_ABORTED(4);
private int mValue;
FinishType(int value) {
mValue = value;
}
}
/** Get the next data chunk from the backing store */
public abstract int readNextChunk(byte[] buffer) throws IOException;
/** Called when we've finished restoring the data */
public abstract void finish(FinishType finishType);
}

View File

@@ -1,143 +0,0 @@
/*
* 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.content.Context;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.KeyWrapUtils;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Map;
public class KeyValueEncrypter {
private static final String TAG = "KeyValueEncrypter";
private final Context mContext;
private final EncryptionKeyHelper mKeyHelper;
public KeyValueEncrypter(Context context) {
mContext = context;
mKeyHelper = new EncryptionKeyHelper(mContext);
}
public void encryptKeyValueData(
String packageName, ParcelFileDescriptor inputFd, OutputStream outputStream)
throws Exception {
EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
EncryptedKvBackupTask backupTask =
backupTaskFactory.newInstance(
mContext,
new SecureRandom(),
new FileBackupServer(outputStream),
CryptoSettings.getInstance(mContext),
mKeyHelper.getKeyManagerProvider(),
inputFd,
packageName);
backupTask.performBackup(/* incremental */ false);
}
public void decryptKeyValueData(String packageName,
InputStream encryptedInputStream, ParcelFileDescriptor outputFd) throws Exception {
RecoverableKeyStoreSecondaryKey secondaryKey = mKeyHelper.getActiveSecondaryKey();
EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
EncryptedKvRestoreTask restoreTask =
restoreTaskFactory.newInstance(
mContext,
mKeyHelper.getKeyManagerProvider(),
new InputStreamFullRestoreDownloader(encryptedInputStream),
secondaryKey.getAlias(),
KeyWrapUtils.wrap(
secondaryKey.getSecretKey(),
mKeyHelper.getTertiaryKey(packageName, secondaryKey)));
restoreTask.getRestoreData(outputFd);
}
// TODO(b/142455725): Extract into a commong class.
private static class FileBackupServer implements CryptoBackupServer {
private static final String EMPTY_DOC_ID = "";
private final OutputStream mOutputStream;
FileBackupServer(OutputStream outputStream) {
mOutputStream = outputStream;
}
@Override
public String uploadIncrementalBackup(
String packageName,
String oldDocId,
byte[] diffScript,
WrappedKeyProto.WrappedKey tertiaryKey) {
throw new UnsupportedOperationException();
}
@Override
public String uploadNonIncrementalBackup(
String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
try {
mOutputStream.write(data);
} catch (IOException e) {
Log.w(TAG, "Failed to write encrypted data to file: ", e);
}
return EMPTY_DOC_ID;
}
@Override
public void setActiveSecondaryKeyAlias(
String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
// Do nothing.
}
}
// TODO(b/142455725): Extract into a commong class.
private static class InputStreamFullRestoreDownloader extends FullRestoreDownloader {
private final InputStream mInputStream;
InputStreamFullRestoreDownloader(InputStream inputStream) {
mInputStream = inputStream;
}
@Override
public int readNextChunk(byte[] buffer) throws IOException {
return mInputStream.read(buffer);
}
@Override
public void finish(FinishType finishType) {
try {
mInputStream.close();
} catch (IOException e) {
Log.w(TAG, "Error while reading restore data");
}
}
}
}

View File

@@ -1,64 +0,0 @@
/*
* 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;
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.
*
* @param closeable The closeable to close
*/
public static void closeQuietly(Closeable closeable) {
try {
closeable.close();
} catch (IOException ioe) {
// 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;
}
}
}

View File

@@ -1,90 +0,0 @@
/*
* Copyright (C) 2018 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.chunk;
import com.android.internal.util.Preconditions;
import java.util.Arrays;
import java.util.Base64;
/**
* Represents the SHA-256 hash of the plaintext of a chunk, which is frequently used as a key.
*
* <p>This class is {@link Comparable} and implements {@link #equals(Object)} and {@link
* #hashCode()}.
*/
public class ChunkHash implements Comparable<ChunkHash> {
/** The length of the hash in bytes. The hash is a SHA-256, so this is 256 bits. */
public static final int HASH_LENGTH_BYTES = 256 / 8;
private static final int UNSIGNED_MASK = 0xFF;
private final byte[] mHash;
/** Constructs a new instance which wraps the given SHA-256 hash bytes. */
public ChunkHash(byte[] hash) {
Preconditions.checkArgument(hash.length == HASH_LENGTH_BYTES, "Hash must have 256 bits");
mHash = hash;
}
public byte[] getHash() {
return mHash;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ChunkHash)) {
return false;
}
ChunkHash chunkHash = (ChunkHash) o;
return Arrays.equals(mHash, chunkHash.mHash);
}
@Override
public int hashCode() {
return Arrays.hashCode(mHash);
}
@Override
public int compareTo(ChunkHash other) {
return lexicographicalCompareUnsignedBytes(getHash(), other.getHash());
}
@Override
public String toString() {
return Base64.getEncoder().encodeToString(mHash);
}
private static int lexicographicalCompareUnsignedBytes(byte[] left, byte[] right) {
int minLength = Math.min(left.length, right.length);
for (int i = 0; i < minLength; i++) {
int result = toInt(left[i]) - toInt(right[i]);
if (result != 0) {
return result;
}
}
return left.length - right.length;
}
private static int toInt(byte value) {
return value & UNSIGNED_MASK;
}
}

View File

@@ -1,94 +0,0 @@
/*
* 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.chunk;
import android.annotation.Nullable;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import java.util.HashMap;
import java.util.Map;
/**
* Chunk listing in a format optimized for quick look up of chunks via their hash keys. This is
* useful when building an incremental backup. After a chunk has been produced, the algorithm can
* quickly look up whether the chunk existed in the previous backup by checking this chunk listing.
* It can then tell the server to use that chunk, through telling it the position and length of the
* chunk in the previous backup's blob.
*/
public class ChunkListingMap {
private final Map<ChunkHash, Entry> mChunksByHash;
/** Construct a map from a {@link ChunksMetadataProto.ChunkListing} protobuf */
public static ChunkListingMap fromProto(ChunksMetadataProto.ChunkListing chunkListingProto) {
Map<ChunkHash, Entry> entries = new HashMap<>();
long start = 0;
for (ChunksMetadataProto.Chunk chunk : chunkListingProto.chunks) {
entries.put(new ChunkHash(chunk.hash), new Entry(start, chunk.length));
start += chunk.length;
}
return new ChunkListingMap(entries);
}
private ChunkListingMap(Map<ChunkHash, Entry> chunksByHash) {
// This is only called from the {@link #fromProto} method, so we don't
// need to take a copy.
this.mChunksByHash = chunksByHash;
}
/** Returns {@code true} if there is a chunk with the given SHA-256 MAC key in the listing. */
public boolean hasChunk(ChunkHash hash) {
return mChunksByHash.containsKey(hash);
}
/**
* Returns the entry for the chunk with the given hash.
*
* @param hash The SHA-256 MAC of the plaintext of the chunk.
* @return The entry, containing position and length of the chunk in the backup blob, or null if
* it does not exist.
*/
@Nullable
public Entry getChunkEntry(ChunkHash hash) {
return mChunksByHash.get(hash);
}
/** Information about a chunk entry in a backup blob - i.e., its position and length. */
public static final class Entry {
private final int mLength;
private final long mStart;
private Entry(long start, int length) {
mLength = length;
mStart = start;
}
/** Returns the length of the chunk in bytes. */
public int getLength() {
return mLength;
}
/** Returns the start position of the chunk in the backup blob, in bytes. */
public long getStart() {
return mStart;
}
}
}

View File

@@ -1,31 +0,0 @@
/*
* Copyright (C) 2018 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.chunk;
import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.EXPLICIT_STARTS;
import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.INLINE_LENGTHS;
import android.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** IntDef corresponding to the ChunkOrderingType enum in the ChunksMetadataProto protobuf. */
@IntDef({CHUNK_ORDERING_TYPE_UNSPECIFIED, EXPLICIT_STARTS, INLINE_LENGTHS})
@Retention(RetentionPolicy.SOURCE)
public @interface ChunkOrderingType {}

View File

@@ -1,68 +0,0 @@
/*
* Copyright (C) 2018 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.chunk;
import java.util.Arrays;
/**
* Holds the bytes of an encrypted {@link ChunksMetadataProto.ChunkOrdering}.
*
* <p>TODO(b/116575321): After all code is ported, remove the factory method and rename
* encryptedChunkOrdering() to getBytes().
*/
public class EncryptedChunkOrdering {
/**
* Constructs a new object holding the given bytes of an encrypted {@link
* ChunksMetadataProto.ChunkOrdering}.
*
* <p>Note that this just holds an ordering which is already encrypted, it does not encrypt the
* ordering.
*/
public static EncryptedChunkOrdering create(byte[] encryptedChunkOrdering) {
return new EncryptedChunkOrdering(encryptedChunkOrdering);
}
private final byte[] mEncryptedChunkOrdering;
/** Get the encrypted chunk ordering */
public byte[] encryptedChunkOrdering() {
return mEncryptedChunkOrdering;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof EncryptedChunkOrdering)) {
return false;
}
EncryptedChunkOrdering encryptedChunkOrdering = (EncryptedChunkOrdering) o;
return Arrays.equals(
mEncryptedChunkOrdering, encryptedChunkOrdering.mEncryptedChunkOrdering);
}
@Override
public int hashCode() {
return Arrays.hashCode(mEncryptedChunkOrdering);
}
private EncryptedChunkOrdering(byte[] encryptedChunkOrdering) {
mEncryptedChunkOrdering = encryptedChunkOrdering;
}
}

View File

@@ -1,232 +0,0 @@
/*
* 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.chunking;
import static com.android.internal.util.Preconditions.checkArgument;
import static com.android.internal.util.Preconditions.checkState;
import android.annotation.Nullable;
import android.util.Slog;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunk.ChunkListingMap;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Writes batches of {@link EncryptedChunk} to a diff script, and generates the associated {@link
* ChunksMetadataProto.ChunkListing} and {@link ChunksMetadataProto.ChunkOrdering}.
*/
public class BackupFileBuilder {
private static final String TAG = "BackupFileBuilder";
private static final int BYTES_PER_KILOBYTE = 1024;
private final BackupWriter mBackupWriter;
private final EncryptedChunkEncoder mEncryptedChunkEncoder;
private final ChunkListingMap mOldChunkListing;
private final ChunksMetadataProto.ChunkListing mNewChunkListing;
private final ChunksMetadataProto.ChunkOrdering mChunkOrdering;
private final List<ChunksMetadataProto.Chunk> mKnownChunks = new ArrayList<>();
private final List<Integer> mKnownStarts = new ArrayList<>();
private final Map<ChunkHash, Long> mChunkStartPositions;
private long mNewChunksSizeBytes;
private boolean mFinished;
/**
* Constructs a new instance which writes raw data to the given {@link OutputStream}, without
* generating a diff.
*
* <p>This class never closes the output stream.
*/
public static BackupFileBuilder createForNonIncremental(OutputStream outputStream) {
return new BackupFileBuilder(
new RawBackupWriter(outputStream), new ChunksMetadataProto.ChunkListing());
}
/**
* Constructs a new instance which writes a diff script to the given {@link OutputStream} using
* a {@link SingleStreamDiffScriptWriter}.
*
* <p>This class never closes the output stream.
*
* @param oldChunkListing against which the diff will be generated.
*/
public static BackupFileBuilder createForIncremental(
OutputStream outputStream, ChunksMetadataProto.ChunkListing oldChunkListing) {
return new BackupFileBuilder(
DiffScriptBackupWriter.newInstance(outputStream), oldChunkListing);
}
private BackupFileBuilder(
BackupWriter backupWriter, ChunksMetadataProto.ChunkListing oldChunkListing) {
this.mBackupWriter = backupWriter;
// TODO(b/77188289): Use InlineLengthsEncryptedChunkEncoder for key-value backups
this.mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
this.mOldChunkListing = ChunkListingMap.fromProto(oldChunkListing);
mNewChunkListing = new ChunksMetadataProto.ChunkListing();
mNewChunkListing.cipherType = ChunksMetadataProto.AES_256_GCM;
mNewChunkListing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
mChunkOrdering = new ChunksMetadataProto.ChunkOrdering();
mChunkStartPositions = new HashMap<>();
}
/**
* Writes the given chunks to the output stream, and adds them to the new chunk listing and
* chunk ordering.
*
* <p>Sorts the chunks in lexicographical order before writing.
*
* @param allChunks The hashes of all the chunks, in the order they appear in the plaintext.
* @param newChunks A map from hash to {@link EncryptedChunk} containing the new chunks not
* present in the previous backup.
*/
public void writeChunks(List<ChunkHash> allChunks, Map<ChunkHash, EncryptedChunk> newChunks)
throws IOException {
checkState(!mFinished, "Cannot write chunks after flushing.");
List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
Collections.sort(sortedChunks);
for (ChunkHash chunkHash : sortedChunks) {
// As we have already included this chunk in the backup file, don't add it again to
// deduplicate identical chunks.
if (!mChunkStartPositions.containsKey(chunkHash)) {
// getBytesWritten() gives us the start of the chunk.
mChunkStartPositions.put(chunkHash, mBackupWriter.getBytesWritten());
writeChunkToFileAndListing(chunkHash, newChunks);
}
}
long totalSizeKb = mBackupWriter.getBytesWritten() / BYTES_PER_KILOBYTE;
long newChunksSizeKb = mNewChunksSizeBytes / BYTES_PER_KILOBYTE;
Slog.d(
TAG,
"Total backup size: "
+ totalSizeKb
+ " kb, new chunks size: "
+ newChunksSizeKb
+ " kb");
for (ChunkHash chunkHash : allChunks) {
mKnownStarts.add(mChunkStartPositions.get(chunkHash).intValue());
}
}
/**
* Returns a new listing for all of the chunks written so far, setting the given fingerprint
* mixer salt (this overrides the {@link ChunksMetadataProto.ChunkListing#fingerprintMixerSalt}
* in the old {@link ChunksMetadataProto.ChunkListing} passed into the
* {@link #BackupFileBuilder).
*/
public ChunksMetadataProto.ChunkListing getNewChunkListing(
@Nullable byte[] fingerprintMixerSalt) {
// TODO: b/141537803 Add check to ensure this is called only once per instance
mNewChunkListing.fingerprintMixerSalt =
fingerprintMixerSalt != null
? Arrays.copyOf(fingerprintMixerSalt, fingerprintMixerSalt.length)
: new byte[0];
mNewChunkListing.chunks = mKnownChunks.toArray(new ChunksMetadataProto.Chunk[0]);
return mNewChunkListing;
}
/** Returns a new ordering for all of the chunks written so far, setting the given checksum. */
public ChunksMetadataProto.ChunkOrdering getNewChunkOrdering(byte[] checksum) {
// TODO: b/141537803 Add check to ensure this is called only once per instance
mChunkOrdering.starts = new int[mKnownStarts.size()];
for (int i = 0; i < mKnownStarts.size(); i++) {
mChunkOrdering.starts[i] = mKnownStarts.get(i).intValue();
}
mChunkOrdering.checksum = Arrays.copyOf(checksum, checksum.length);
return mChunkOrdering;
}
/**
* Finishes the backup file by writing the chunk metadata and metadata position.
*
* <p>Once this is called, calling {@link #writeChunks(List, Map)} will throw {@link
* IllegalStateException}.
*/
public void finish(ChunksMetadataProto.ChunksMetadata metadata) throws IOException {
Objects.requireNonNull(metadata, "Metadata cannot be null");
long startOfMetadata = mBackupWriter.getBytesWritten();
mBackupWriter.writeBytes(ChunksMetadataProto.ChunksMetadata.toByteArray(metadata));
mBackupWriter.writeBytes(toByteArray(startOfMetadata));
mBackupWriter.flush();
mFinished = true;
}
/**
* Checks if the given chunk hash references an existing chunk or a new chunk, and adds this
* chunk to the backup file and new chunk listing.
*/
private void writeChunkToFileAndListing(
ChunkHash chunkHash, Map<ChunkHash, EncryptedChunk> newChunks) throws IOException {
Objects.requireNonNull(chunkHash, "Hash cannot be null");
if (mOldChunkListing.hasChunk(chunkHash)) {
ChunkListingMap.Entry oldChunk = mOldChunkListing.getChunkEntry(chunkHash);
mBackupWriter.writeChunk(oldChunk.getStart(), oldChunk.getLength());
checkArgument(oldChunk.getLength() >= 0, "Chunk must have zero or positive length");
addChunk(chunkHash.getHash(), oldChunk.getLength());
} else if (newChunks.containsKey(chunkHash)) {
EncryptedChunk newChunk = newChunks.get(chunkHash);
mEncryptedChunkEncoder.writeChunkToWriter(mBackupWriter, newChunk);
int length = mEncryptedChunkEncoder.getEncodedLengthOfChunk(newChunk);
mNewChunksSizeBytes += length;
checkArgument(length >= 0, "Chunk must have zero or positive length");
addChunk(chunkHash.getHash(), length);
} else {
throw new IllegalArgumentException(
"Chunk did not exist in old chunks or new chunks: " + chunkHash);
}
}
private void addChunk(byte[] chunkHash, int length) {
ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
chunk.hash = Arrays.copyOf(chunkHash, chunkHash.length);
chunk.length = length;
mKnownChunks.add(chunk);
}
private static byte[] toByteArray(long value) {
// Note that this code needs to stay compatible with GWT, which has known
// bugs when narrowing byte casts of long values occur.
byte[] result = new byte[8];
for (int i = 7; i >= 0; i--) {
result[i] = (byte) (value & 0xffL);
value >>= 8;
}
return result;
}
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import java.io.IOException;
/** Writes backup data either as a diff script or as raw data, determined by the implementation. */
public interface BackupWriter {
/** Writes the given bytes to the output. */
void writeBytes(byte[] bytes) throws IOException;
/**
* Writes an existing chunk from the previous backup to the output.
*
* <p>Note: not all implementations support this method.
*/
void writeChunk(long start, int length) throws IOException;
/** Returns the number of bytes written, included bytes copied from the old file. */
long getBytesWritten();
/**
* Indicates that no more bytes or chunks will be written.
*
* <p>After calling this, you may not call {@link #writeBytes(byte[])} or {@link
* #writeChunk(long, int)}
*/
void flush() throws IOException;
}

View File

@@ -1,80 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.internal.util.Preconditions;
/** Representation of a range of bytes to be downloaded. */
final class ByteRange {
private final long mStart;
private final long mEnd;
/** Creates a range of bytes which includes {@code mStart} and {@code mEnd}. */
ByteRange(long start, long end) {
Preconditions.checkArgument(start >= 0);
Preconditions.checkArgument(end >= start);
mStart = start;
mEnd = end;
}
/** Returns the start of the {@code ByteRange}. The start is included in the range. */
long getStart() {
return mStart;
}
/** Returns the end of the {@code ByteRange}. The end is included in the range. */
long getEnd() {
return mEnd;
}
/** Returns the number of bytes included in the {@code ByteRange}. */
int getLength() {
return (int) (mEnd - mStart + 1);
}
/** Creates a new {@link ByteRange} from {@code mStart} to {@code mEnd + length}. */
ByteRange extend(long length) {
Preconditions.checkArgument(length > 0);
return new ByteRange(mStart, mEnd + length);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ByteRange)) {
return false;
}
ByteRange byteRange = (ByteRange) o;
return (mEnd == byteRange.mEnd && mStart == byteRange.mStart);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (int) (mStart ^ (mStart >>> 32));
result = 31 * result + (int) (mEnd ^ (mEnd >>> 32));
return result;
}
@Override
public String toString() {
return String.format("ByteRange{mStart=%d, mEnd=%d}", mStart, mEnd);
}
}

View File

@@ -1,92 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.server.backup.encryption.chunk.ChunkHash;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
/** Encrypts chunks of a file using AES/GCM. */
public class ChunkEncryptor {
private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_NONCE_LENGTH_BYTES = 12;
private static final int GCM_TAG_LENGTH_BYTES = 16;
private final SecretKey mSecretKey;
private final SecureRandom mSecureRandom;
/**
* A new instance using {@code mSecretKey} to encrypt chunks and {@code mSecureRandom} to
* generate nonces.
*/
public ChunkEncryptor(SecretKey secretKey, SecureRandom secureRandom) {
this.mSecretKey = secretKey;
this.mSecureRandom = secureRandom;
}
/**
* Transforms {@code plaintext} into an {@link EncryptedChunk}.
*
* @param plaintextHash The hash of the plaintext to encrypt, to attach as the key of the chunk.
* @param plaintext Bytes to encrypt.
* @throws InvalidKeyException If the given secret key is not a valid AES key for decryption.
* @throws IllegalBlockSizeException If the input data cannot be encrypted using
* AES/GCM/NoPadding. This should never be the case.
*/
public EncryptedChunk encrypt(ChunkHash plaintextHash, byte[] plaintext)
throws InvalidKeyException, IllegalBlockSizeException {
byte[] nonce = generateNonce();
Cipher cipher;
try {
cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(
Cipher.ENCRYPT_MODE,
mSecretKey,
new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, nonce));
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException e) {
// This can not happen - AES/GCM/NoPadding is supported.
throw new AssertionError(e);
}
byte[] encryptedBytes;
try {
encryptedBytes = cipher.doFinal(plaintext);
} catch (BadPaddingException e) {
// This can not happen - BadPaddingException can only be thrown in decrypt mode.
throw new AssertionError("Impossible: threw BadPaddingException in encrypt mode.");
}
return EncryptedChunk.create(/*key=*/ plaintextHash, nonce, encryptedBytes);
}
private byte[] generateNonce() {
byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
mSecureRandom.nextBytes(nonce);
return nonce;
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.server.backup.encryption.chunk.ChunkHash;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
/** Computes the SHA-256 HMAC of a chunk of bytes. */
public class ChunkHasher {
private static final String MAC_ALGORITHM = "HmacSHA256";
private final SecretKey mSecretKey;
/** Constructs a new hasher which computes the HMAC using the given secret key. */
public ChunkHasher(SecretKey secretKey) {
this.mSecretKey = secretKey;
}
/** Returns the SHA-256 over the given bytes. */
public ChunkHash computeHash(byte[] plaintext) throws InvalidKeyException {
try {
Mac mac = Mac.getInstance(MAC_ALGORITHM);
mac.init(mSecretKey);
return new ChunkHash(mac.doFinal(plaintext));
} catch (NoSuchAlgorithmException e) {
// This can not happen - AES/GCM/NoPadding is available as part of the framework.
throw new AssertionError(e);
}
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
/** Splits an input stream into chunks, which are to be encrypted separately. */
public interface Chunker {
/**
* Splits the input stream into chunks.
*
* @param inputStream The input stream.
* @param chunkConsumer A function that processes each chunk as it is produced.
* @throws IOException If there is a problem reading the input stream.
* @throws GeneralSecurityException if the consumer function throws an error.
*/
void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
throws IOException, GeneralSecurityException;
/** Function that consumes chunks. */
interface ChunkConsumer {
/**
* Invoked for each chunk.
*
* @param chunk Plaintext bytes of chunk.
* @throws GeneralSecurityException if there is an issue encrypting the chunk.
*/
void accept(byte[] chunk) throws GeneralSecurityException;
}
}

View File

@@ -1,87 +0,0 @@
/*
* 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.chunking;
import static com.android.internal.util.Preconditions.checkState;
import android.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/** Writes plaintext chunks to a file, building a digest of the plaintext of the resulting file. */
public class DecryptedChunkFileOutput implements DecryptedChunkOutput {
@VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
private final File mOutputFile;
private final MessageDigest mMessageDigest;
@Nullable private FileOutputStream mFileOutputStream;
private boolean mClosed;
@Nullable private byte[] mDigest;
/**
* Constructs a new instance which writes chunks to the given file and uses the default message
* digest algorithm.
*/
public DecryptedChunkFileOutput(File outputFile) {
mOutputFile = outputFile;
try {
mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(
"Impossible condition: JCE thinks it does not support AES.", e);
}
}
@Override
public DecryptedChunkOutput open() throws IOException {
checkState(mFileOutputStream == null, "Cannot open twice");
mFileOutputStream = new FileOutputStream(mOutputFile);
return this;
}
@Override
public void processChunk(byte[] plaintextBuffer, int length) throws IOException {
checkState(mFileOutputStream != null, "Must open before processing chunks");
mFileOutputStream.write(plaintextBuffer, /*off=*/ 0, length);
mMessageDigest.update(plaintextBuffer, /*offset=*/ 0, length);
}
@Override
public byte[] getDigest() {
checkState(mClosed, "Must close before getting mDigest");
// After the first call to mDigest() the MessageDigest is reset, thus we must store the
// result.
if (mDigest == null) {
mDigest = mMessageDigest.digest();
}
return mDigest;
}
@Override
public void close() throws IOException {
mFileOutputStream.close();
mClosed = true;
}
}

View File

@@ -1,75 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.internal.annotations.VisibleForTesting;
import java.io.IOException;
import java.io.OutputStream;
/** Writes backup data to a diff script, using a {@link SingleStreamDiffScriptWriter}. */
public class DiffScriptBackupWriter implements BackupWriter {
/**
* The maximum size of a chunk in the diff script. The diff script writer {@code mWriter} will
* buffer this many bytes in memory.
*/
private static final int ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES = 1024 * 1024;
private final SingleStreamDiffScriptWriter mWriter;
private long mBytesWritten;
/**
* Constructs a new writer which writes the diff script to the given output stream, using the
* maximum new chunk size {@code ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES}.
*/
public static DiffScriptBackupWriter newInstance(OutputStream outputStream) {
SingleStreamDiffScriptWriter writer =
new SingleStreamDiffScriptWriter(
outputStream, ENCRYPTION_DIFF_SCRIPT_MAX_CHUNK_SIZE_BYTES);
return new DiffScriptBackupWriter(writer);
}
@VisibleForTesting
DiffScriptBackupWriter(SingleStreamDiffScriptWriter writer) {
mWriter = writer;
}
@Override
public void writeBytes(byte[] bytes) throws IOException {
for (byte b : bytes) {
mWriter.writeByte(b);
}
mBytesWritten += bytes.length;
}
@Override
public void writeChunk(long start, int length) throws IOException {
mWriter.writeChunk(start, length);
mBytesWritten += length;
}
@Override
public long getBytesWritten() {
return mBytesWritten;
}
@Override
public void flush() throws IOException {
mWriter.flush();
}
}

View File

@@ -1,36 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import java.io.IOException;
import java.io.OutputStream;
/** Writer that formats a Diff Script and writes it to an output source. */
interface DiffScriptWriter {
/** Adds a new byte to the diff script. */
void writeByte(byte b) throws IOException;
/** Adds a known chunk to the diff script. */
void writeChunk(long chunkStart, int chunkLength) throws IOException;
/** Indicates that no more bytes or chunks will be added to the diff script. */
void flush() throws IOException;
interface Factory {
DiffScriptWriter create(OutputStream outputStream);
}
}

View File

@@ -1,92 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.internal.util.Preconditions;
import com.android.server.backup.encryption.chunk.ChunkHash;
import java.util.Arrays;
import java.util.Objects;
/**
* A chunk of a file encrypted using AES/GCM.
*
* <p>TODO(b/116575321): After all code is ported, remove the factory method and rename
* encryptedBytes(), key() and nonce().
*/
public class EncryptedChunk {
public static final int KEY_LENGTH_BYTES = ChunkHash.HASH_LENGTH_BYTES;
public static final int NONCE_LENGTH_BYTES = 12;
/**
* Constructs a new instance with the given key, nonce, and encrypted bytes.
*
* @param key SHA-256 Hmac of the chunk plaintext.
* @param nonce Nonce with which the bytes of the chunk were encrypted.
* @param encryptedBytes Encrypted bytes of the chunk.
*/
public static EncryptedChunk create(ChunkHash key, byte[] nonce, byte[] encryptedBytes) {
Preconditions.checkArgument(
nonce.length == NONCE_LENGTH_BYTES, "Nonce does not have the correct length.");
return new EncryptedChunk(key, nonce, encryptedBytes);
}
private ChunkHash mKey;
private byte[] mNonce;
private byte[] mEncryptedBytes;
private EncryptedChunk(ChunkHash key, byte[] nonce, byte[] encryptedBytes) {
mKey = key;
mNonce = nonce;
mEncryptedBytes = encryptedBytes;
}
/** The SHA-256 Hmac of the plaintext bytes of the chunk. */
public ChunkHash key() {
return mKey;
}
/** The nonce with which the chunk was encrypted. */
public byte[] nonce() {
return mNonce;
}
/** The encrypted bytes of the chunk. */
public byte[] encryptedBytes() {
return mEncryptedBytes;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof EncryptedChunk)) {
return false;
}
EncryptedChunk encryptedChunkOrdering = (EncryptedChunk) o;
return Arrays.equals(mEncryptedBytes, encryptedChunkOrdering.mEncryptedBytes)
&& Arrays.equals(mNonce, encryptedChunkOrdering.mNonce)
&& mKey.equals(encryptedChunkOrdering.mKey);
}
@Override
public int hashCode() {
return Objects.hash(mKey, Arrays.hashCode(mNonce), Arrays.hashCode(mEncryptedBytes));
}
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.server.backup.encryption.chunk.ChunkOrderingType;
import java.io.IOException;
/** Encodes an {@link EncryptedChunk} as bytes to write to the encrypted backup file. */
public interface EncryptedChunkEncoder {
/**
* Encodes the given chunk and asks the writer to write it.
*
* <p>The chunk will be encoded in the format [nonce]+[encrypted data].
*
* <p>TODO(b/116575321): Choose a more descriptive method name after the code move is done.
*/
void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException;
/**
* Returns the length in bytes that this chunk would be if encoded with {@link
* #writeChunkToWriter}.
*/
int getEncodedLengthOfChunk(EncryptedChunk chunk);
/**
* Returns the {@link ChunkOrderingType} that must be included in the backup file, when using
* this decoder, so that the file may be correctly decoded.
*/
@ChunkOrderingType
int getChunkOrderingType();
}

View File

@@ -1,69 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.server.backup.encryption.chunk.ChunkOrderingType;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import java.io.IOException;
/**
* Encodes an {@link EncryptedChunk} as bytes, prepending the length of the chunk.
*
* <p>This allows us to decode the backup file during restore without any extra information about
* the boundaries of the chunks. The backup file should contain a chunk ordering in mode {@link
* ChunksMetadataProto#INLINE_LENGTHS}.
*
* <p>We use this implementation during key value backup.
*/
public class InlineLengthsEncryptedChunkEncoder implements EncryptedChunkEncoder {
public static final int BYTES_LENGTH = Integer.SIZE / Byte.SIZE;
private final LengthlessEncryptedChunkEncoder mLengthlessEncryptedChunkEncoder =
new LengthlessEncryptedChunkEncoder();
@Override
public void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException {
int length = mLengthlessEncryptedChunkEncoder.getEncodedLengthOfChunk(chunk);
writer.writeBytes(toByteArray(length));
mLengthlessEncryptedChunkEncoder.writeChunkToWriter(writer, chunk);
}
@Override
public int getEncodedLengthOfChunk(EncryptedChunk chunk) {
return BYTES_LENGTH + mLengthlessEncryptedChunkEncoder.getEncodedLengthOfChunk(chunk);
}
@Override
@ChunkOrderingType
public int getChunkOrderingType() {
return ChunksMetadataProto.INLINE_LENGTHS;
}
/**
* Returns a big-endian representation of {@code value} in a 4-element byte array; equivalent to
* {@code ByteBuffer.allocate(4).putInt(value).array()}. For example, the input value {@code
* 0x12131415} would yield the byte array {@code {0x12, 0x13, 0x14, 0x15}}.
*
* <p>Equivalent to guava's Ints.toByteArray.
*/
static byte[] toByteArray(int value) {
return new byte[] {
(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value
};
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import com.android.server.backup.encryption.chunk.ChunkOrderingType;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import java.io.IOException;
/**
* Encodes an {@link EncryptedChunk} as bytes without including any information about the length of
* the chunk.
*
* <p>In order for us to decode the backup file during restore it must include a chunk ordering in
* mode {@link ChunksMetadataProto#EXPLICIT_STARTS}, which contains the boundaries of the chunks in
* the encrypted file. This information allows us to decode the backup file and divide it into
* chunks without including the length of each chunk inline.
*
* <p>We use this implementation during full backup.
*/
public class LengthlessEncryptedChunkEncoder implements EncryptedChunkEncoder {
@Override
public void writeChunkToWriter(BackupWriter writer, EncryptedChunk chunk) throws IOException {
writer.writeBytes(chunk.nonce());
writer.writeBytes(chunk.encryptedBytes());
}
@Override
public int getEncodedLengthOfChunk(EncryptedChunk chunk) {
return chunk.nonce().length + chunk.encryptedBytes().length;
}
@Override
@ChunkOrderingType
public int getChunkOrderingType() {
return ChunksMetadataProto.EXPLICIT_STARTS;
}
}

View File

@@ -1,25 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import java.io.OutputStream;
/** An interface that wraps one {@link OutputStream} with another for filtration purposes. */
public interface OutputStreamWrapper {
/** Wraps a given {@link OutputStream}. */
OutputStream wrap(OutputStream outputStream);
}

View File

@@ -1,173 +0,0 @@
/*
* 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.chunking;
import android.content.Context;
import android.text.TextUtils;
import android.util.AtomicFile;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.google.protobuf.nano.MessageNano;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Objects;
import java.util.Optional;
/**
* Stores a nano proto for each package, persisting the proto to disk.
*
* <p>This is used to store {@link ChunksMetadataProto.ChunkListing}.
*
* @param <T> the type of nano proto to store.
*/
public class ProtoStore<T extends MessageNano> {
private static final String CHUNK_LISTING_FOLDER = "backup_chunk_listings";
private static final String KEY_VALUE_LISTING_FOLDER = "backup_kv_listings";
private static final String TAG = "BupEncProtoStore";
private final File mStoreFolder;
private final Class<T> mClazz;
/** Creates a new instance which stores chunk listings at the default location. */
public static ProtoStore<ChunksMetadataProto.ChunkListing> createChunkListingStore(
Context context) throws IOException {
return new ProtoStore<>(
ChunksMetadataProto.ChunkListing.class,
new File(context.getFilesDir().getAbsoluteFile(), CHUNK_LISTING_FOLDER));
}
/** Creates a new instance which stores key value listings in the default location. */
public static ProtoStore<KeyValueListingProto.KeyValueListing> createKeyValueListingStore(
Context context) throws IOException {
return new ProtoStore<>(
KeyValueListingProto.KeyValueListing.class,
new File(context.getFilesDir().getAbsoluteFile(), KEY_VALUE_LISTING_FOLDER));
}
/**
* Creates a new instance which stores protos in the given folder.
*
* @param storeFolder The location where the serialized form is stored.
*/
@VisibleForTesting
ProtoStore(Class<T> clazz, File storeFolder) throws IOException {
mClazz = Objects.requireNonNull(clazz);
mStoreFolder = ensureDirectoryExistsOrThrow(storeFolder);
}
private static File ensureDirectoryExistsOrThrow(File directory) throws IOException {
if (directory.exists() && !directory.isDirectory()) {
throw new IOException("Store folder already exists, but isn't a directory.");
}
if (!directory.exists() && !directory.mkdir()) {
throw new IOException("Unable to create store folder.");
}
return directory;
}
/**
* Returns the chunk listing for the given package, or {@link Optional#empty()} if no listing
* exists.
*/
public Optional<T> loadProto(String packageName)
throws IOException, IllegalAccessException, InstantiationException,
NoSuchMethodException, InvocationTargetException {
File file = getFileForPackage(packageName);
if (!file.exists()) {
Slog.d(
TAG,
"No chunk listing existed for " + packageName + ", returning empty listing.");
return Optional.empty();
}
AtomicFile protoStore = new AtomicFile(file);
byte[] data = protoStore.readFully();
Constructor<T> constructor = mClazz.getDeclaredConstructor();
T proto = constructor.newInstance();
MessageNano.mergeFrom(proto, data);
return Optional.of(proto);
}
/** Saves a proto to disk, associating it with the given package. */
public void saveProto(String packageName, T proto) throws IOException {
Objects.requireNonNull(proto);
File file = getFileForPackage(packageName);
try (FileOutputStream os = new FileOutputStream(file)) {
os.write(MessageNano.toByteArray(proto));
} catch (IOException e) {
Slog.e(
TAG,
"Exception occurred when saving the listing for "
+ packageName
+ ", deleting saved listing.",
e);
// If a problem occurred when writing the listing then it might be corrupt, so delete
// it.
file.delete();
throw e;
}
}
/** Deletes the proto for the given package, or does nothing if the package has no proto. */
public void deleteProto(String packageName) {
File file = getFileForPackage(packageName);
file.delete();
}
/** Deletes every proto of this type, for all package names. */
public void deleteAllProtos() {
File[] files = mStoreFolder.listFiles();
// We ensure that the storeFolder exists in the constructor, but check just in case it has
// mysteriously disappeared.
if (files == null) {
return;
}
for (File file : files) {
file.delete();
}
}
private File getFileForPackage(String packageName) {
checkPackageName(packageName);
return new File(mStoreFolder, packageName);
}
private static void checkPackageName(String packageName) {
if (TextUtils.isEmpty(packageName) || packageName.contains("/")) {
throw new IllegalArgumentException(
"Package name must not contain '/' or be empty: " + packageName);
}
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import java.io.IOException;
import java.io.OutputStream;
/** Writes data straight to an output stream. */
public class RawBackupWriter implements BackupWriter {
private final OutputStream mOutputStream;
private long mBytesWritten;
/** Constructs a new writer which writes bytes to the given output stream. */
public RawBackupWriter(OutputStream outputStream) {
this.mOutputStream = outputStream;
}
@Override
public void writeBytes(byte[] bytes) throws IOException {
mOutputStream.write(bytes);
mBytesWritten += bytes.length;
}
@Override
public void writeChunk(long start, int length) throws IOException {
throw new UnsupportedOperationException("RawBackupWriter cannot write existing chunks");
}
@Override
public long getBytesWritten() {
return mBytesWritten;
}
@Override
public void flush() throws IOException {
mOutputStream.flush();
}
}

View File

@@ -1,130 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import android.annotation.Nullable;
import com.android.internal.util.Preconditions;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Locale;
/**
* A {@link DiffScriptWriter} that writes an entire diff script to a single {@link OutputStream}.
*/
public class SingleStreamDiffScriptWriter implements DiffScriptWriter {
static final byte LINE_SEPARATOR = 0xA;
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final int mMaxNewByteChunkSize;
private final OutputStream mOutputStream;
private final byte[] mByteBuffer;
private int mBufferSize = 0;
// Each chunk could be written immediately to the output stream. However,
// it is possible that chunks may overlap. We therefore cache the most recent
// reusable chunk and try to merge it with future chunks.
private ByteRange mReusableChunk;
public SingleStreamDiffScriptWriter(OutputStream outputStream, int maxNewByteChunkSize) {
mOutputStream = outputStream;
mMaxNewByteChunkSize = maxNewByteChunkSize;
mByteBuffer = new byte[maxNewByteChunkSize];
}
@Override
public void writeByte(byte b) throws IOException {
if (mReusableChunk != null) {
writeReusableChunk();
}
mByteBuffer[mBufferSize++] = b;
if (mBufferSize == mMaxNewByteChunkSize) {
writeByteBuffer();
}
}
@Override
public void writeChunk(long chunkStart, int chunkLength) throws IOException {
Preconditions.checkArgument(chunkStart >= 0);
Preconditions.checkArgument(chunkLength > 0);
if (mBufferSize != 0) {
writeByteBuffer();
}
if (mReusableChunk != null && mReusableChunk.getEnd() + 1 == chunkStart) {
// The new chunk overlaps the old, so combine them into a single byte range.
mReusableChunk = mReusableChunk.extend(chunkLength);
} else {
writeReusableChunk();
mReusableChunk = new ByteRange(chunkStart, chunkStart + chunkLength - 1);
}
}
@Override
public void flush() throws IOException {
Preconditions.checkState(!(mBufferSize != 0 && mReusableChunk != null));
if (mBufferSize != 0) {
writeByteBuffer();
}
if (mReusableChunk != null) {
writeReusableChunk();
}
mOutputStream.flush();
}
private void writeByteBuffer() throws IOException {
mOutputStream.write(Integer.toString(mBufferSize).getBytes(UTF_8));
mOutputStream.write(LINE_SEPARATOR);
mOutputStream.write(mByteBuffer, 0, mBufferSize);
mOutputStream.write(LINE_SEPARATOR);
mBufferSize = 0;
}
private void writeReusableChunk() throws IOException {
if (mReusableChunk != null) {
mOutputStream.write(
String.format(
Locale.US,
"%d-%d",
mReusableChunk.getStart(),
mReusableChunk.getEnd())
.getBytes(UTF_8));
mOutputStream.write(LINE_SEPARATOR);
mReusableChunk = null;
}
}
/** A factory that creates {@link SingleStreamDiffScriptWriter}s. */
public static class Factory implements DiffScriptWriter.Factory {
private final int mMaxNewByteChunkSize;
private final OutputStreamWrapper mOutputStreamWrapper;
public Factory(int maxNewByteChunkSize, @Nullable OutputStreamWrapper outputStreamWrapper) {
mMaxNewByteChunkSize = maxNewByteChunkSize;
mOutputStreamWrapper = outputStreamWrapper;
}
@Override
public SingleStreamDiffScriptWriter create(OutputStream outputStream) {
if (mOutputStreamWrapper != null) {
outputStream = mOutputStreamWrapper.wrap(outputStream);
}
return new SingleStreamDiffScriptWriter(outputStream, mMaxNewByteChunkSize);
}
}
}

View File

@@ -1,136 +0,0 @@
/*
* Copyright (C) 2018 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.chunking.cdc;
import static com.android.internal.util.Preconditions.checkArgument;
import com.android.server.backup.encryption.chunking.Chunker;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.util.Arrays;
/** Splits a stream of bytes into variable-sized chunks, using content-defined chunking. */
public class ContentDefinedChunker implements Chunker {
private static final int WINDOW_SIZE = 31;
private static final byte DEFAULT_OUT_BYTE = (byte) 0;
private final byte[] mChunkBuffer;
private final RabinFingerprint64 mRabinFingerprint64;
private final FingerprintMixer mFingerprintMixer;
private final BreakpointPredicate mBreakpointPredicate;
private final int mMinChunkSize;
private final int mMaxChunkSize;
/**
* Constructor.
*
* @param minChunkSize The minimum size of a chunk. No chunk will be produced of a size smaller
* than this except possibly at the very end of the stream.
* @param maxChunkSize The maximum size of a chunk. No chunk will be produced of a larger size.
* @param rabinFingerprint64 Calculates fingerprints, with which to determine breakpoints.
* @param breakpointPredicate Given a Rabin fingerprint, returns whether this ought to be a
* breakpoint.
*/
public ContentDefinedChunker(
int minChunkSize,
int maxChunkSize,
RabinFingerprint64 rabinFingerprint64,
FingerprintMixer fingerprintMixer,
BreakpointPredicate breakpointPredicate) {
checkArgument(
minChunkSize >= WINDOW_SIZE,
"Minimum chunk size must be greater than window size.");
checkArgument(
maxChunkSize >= minChunkSize,
"Maximum chunk size cannot be smaller than minimum chunk size.");
mChunkBuffer = new byte[maxChunkSize];
mRabinFingerprint64 = rabinFingerprint64;
mBreakpointPredicate = breakpointPredicate;
mFingerprintMixer = fingerprintMixer;
mMinChunkSize = minChunkSize;
mMaxChunkSize = maxChunkSize;
}
/**
* Breaks the input stream into variable-sized chunks.
*
* @param inputStream The input bytes to break into chunks.
* @param chunkConsumer A function to process each chunk as it's generated.
* @throws IOException Thrown if there is an issue reading from the input stream.
* @throws GeneralSecurityException Thrown if the {@link ChunkConsumer} throws it.
*/
@Override
public void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
throws IOException, GeneralSecurityException {
int chunkLength;
int initialReadLength = mMinChunkSize - WINDOW_SIZE;
// Performance optimization - there is no reason to calculate fingerprints for windows
// ending before the minimum chunk size.
while ((chunkLength =
inputStream.read(mChunkBuffer, /*off=*/ 0, /*len=*/ initialReadLength))
!= -1) {
int b;
long fingerprint = 0L;
while ((b = inputStream.read()) != -1) {
byte inByte = (byte) b;
byte outByte = getCurrentWindowStartByte(chunkLength);
mChunkBuffer[chunkLength++] = inByte;
fingerprint =
mRabinFingerprint64.computeFingerprint64(inByte, outByte, fingerprint);
if (chunkLength >= mMaxChunkSize
|| (chunkLength >= mMinChunkSize
&& mBreakpointPredicate.isBreakpoint(
mFingerprintMixer.mix(fingerprint)))) {
chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
chunkLength = 0;
break;
}
}
if (chunkLength > 0) {
chunkConsumer.accept(Arrays.copyOf(mChunkBuffer, chunkLength));
}
}
}
private byte getCurrentWindowStartByte(int chunkLength) {
if (chunkLength < mMinChunkSize) {
return DEFAULT_OUT_BYTE;
} else {
return mChunkBuffer[chunkLength - WINDOW_SIZE];
}
}
/** Whether the current fingerprint indicates the end of a chunk. */
public interface BreakpointPredicate {
/**
* Returns {@code true} if the fingerprint of the last {@code WINDOW_SIZE} bytes indicates
* the chunk ought to end at this position.
*
* @param fingerprint Fingerprint of the last {@code WINDOW_SIZE} bytes.
* @return Whether this ought to be a chunk breakpoint.
*/
boolean isBreakpoint(long fingerprint);
}
}

View File

@@ -1,95 +0,0 @@
/*
* Copyright (C) 2018 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.chunking.cdc;
import static com.android.internal.util.Preconditions.checkArgument;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import javax.crypto.SecretKey;
/**
* Helper for mixing fingerprint with key material.
*
* <p>We do this as otherwise the Rabin fingerprint leaks information about the plaintext. i.e., if
* two users have the same file, it will be partitioned by Rabin in the same way, allowing us to
* infer that it is the same as another user's file.
*
* <p>By mixing the fingerprint with the user's secret key, the chunking method is different on a
* per key basis. Each application has its own {@link SecretKey}, so we cannot infer that a file is
* the same even across multiple applications owned by the same user, never mind across multiple
* users.
*
* <p>Instead of directly mixing the fingerprint with the user's secret, we first securely and
* deterministically derive a secondary chunking key. As Rabin is not a cryptographically secure
* hash, it might otherwise leak information about the user's secret. This prevents that from
* happening.
*/
public class FingerprintMixer {
public static final int SALT_LENGTH_BYTES = 256 / Byte.SIZE;
private static final String DERIVED_KEY_NAME = "RabinFingerprint64Mixer";
private final long mAddend;
private final long mMultiplicand;
/**
* A new instance from a given secret key and salt. Salt must be the same across incremental
* backups, or a different chunking strategy will be used each time, defeating the dedup.
*
* @param secretKey The application-specific secret.
* @param salt The salt.
* @throws InvalidKeyException If the encoded form of {@code secretKey} is inaccessible.
*/
public FingerprintMixer(SecretKey secretKey, byte[] salt) throws InvalidKeyException {
checkArgument(salt.length == SALT_LENGTH_BYTES, "Requires a 256-bit salt.");
byte[] keyBytes = secretKey.getEncoded();
if (keyBytes == null) {
throw new InvalidKeyException("SecretKey must support encoding for FingerprintMixer.");
}
byte[] derivedKey =
Hkdf.hkdf(keyBytes, salt, DERIVED_KEY_NAME.getBytes(StandardCharsets.UTF_8));
ByteBuffer buffer = ByteBuffer.wrap(derivedKey);
mAddend = buffer.getLong();
// Multiplicand must be odd - otherwise we lose some bits of the Rabin fingerprint when
// mixing
mMultiplicand = buffer.getLong() | 1;
}
/**
* Mixes the fingerprint with the derived key material. This is performed by adding part of the
* derived key and multiplying by another part of the derived key (which is forced to be odd, so
* that the operation is reversible).
*
* @param fingerprint A 64-bit Rabin fingerprint.
* @return The mixed fingerprint.
*/
long mix(long fingerprint) {
return ((fingerprint + mAddend) * mMultiplicand);
}
/** The addend part of the derived key. */
long getAddend() {
return mAddend;
}
/** The multiplicand part of the derived key. */
long getMultiplicand() {
return mMultiplicand;
}
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright (C) 2018 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.chunking.cdc;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* Secure HKDF utils. Allows client to deterministically derive additional key material from a base
* secret. If the derived key material is compromised, this does not in of itself compromise the
* root secret.
*
* <p>TODO(b/116575321): After all code is ported, rename this class to HkdfUtils.
*/
public final class Hkdf {
private static final byte[] CONSTANT_01 = {0x01};
private static final String HmacSHA256 = "HmacSHA256";
private static final String AES = "AES";
/**
* Implements HKDF (RFC 5869) with the SHA-256 hash and a 256-bit output key length.
*
* <p>IMPORTANT: The use or edit of this method requires a security review.
*
* @param mainKey Main key from which to derive sub-keys.
* @param salt A randomly generated 256-bit byte string.
* @param data Arbitrary information that is bound to the derived key (i.e., used in its
* creation).
* @return Raw derived key bytes = HKDF-SHA256(mainKey, salt, data).
* @throws InvalidKeyException If the salt can not be used as a valid key.
*/
static byte[] hkdf(byte[] mainKey, byte[] salt, byte[] data) throws InvalidKeyException {
Objects.requireNonNull(mainKey, "HKDF requires main key to be set.");
Objects.requireNonNull(salt, "HKDF requires a salt.");
Objects.requireNonNull(data, "No data provided to HKDF.");
return hkdfSha256Expand(hkdfSha256Extract(mainKey, salt), data);
}
private Hkdf() {}
/**
* The HKDF (RFC 5869) extraction function, using the SHA-256 hash function. This function is
* used to pre-process the {@code inputKeyMaterial} and mix it with the {@code salt}, producing
* output suitable for use with HKDF expansion function (which produces the actual derived key).
*
* <p>IMPORTANT: The use or edit of this method requires a security review.
*
* @see #hkdfSha256Expand(byte[], byte[])
* @return HMAC-SHA256(salt, inputKeyMaterial) (salt is the "key" for the HMAC)
* @throws InvalidKeyException If the salt can not be used as a valid key.
*/
private static byte[] hkdfSha256Extract(byte[] inputKeyMaterial, byte[] salt)
throws InvalidKeyException {
// Note that the SecretKey encoding format is defined to be RAW, so the encoded form should
// be consistent across implementations.
Mac sha256;
try {
sha256 = Mac.getInstance(HmacSHA256);
} catch (NoSuchAlgorithmException e) {
// This can not happen - HmacSHA256 is supported by the platform.
throw new AssertionError(e);
}
sha256.init(new SecretKeySpec(salt, AES));
return sha256.doFinal(inputKeyMaterial);
}
/**
* Special case of HKDF (RFC 5869) expansion function, using the SHA-256 hash function and
* allowing for a maximum output length of 256 bits.
*
* <p>IMPORTANT: The use or edit of this method requires a security review.
*
* @param pseudoRandomKey Generated by {@link #hkdfSha256Extract(byte[], byte[])}.
* @param info Arbitrary information the derived key should be bound to.
* @return Raw derived key bytes = HMAC-SHA256(pseudoRandomKey, info | 0x01).
* @throws InvalidKeyException If the salt can not be used as a valid key.
*/
private static byte[] hkdfSha256Expand(byte[] pseudoRandomKey, byte[] info)
throws InvalidKeyException {
// Note that RFC 5869 computes number of blocks N = ceil(hash length / output length), but
// here we only deal with a 256 bit hash up to a 256 bit output, yielding N=1.
Mac sha256;
try {
sha256 = Mac.getInstance(HmacSHA256);
} catch (NoSuchAlgorithmException e) {
// This can not happen - HmacSHA256 is supported by the platform.
throw new AssertionError(e);
}
sha256.init(new SecretKeySpec(pseudoRandomKey, AES));
sha256.update(info);
sha256.update(CONSTANT_01);
return sha256.doFinal();
}
}

View File

@@ -1,78 +0,0 @@
/*
* Copyright (C) 2018 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.chunking.cdc;
import static com.android.internal.util.Preconditions.checkArgument;
import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker.BreakpointPredicate;
/**
* Function to determine whether a 64-bit fingerprint ought to be a chunk breakpoint.
*
* <p>This works by checking whether there are at least n leading zeros in the fingerprint. n is
* calculated to on average cause a breakpoint after a given number of trials (provided in the
* constructor). This allows us to choose a number of trials that gives a desired average chunk
* size. This works because the fingerprint is pseudo-randomly distributed.
*/
public class IsChunkBreakpoint implements BreakpointPredicate {
private final int mLeadingZeros;
private final long mBitmask;
/**
* A new instance that causes a breakpoint after a given number of trials on average.
*
* @param averageNumberOfTrialsUntilBreakpoint The number of trials after which on average to
* create a new chunk. If this is not a power of 2, some precision is sacrificed (i.e., on
* average, breaks will actually happen after the nearest power of 2 to the average number
* of trials passed in).
*/
public IsChunkBreakpoint(long averageNumberOfTrialsUntilBreakpoint) {
checkArgument(
averageNumberOfTrialsUntilBreakpoint >= 0,
"Average number of trials must be non-negative");
// Want n leading zeros after t trials.
// P(leading zeros = n) = 1/2^n
// Expected num trials to get n leading zeros = 1/2^-n
// t = 1/2^-n
// n = log2(t)
mLeadingZeros = (int) Math.round(log2(averageNumberOfTrialsUntilBreakpoint));
mBitmask = ~(~0L >>> mLeadingZeros);
}
/**
* Returns {@code true} if {@code fingerprint} indicates that there should be a chunk
* breakpoint.
*/
@Override
public boolean isBreakpoint(long fingerprint) {
return (fingerprint & mBitmask) == 0;
}
/** Returns the number of leading zeros in the fingerprint that causes a breakpoint. */
public int getLeadingZeros() {
return mLeadingZeros;
}
/**
* Calculates log base 2 of x. Not the most efficient possible implementation, but it's simple,
* obviously correct, and is only invoked on object construction.
*/
private static double log2(double x) {
return Math.log(x) / Math.log(2);
}
}

View File

@@ -1,113 +0,0 @@
/*
* Copyright (C) 2018 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.chunking.cdc;
/** Helper to calculate a 64-bit Rabin fingerprint over a 31-byte window. */
public class RabinFingerprint64 {
private static final long DEFAULT_IRREDUCIBLE_POLYNOMIAL_64 = 0x000000000000001BL;
private static final int POLYNOMIAL_DEGREE = 64;
private static final int SLIDING_WINDOW_SIZE_BYTES = 31;
private final long mPoly64;
// Auxiliary tables to speed up the computation of Rabin fingerprints.
private final long[] mTableFP64 = new long[256];
private final long[] mTableOutByte = new long[256];
/**
* Constructs a new instance over the given irreducible 64-degree polynomial. It is up to the
* caller to determine that the polynomial is irreducible. If it is not the fingerprinting will
* not behave as expected.
*
* @param poly64 The polynomial.
*/
public RabinFingerprint64(long poly64) {
mPoly64 = poly64;
}
/** Constructs a new instance using {@code x^64 + x^4 + x + 1} as the irreducible polynomial. */
public RabinFingerprint64() {
this(DEFAULT_IRREDUCIBLE_POLYNOMIAL_64);
computeFingerprintTables64();
computeFingerprintTables64Windowed();
}
/**
* Computes the fingerprint for the new sliding window given the fingerprint of the previous
* sliding window, the byte sliding in, and the byte sliding out.
*
* @param inChar The new char coming into the sliding window.
* @param outChar The left most char sliding out of the window.
* @param fingerPrint Fingerprint for previous window.
* @return New fingerprint for the new sliding window.
*/
public long computeFingerprint64(byte inChar, byte outChar, long fingerPrint) {
return (fingerPrint << 8)
^ (inChar & 0xFF)
^ mTableFP64[(int) (fingerPrint >>> 56)]
^ mTableOutByte[outChar & 0xFF];
}
/** Compute auxiliary tables to speed up the fingerprint computation. */
private void computeFingerprintTables64() {
long[] degreesRes64 = new long[POLYNOMIAL_DEGREE];
degreesRes64[0] = mPoly64;
for (int i = 1; i < POLYNOMIAL_DEGREE; i++) {
if ((degreesRes64[i - 1] & (1L << 63)) == 0) {
degreesRes64[i] = degreesRes64[i - 1] << 1;
} else {
degreesRes64[i] = (degreesRes64[i - 1] << 1) ^ mPoly64;
}
}
for (int i = 0; i < 256; i++) {
int currIndex = i;
for (int j = 0; (currIndex > 0) && (j < 8); j++) {
if ((currIndex & 0x1) == 1) {
mTableFP64[i] ^= degreesRes64[j];
}
currIndex >>>= 1;
}
}
}
/**
* Compute auxiliary table {@code mTableOutByte} to facilitate the computing of fingerprints for
* sliding windows. This table is to take care of the effect on the fingerprint when the
* leftmost byte in the window slides out.
*/
private void computeFingerprintTables64Windowed() {
// Auxiliary array degsRes64[8] defined by: <code>degsRes64[i] = x^(8 *
// SLIDING_WINDOW_SIZE_BYTES + i) mod this.mPoly64.</code>
long[] degsRes64 = new long[8];
degsRes64[0] = mPoly64;
for (int i = 65; i < 8 * (SLIDING_WINDOW_SIZE_BYTES + 1); i++) {
if ((degsRes64[(i - 1) % 8] & (1L << 63)) == 0) {
degsRes64[i % 8] = degsRes64[(i - 1) % 8] << 1;
} else {
degsRes64[i % 8] = (degsRes64[(i - 1) % 8] << 1) ^ mPoly64;
}
}
for (int i = 0; i < 256; i++) {
int currIndex = i;
for (int j = 0; (currIndex > 0) && (j < 8); j++) {
if ((currIndex & 0x1) == 1) {
mTableOutByte[i] ^= degsRes64[j];
}
currIndex >>>= 1;
}
}
}
}

View File

@@ -1,67 +0,0 @@
/*
* 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.client;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.util.Map;
/**
* Contains methods for communicating with the parts of the backup server relevant to encryption.
*/
public interface CryptoBackupServer {
/**
* Uploads an incremental backup to the server.
*
* <p>Handles setting up and tearing down the connection.
*
* @param packageName the package to associate the data with
* @param oldDocId the id of the previous backup doc in Drive
* @param diffScript containing the actual backup data
* @param tertiaryKey the wrapped key used to encrypt this backup
* @return the id of the new backup doc in Drive.
*/
String uploadIncrementalBackup(
String packageName,
String oldDocId,
byte[] diffScript,
WrappedKeyProto.WrappedKey tertiaryKey);
/**
* Uploads non-incremental backup to the server.
*
* <p>Handles setting up and tearing down the connection.
*
* @param packageName the package to associate the data with
* @param data the actual backup data
* @param tertiaryKey the wrapped key used to encrypt this backup
* @return the id of the new backup doc in Drive.
*/
String uploadNonIncrementalBackup(
String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey);
/**
* Sets the alias of the active secondary key. This is the alias used to refer to the key in the
* {@link java.security.KeyStore}. It is also used to key storage for tertiary keys on the
* backup server. Also has to upload all existing tertiary keys, wrapped with the new key.
*
* @param keyAlias The ID of the secondary key.
* @param tertiaryKeys The tertiary keys, wrapped with the new secondary key.
*/
void setActiveSecondaryKeyAlias(
String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys);
}

View File

@@ -1,30 +0,0 @@
/*
* 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.client;
/**
* Error thrown when the user attempts to retrieve a key set from the server, but is asking for keys
* from an inactive secondary.
*
* <p>Although we could just return old keys, there is no good reason to do this. It almost
* certainly indicates a logic error on the client.
*/
public class UnexpectedActiveSecondaryOnServerException extends Exception {
public UnexpectedActiveSecondaryOnServerException(String message) {
super(message);
}
}

View File

@@ -1,132 +0,0 @@
/*
* 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.keys;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
/** Utility functions for wrapping and unwrapping tertiary keys. */
public class KeyWrapUtils {
private static final String AES_GCM_MODE = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH_BYTES = 16;
private static final int BITS_PER_BYTE = 8;
private static final int GCM_TAG_LENGTH_BITS = GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE;
private static final String KEY_ALGORITHM = "AES";
/**
* Uses the secondary key to unwrap the wrapped tertiary key.
*
* @param secondaryKey The secondary key used to wrap the tertiary key.
* @param wrappedKey The wrapped tertiary key.
* @return The unwrapped tertiary key.
* @throws InvalidKeyException if the provided secondary key cannot unwrap the tertiary key.
*/
public static SecretKey unwrap(SecretKey secondaryKey, WrappedKeyProto.WrappedKey wrappedKey)
throws InvalidKeyException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, NoSuchPaddingException {
if (wrappedKey.wrapAlgorithm != WrappedKeyProto.WrappedKey.AES_256_GCM) {
throw new InvalidKeyException(
String.format(
Locale.US,
"Could not unwrap key wrapped with %s algorithm",
wrappedKey.wrapAlgorithm));
}
if (wrappedKey.metadata == null) {
throw new InvalidKeyException("Metadata missing from wrapped tertiary key.");
}
if (wrappedKey.metadata.type != WrappedKeyProto.KeyMetadata.AES_256_GCM) {
throw new InvalidKeyException(
String.format(
Locale.US,
"Wrapped key was unexpected %s algorithm. Only support"
+ " AES/GCM/NoPadding.",
wrappedKey.metadata.type));
}
Cipher cipher = getCipher();
cipher.init(
Cipher.UNWRAP_MODE,
secondaryKey,
new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.nonce));
return (SecretKey) cipher.unwrap(wrappedKey.key, KEY_ALGORITHM, Cipher.SECRET_KEY);
}
/**
* Wraps the tertiary key with the secondary key.
*
* @param secondaryKey The secondary key to use for wrapping.
* @param tertiaryKey The key to wrap.
* @return The wrapped key.
* @throws InvalidKeyException if the key is not good for wrapping.
* @throws IllegalBlockSizeException if there is an issue wrapping.
*/
public static WrappedKeyProto.WrappedKey wrap(SecretKey secondaryKey, SecretKey tertiaryKey)
throws InvalidKeyException, IllegalBlockSizeException, NoSuchAlgorithmException,
NoSuchPaddingException {
Cipher cipher = getCipher();
cipher.init(Cipher.WRAP_MODE, secondaryKey);
WrappedKeyProto.WrappedKey wrappedKey = new WrappedKeyProto.WrappedKey();
wrappedKey.key = cipher.wrap(tertiaryKey);
wrappedKey.nonce = cipher.getIV();
wrappedKey.wrapAlgorithm = WrappedKeyProto.WrappedKey.AES_256_GCM;
wrappedKey.metadata = new WrappedKeyProto.KeyMetadata();
wrappedKey.metadata.type = WrappedKeyProto.KeyMetadata.AES_256_GCM;
return wrappedKey;
}
/**
* Rewraps a tertiary key with a new secondary key.
*
* @param oldSecondaryKey The old secondary key, used to unwrap the tertiary key.
* @param newSecondaryKey The new secondary key, used to rewrap the tertiary key.
* @param tertiaryKey The tertiary key, wrapped by {@code oldSecondaryKey}.
* @return The tertiary key, wrapped by {@code newSecondaryKey}.
* @throws InvalidKeyException if the key is not good for wrapping or unwrapping.
* @throws IllegalBlockSizeException if there is an issue wrapping.
*/
public static WrappedKeyProto.WrappedKey rewrap(
SecretKey oldSecondaryKey,
SecretKey newSecondaryKey,
WrappedKeyProto.WrappedKey tertiaryKey)
throws InvalidKeyException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchPaddingException {
return wrap(newSecondaryKey, unwrap(oldSecondaryKey, tertiaryKey));
}
private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException {
return Cipher.getInstance(AES_GCM_MODE);
}
// Statics only
private KeyWrapUtils() {}
}

View File

@@ -1,117 +0,0 @@
/*
* Copyright (C) 2018 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.keys;
import android.annotation.IntDef;
import android.content.Context;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.RecoveryController;
import android.util.Slog;
import java.util.Objects;
import javax.crypto.SecretKey;
/**
* Wraps a {@link RecoveryController}'s {@link SecretKey}. These are kept in "AndroidKeyStore" (a
* provider for {@link java.security.KeyStore} and {@link javax.crypto.KeyGenerator}. They are also
* synced with the recoverable key store, wrapped by the primary key. This allows them to be
* recovered on a user's subsequent device through providing their lock screen secret.
*/
public class RecoverableKeyStoreSecondaryKey {
private static final String TAG = "RecoverableKeyStoreSecondaryKey";
private final String mAlias;
private final SecretKey mSecretKey;
/**
* A new instance.
*
* @param alias The alias. It is keyed with this in AndroidKeyStore and the recoverable key
* store.
* @param secretKey The key.
*/
public RecoverableKeyStoreSecondaryKey(String alias, SecretKey secretKey) {
mAlias = Objects.requireNonNull(alias);
mSecretKey = Objects.requireNonNull(secretKey);
}
/**
* The ID, as stored in the recoverable {@link java.security.KeyStore}, and as used to identify
* wrapped tertiary keys on the backup server.
*/
public String getAlias() {
return mAlias;
}
/** The secret key, to be used to wrap tertiary keys. */
public SecretKey getSecretKey() {
return mSecretKey;
}
/**
* The status of the key. i.e., whether it's been synced to remote trusted hardware.
*
* @param context The application context.
* @return One of {@link Status#SYNCED}, {@link Status#NOT_SYNCED} or {@link Status#DESTROYED}.
*/
public @Status int getStatus(Context context) {
try {
return getStatusInternal(context);
} catch (InternalRecoveryServiceException e) {
Slog.wtf(TAG, "Internal error getting recovery status", e);
// Return NOT_SYNCED by default, as we do not want the backups to fail or to repeatedly
// attempt to reinitialize.
return Status.NOT_SYNCED;
}
}
private @Status int getStatusInternal(Context context) throws InternalRecoveryServiceException {
int status = RecoveryController.getInstance(context).getRecoveryStatus(mAlias);
switch (status) {
case RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE:
return Status.DESTROYED;
case RecoveryController.RECOVERY_STATUS_SYNCED:
return Status.SYNCED;
case RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS:
return Status.NOT_SYNCED;
default:
// Throw an exception if we encounter a status that doesn't match any of the above.
throw new InternalRecoveryServiceException(
"Unexpected status from getRecoveryStatus: " + status);
}
}
/** Status of a key in the recoverable key store. */
@IntDef({Status.NOT_SYNCED, Status.SYNCED, Status.DESTROYED})
public @interface Status {
/**
* The key has not yet been synced to remote trusted hardware. This may be because the user
* has not yet unlocked their device.
*/
int NOT_SYNCED = 1;
/**
* The key has been synced with remote trusted hardware. It should now be recoverable on
* another device.
*/
int SYNCED = 2;
/** The key has been lost forever. This can occur if the user disables their lock screen. */
int DESTROYED = 3;
}
}

View File

@@ -1,120 +0,0 @@
/*
* Copyright (C) 2018 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.keys;
import android.content.Context;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.security.keystore.recovery.RecoveryController;
import com.android.internal.annotations.VisibleForTesting;
import libcore.util.HexEncoding;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.util.Optional;
import javax.crypto.SecretKey;
/**
* Manages generating, deleting, and retrieving secondary keys through {@link RecoveryController}.
*
* <p>The recoverable key store will be synced remotely via the {@link RecoveryController}, allowing
* recovery of keys on other devices owned by the user.
*/
public class RecoverableKeyStoreSecondaryKeyManager {
private static final String BACKUP_KEY_ALIAS_PREFIX =
"com.android.server.backup/recoverablekeystore/";
private static final int BACKUP_KEY_SUFFIX_LENGTH_BITS = 128;
private static final int BITS_PER_BYTE = 8;
/** A new instance. */
public static RecoverableKeyStoreSecondaryKeyManager getInstance(Context context) {
return new RecoverableKeyStoreSecondaryKeyManager(
RecoveryController.getInstance(context), new SecureRandom());
}
private final RecoveryController mRecoveryController;
private final SecureRandom mSecureRandom;
@VisibleForTesting
public RecoverableKeyStoreSecondaryKeyManager(
RecoveryController recoveryController, SecureRandom secureRandom) {
mRecoveryController = recoveryController;
mSecureRandom = secureRandom;
}
/**
* Generates a new recoverable key using the {@link RecoveryController}.
*
* @throws InternalRecoveryServiceException if an unexpected error occurred generating the key.
* @throws LockScreenRequiredException if the user does not have a lock screen. A lock screen is
* required to generate a recoverable key.
*/
public RecoverableKeyStoreSecondaryKey generate()
throws InternalRecoveryServiceException, LockScreenRequiredException,
UnrecoverableKeyException {
String alias = generateId();
mRecoveryController.generateKey(alias);
SecretKey key = (SecretKey) mRecoveryController.getKey(alias);
if (key == null) {
throw new InternalRecoveryServiceException(
String.format(
"Generated key %s but could not get it back immediately afterwards.",
alias));
}
return new RecoverableKeyStoreSecondaryKey(alias, key);
}
/**
* Removes the secondary key. This means the key will no longer be recoverable.
*
* @param alias The alias of the key.
* @throws InternalRecoveryServiceException if there was a {@link RecoveryController} error.
*/
public void remove(String alias) throws InternalRecoveryServiceException {
mRecoveryController.removeKey(alias);
}
/**
* Returns the {@link RecoverableKeyStoreSecondaryKey} with {@code alias} if it is in the {@link
* RecoveryController}. Otherwise, {@link Optional#empty()}.
*/
public Optional<RecoverableKeyStoreSecondaryKey> get(String alias)
throws InternalRecoveryServiceException, UnrecoverableKeyException {
SecretKey secretKey = (SecretKey) mRecoveryController.getKey(alias);
return Optional.ofNullable(secretKey)
.map(key -> new RecoverableKeyStoreSecondaryKey(alias, key));
}
/**
* Generates a new key alias. This has more entropy than a UUID - it can be considered
* universally unique.
*/
private String generateId() {
byte[] id = new byte[BACKUP_KEY_SUFFIX_LENGTH_BITS / BITS_PER_BYTE];
mSecureRandom.nextBytes(id);
return BACKUP_KEY_ALIAS_PREFIX + HexEncoding.encodeToString(id);
}
/** Constructs a {@link RecoverableKeyStoreSecondaryKeyManager}. */
public interface RecoverableKeyStoreSecondaryKeyManagerProvider {
/** Returns a newly constructed {@link RecoverableKeyStoreSecondaryKeyManager}. */
RecoverableKeyStoreSecondaryKeyManager get();
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.keys;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.Optional;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
/** Fetches the secondary key and uses it to unwrap the tertiary key during restore. */
public class RestoreKeyFetcher {
/**
* Retrieves the secondary key with the given alias and uses it to unwrap the given wrapped
* tertiary key.
*
* @param secondaryKeyManagerProvider Provider which creates {@link
* RecoverableKeyStoreSecondaryKeyManager}
* @param secondaryKeyAlias Alias of the secondary key used to wrap the tertiary key
* @param wrappedTertiaryKey Tertiary key wrapped with the secondary key above
* @return The unwrapped tertiary key
*/
public static SecretKey unwrapTertiaryKey(
RecoverableKeyStoreSecondaryKeyManagerProvider secondaryKeyManagerProvider,
String secondaryKeyAlias,
WrappedKeyProto.WrappedKey wrappedTertiaryKey)
throws KeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchPaddingException {
Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
getSecondaryKey(secondaryKeyManagerProvider, secondaryKeyAlias);
if (!secondaryKey.isPresent()) {
throw new KeyException("No key:" + secondaryKeyAlias);
}
return KeyWrapUtils.unwrap(secondaryKey.get().getSecretKey(), wrappedTertiaryKey);
}
private static Optional<RecoverableKeyStoreSecondaryKey> getSecondaryKey(
RecoverableKeyStoreSecondaryKeyManagerProvider secondaryKeyManagerProvider,
String secondaryKeyAlias)
throws KeyException {
try {
return secondaryKeyManagerProvider.get().get(secondaryKeyAlias);
} catch (InternalRecoveryServiceException | UnrecoverableKeyException e) {
throw new KeyException("Could not retrieve key:" + secondaryKeyAlias, e);
}
}
}

View File

@@ -1,116 +0,0 @@
/*
* 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.keys;
import android.content.Context;
import android.util.Slog;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.tasks.StartSecondaryKeyRotationTask;
import java.io.File;
import java.time.Clock;
import java.util.Optional;
/**
* Helps schedule rotations of secondary keys.
*
* <p>TODO(b/72028016) Replace with a job.
*/
public class SecondaryKeyRotationScheduler {
private static final String TAG = "SecondaryKeyRotationScheduler";
private static final String SENTINEL_FILE_PATH = "force_secondary_key_rotation";
private final Context mContext;
private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
private final CryptoSettings mCryptoSettings;
private final Clock mClock;
public SecondaryKeyRotationScheduler(
Context context,
RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
CryptoSettings cryptoSettings,
Clock clock) {
mContext = context;
mCryptoSettings = cryptoSettings;
mClock = clock;
mSecondaryKeyManager = secondaryKeyManager;
}
/**
* Returns {@code true} if a sentinel file for forcing secondary key rotation is present. This
* is only for testing purposes.
*/
private boolean isForceRotationTestSentinelPresent() {
File file = new File(mContext.getFilesDir(), SENTINEL_FILE_PATH);
if (file.exists()) {
file.delete();
return true;
}
return false;
}
/** Start the key rotation task if it's time to do so */
public void startRotationIfScheduled() {
if (isForceRotationTestSentinelPresent()) {
Slog.i(TAG, "Found force flag for secondary rotation. Starting now.");
startRotation();
return;
}
Optional<Long> maybeLastRotated = mCryptoSettings.getSecondaryLastRotated();
if (!maybeLastRotated.isPresent()) {
Slog.v(TAG, "No previous rotation, scheduling from now.");
scheduleRotationFromNow();
return;
}
long lastRotated = maybeLastRotated.get();
long now = mClock.millis();
if (lastRotated > now) {
Slog.i(TAG, "Last rotation was in the future. Clock must have changed. Rotate now.");
startRotation();
return;
}
long millisSinceLastRotation = now - lastRotated;
long rotationInterval = mCryptoSettings.backupSecondaryKeyRotationIntervalMs();
if (millisSinceLastRotation >= rotationInterval) {
Slog.i(
TAG,
"Last rotation was more than "
+ rotationInterval
+ "ms ("
+ millisSinceLastRotation
+ "ms) in the past. Rotate now.");
startRotation();
}
Slog.v(TAG, "No rotation required, last " + lastRotated + ".");
}
private void startRotation() {
scheduleRotationFromNow();
new StartSecondaryKeyRotationTask(mCryptoSettings, mSecondaryKeyManager).run();
}
private void scheduleRotationFromNow() {
mCryptoSettings.setSecondaryLastRotated(mClock.millis());
}
}

View File

@@ -1,47 +0,0 @@
/*
* Copyright (C) 2018 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.keys;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
/** 256-bit AES key generator. Each app should have its own separate AES key. */
public class TertiaryKeyGenerator {
private static final int KEY_SIZE_BITS = 256;
private static final String KEY_ALGORITHM = "AES";
private final KeyGenerator mKeyGenerator;
/** New instance generating keys using {@code secureRandom}. */
public TertiaryKeyGenerator(SecureRandom secureRandom) {
try {
mKeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
mKeyGenerator.init(KEY_SIZE_BITS, secureRandom);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(
"Impossible condition: JCE thinks it does not support AES.", e);
}
}
/** Generates a new random AES key. */
public SecretKey generate() {
return mKeyGenerator.generateKey();
}
}

View File

@@ -1,136 +0,0 @@
/*
* 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.keys;
import android.annotation.Nullable;
import android.content.Context;
import android.util.Slog;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Optional;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
/**
* Gets the correct tertiary key to use during a backup, rotating it if required.
*
* <p>Calling any method on this class will count a incremental backup against the app, and the key
* will be rotated if required.
*/
public class TertiaryKeyManager {
private static final String TAG = "TertiaryKeyMgr";
private final TertiaryKeyStore mKeyStore;
private final TertiaryKeyGenerator mKeyGenerator;
private final TertiaryKeyRotationScheduler mTertiaryKeyRotationScheduler;
private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
private final String mPackageName;
private boolean mKeyRotated;
@Nullable private SecretKey mTertiaryKey;
public TertiaryKeyManager(
Context context,
SecureRandom secureRandom,
TertiaryKeyRotationScheduler tertiaryKeyRotationScheduler,
RecoverableKeyStoreSecondaryKey secondaryKey,
String packageName) {
mSecondaryKey = secondaryKey;
mPackageName = packageName;
mKeyGenerator = new TertiaryKeyGenerator(secureRandom);
mKeyStore = TertiaryKeyStore.newInstance(context, secondaryKey);
mTertiaryKeyRotationScheduler = tertiaryKeyRotationScheduler;
}
/**
* Returns either the previously used tertiary key, or a new tertiary key if there was no
* previous key or it needed to be rotated.
*/
public SecretKey getKey()
throws InvalidKeyException, IOException, IllegalBlockSizeException,
NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException {
init();
return mTertiaryKey;
}
/** Returns the key given by {@link #getKey()} wrapped by the secondary key. */
public WrappedKeyProto.WrappedKey getWrappedKey()
throws InvalidKeyException, IOException, IllegalBlockSizeException,
NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException {
init();
return KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), mTertiaryKey);
}
/**
* Returns {@code true} if a new tertiary key was generated at the start of this session,
* otherwise {@code false}.
*/
public boolean wasKeyRotated()
throws InvalidKeyException, IllegalBlockSizeException, IOException,
NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException {
init();
return mKeyRotated;
}
private void init()
throws IllegalBlockSizeException, InvalidKeyException, IOException,
NoSuchAlgorithmException, NoSuchPaddingException,
InvalidAlgorithmParameterException {
if (mTertiaryKey != null) {
return;
}
Optional<SecretKey> key = getExistingKeyIfNotRotated();
if (!key.isPresent()) {
Slog.d(TAG, "Generating new tertiary key for " + mPackageName);
key = Optional.of(mKeyGenerator.generate());
mKeyRotated = true;
mTertiaryKeyRotationScheduler.recordKeyRotation(mPackageName);
mKeyStore.save(mPackageName, key.get());
}
mTertiaryKey = key.get();
mTertiaryKeyRotationScheduler.recordBackup(mPackageName);
}
private Optional<SecretKey> getExistingKeyIfNotRotated()
throws InvalidKeyException, IOException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchPaddingException {
if (mTertiaryKeyRotationScheduler.isKeyRotationDue(mPackageName)) {
Slog.i(TAG, "Tertiary key rotation was required for " + mPackageName);
return Optional.empty();
} else {
Slog.i(TAG, "Tertiary key rotation was not required");
return mKeyStore.load(mPackageName);
}
}
}

View File

@@ -1,104 +0,0 @@
/*
* 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.keys;
import android.content.Context;
import com.android.internal.annotations.VisibleForTesting;
/**
* Schedules tertiary key rotations in a staggered fashion.
*
* <p>Apps are due a key rotation after a certain number of backups. Rotations are then staggerered
* over a period of time, through restricting the number of rotations allowed in a 24-hour window.
* This will causes the apps to enter a staggered cycle of regular rotations.
*
* <p>Note: the methods in this class are not optimized to be super fast. They make blocking IO to
* ensure that scheduler information is committed to disk, so that it is available after the user
* turns their device off and on. This ought to be fine as
*
* <ul>
* <li>It will be invoked before a backup, so should never be invoked on the UI thread
* <li>It will be invoked before a backup, so the vast amount of time is spent on the backup, not
* writing tiny amounts of data to disk.
* </ul>
*/
public class TertiaryKeyRotationScheduler {
/** Default number of key rotations allowed within 24 hours. */
private static final int KEY_ROTATION_LIMIT = 2;
/** A new instance, using {@code context} to determine where to store state. */
public static TertiaryKeyRotationScheduler getInstance(Context context) {
TertiaryKeyRotationWindowedCount windowedCount =
TertiaryKeyRotationWindowedCount.getInstance(context);
TertiaryKeyRotationTracker tracker = TertiaryKeyRotationTracker.getInstance(context);
return new TertiaryKeyRotationScheduler(tracker, windowedCount, KEY_ROTATION_LIMIT);
}
private final TertiaryKeyRotationTracker mTracker;
private final TertiaryKeyRotationWindowedCount mWindowedCount;
private final int mMaximumRotationsPerWindow;
/**
* A new instance.
*
* @param tracker Tracks how many times each application has backed up.
* @param windowedCount Tracks how many rotations have happened in the last 24 hours.
* @param maximumRotationsPerWindow The maximum number of key rotations allowed per 24 hours.
*/
@VisibleForTesting
TertiaryKeyRotationScheduler(
TertiaryKeyRotationTracker tracker,
TertiaryKeyRotationWindowedCount windowedCount,
int maximumRotationsPerWindow) {
mTracker = tracker;
mWindowedCount = windowedCount;
mMaximumRotationsPerWindow = maximumRotationsPerWindow;
}
/**
* Returns {@code true} if the app with {@code packageName} is due having its key rotated.
*
* <p>This ought to be queried before backing up an app, to determine whether to do an
* incremental backup or a full backup. (A full backup forces key rotation.)
*/
public boolean isKeyRotationDue(String packageName) {
if (mWindowedCount.getCount() >= mMaximumRotationsPerWindow) {
return false;
}
return mTracker.isKeyRotationDue(packageName);
}
/**
* Records that a backup happened for the app with the given {@code packageName}.
*
* <p>Each backup brings the app closer to the point at which a key rotation is due.
*/
public void recordBackup(String packageName) {
mTracker.recordBackup(packageName);
}
/**
* Records a key rotation happened for the app with the given {@code packageName}.
*
* <p>This resets the countdown until the next key rotation is due.
*/
public void recordKeyRotation(String packageName) {
mTracker.resetCountdown(packageName);
mWindowedCount.record();
}
}

View File

@@ -1,127 +0,0 @@
/*
* 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.keys;
import static com.android.internal.util.Preconditions.checkArgument;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Locale;
/**
* Tracks when a tertiary key rotation is due.
*
* <p>After a certain number of incremental backups, the device schedules a full backup, which will
* generate a new encryption key, effecting a key rotation. We should do this on a regular basis so
* that if a key does become compromised it has limited value to the attacker.
*
* <p>No additional synchronization of this class is provided. Only one instance should be used at
* any time. This should be fine as there should be no parallelism in backups.
*/
public class TertiaryKeyRotationTracker {
private static final int MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION = 31;
private static final String SHARED_PREFERENCES_NAME = "tertiary_key_rotation_tracker";
private static final String TAG = "TertiaryKeyRotationTracker";
private static final boolean DEBUG = false;
/**
* A new instance, using {@code context} to commit data to disk via {@link SharedPreferences}.
*/
public static TertiaryKeyRotationTracker getInstance(Context context) {
return new TertiaryKeyRotationTracker(
context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE),
MAX_BACKUPS_UNTIL_TERTIARY_KEY_ROTATION);
}
private final SharedPreferences mSharedPreferences;
private final int mMaxBackupsTillRotation;
/**
* New instance, storing data in {@code sharedPreferences} and initializing backup countdown to
* {@code maxBackupsTillRotation}.
*/
@VisibleForTesting
TertiaryKeyRotationTracker(SharedPreferences sharedPreferences, int maxBackupsTillRotation) {
checkArgument(
maxBackupsTillRotation >= 0,
String.format(
Locale.US,
"maxBackupsTillRotation should be non-negative but was %d",
maxBackupsTillRotation));
mSharedPreferences = sharedPreferences;
mMaxBackupsTillRotation = maxBackupsTillRotation;
}
/**
* Returns {@code true} if the given app is due having its key rotated.
*
* @param packageName The package name of the app.
*/
public boolean isKeyRotationDue(String packageName) {
return getBackupsSinceRotation(packageName) >= mMaxBackupsTillRotation;
}
/**
* Records that an incremental backup has occurred. Each incremental backup brings the app
* closer to the time when its key should be rotated.
*
* @param packageName The package name of the app for which the backup occurred.
*/
public void recordBackup(String packageName) {
int backupsSinceRotation = getBackupsSinceRotation(packageName) + 1;
mSharedPreferences.edit().putInt(packageName, backupsSinceRotation).apply();
if (DEBUG) {
Slog.d(
TAG,
String.format(
Locale.US,
"Incremental backup for %s. %d backups until key rotation.",
packageName,
Math.max(
0,
mMaxBackupsTillRotation
- backupsSinceRotation)));
}
}
/**
* Resets the rotation delay for the given app. Should be invoked after a key rotation.
*
* @param packageName Package name of the app whose key has rotated.
*/
public void resetCountdown(String packageName) {
mSharedPreferences.edit().putInt(packageName, 0).apply();
}
/** Marks all enrolled packages for key rotation. */
public void markAllForRotation() {
SharedPreferences.Editor editor = mSharedPreferences.edit();
for (String packageName : mSharedPreferences.getAll().keySet()) {
editor.putInt(packageName, mMaxBackupsTillRotation);
}
editor.apply();
}
private int getBackupsSinceRotation(String packageName) {
return mSharedPreferences.getInt(packageName, 0);
}
}

View File

@@ -1,132 +0,0 @@
/*
* 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.keys;
import android.content.Context;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* Tracks (and commits to disk) how many key rotations have happened in the last 24 hours. This
* allows us to limit (and therefore stagger) the number of key rotations in a given period of time.
*
* <p>Note to engineers thinking of replacing the below with fancier algorithms and data structures:
* we expect the total size of this count at any time to be below however many rotations we allow in
* the window, which is going to be in single digits. Any changes that mean we write to disk more
* frequently, that the code is no longer resistant to clock changes, or that the code is more
* difficult to understand are almost certainly not worthwhile.
*/
public class TertiaryKeyRotationWindowedCount {
private static final String TAG = "TertiaryKeyRotCount";
private static final int WINDOW_IN_HOURS = 24;
private static final String LOG_FILE_NAME = "tertiary_key_rotation_windowed_count";
private final Clock mClock;
private final File mFile;
private ArrayList<Long> mEvents;
/** Returns a new instance, persisting state to the files dir of {@code context}. */
public static TertiaryKeyRotationWindowedCount getInstance(Context context) {
File logFile = new File(context.getFilesDir(), LOG_FILE_NAME);
return new TertiaryKeyRotationWindowedCount(logFile, Clock.systemDefaultZone());
}
/** A new instance, committing state to {@code file}, and reading time from {@code clock}. */
@VisibleForTesting
TertiaryKeyRotationWindowedCount(File file, Clock clock) {
mFile = file;
mClock = clock;
mEvents = new ArrayList<>();
try {
loadFromFile();
} catch (IOException e) {
Slog.e(TAG, "Error reading " + LOG_FILE_NAME, e);
}
}
/** Records a key rotation at the current time. */
public void record() {
mEvents.add(mClock.millis());
compact();
try {
saveToFile();
} catch (IOException e) {
Slog.e(TAG, "Error saving " + LOG_FILE_NAME, e);
}
}
/** Returns the number of key rotation that have been recorded in the window. */
public int getCount() {
compact();
return mEvents.size();
}
private void compact() {
long minimumTimestamp = getMinimumTimestamp();
long now = mClock.millis();
ArrayList<Long> compacted = new ArrayList<>();
for (long event : mEvents) {
if (event >= minimumTimestamp && event <= now) {
compacted.add(event);
}
}
mEvents = compacted;
}
private long getMinimumTimestamp() {
return mClock.millis() - TimeUnit.HOURS.toMillis(WINDOW_IN_HOURS) + 1;
}
private void loadFromFile() throws IOException {
if (!mFile.exists()) {
return;
}
try (FileInputStream fis = new FileInputStream(mFile);
DataInputStream dis = new DataInputStream(fis)) {
while (true) {
mEvents.add(dis.readLong());
}
} catch (EOFException eof) {
// expected
}
}
private void saveToFile() throws IOException {
// File size is maximum number of key rotations in window multiplied by 8 bytes, which is
// why
// we just overwrite it each time. We expect it will always be less than 100 bytes in size.
try (FileOutputStream fos = new FileOutputStream(mFile);
DataOutputStream dos = new DataOutputStream(fos)) {
for (long event : mEvents) {
dos.writeLong(event);
}
}
}
}

View File

@@ -1,202 +0,0 @@
/*
* 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.keys;
import static com.android.internal.util.Preconditions.checkArgument;
import android.content.Context;
import android.util.ArrayMap;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import com.android.server.backup.encryption.storage.BackupEncryptionDb;
import com.android.server.backup.encryption.storage.TertiaryKey;
import com.android.server.backup.encryption.storage.TertiaryKeysTable;
import com.google.protobuf.nano.CodedOutputByteBufferNano;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Optional;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
/**
* Stores backup package keys. Each application package has its own {@link SecretKey}, which is used
* to encrypt the backup data. These keys are then wrapped by a master backup key, and stored in
* their wrapped form on disk and on the backup server.
*
* <p>For now this code only implements writing to disk. Once the backup server is ready, it will be
* extended to sync the keys there, also.
*/
public class TertiaryKeyStore {
private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
private final BackupEncryptionDb mDatabase;
/**
* Creates an instance, using {@code secondaryKey} to wrap tertiary keys, and storing them in
* the database.
*/
public static TertiaryKeyStore newInstance(
Context context, RecoverableKeyStoreSecondaryKey secondaryKey) {
return new TertiaryKeyStore(secondaryKey, BackupEncryptionDb.newInstance(context));
}
private TertiaryKeyStore(
RecoverableKeyStoreSecondaryKey secondaryKey, BackupEncryptionDb database) {
mSecondaryKey = secondaryKey;
mDatabase = database;
}
/**
* Saves the given key.
*
* @param applicationName The package name of the application for which this key will be used to
* encrypt data. e.g., "com.example.app".
* @param key The key.
* @throws InvalidKeyException if the backup key is not capable of wrapping.
* @throws IOException if there is an issue writing to the database.
*/
public void save(String applicationName, SecretKey key)
throws IOException, InvalidKeyException, IllegalBlockSizeException,
NoSuchPaddingException, NoSuchAlgorithmException {
checkApplicationName(applicationName);
byte[] keyBytes = getEncodedKey(KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), key));
long pk;
try {
pk =
mDatabase
.getTertiaryKeysTable()
.addKey(
new TertiaryKey(
mSecondaryKey.getAlias(), applicationName, keyBytes));
} finally {
mDatabase.close();
}
if (pk == -1) {
throw new IOException("Failed to commit to db");
}
}
/**
* Tries to load a key for the given application.
*
* @param applicationName The package name of the application, e.g. "com.example.app".
* @return The key if it is exists, {@link Optional#empty()} ()} otherwise.
* @throws InvalidKeyException if the backup key is not good for unwrapping.
* @throws IOException if there is a problem loading the key from the database.
*/
public Optional<SecretKey> load(String applicationName)
throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchPaddingException {
checkApplicationName(applicationName);
Optional<TertiaryKey> keyFromDb;
try {
keyFromDb =
mDatabase
.getTertiaryKeysTable()
.getKey(mSecondaryKey.getAlias(), applicationName);
} finally {
mDatabase.close();
}
if (!keyFromDb.isPresent()) {
return Optional.empty();
}
WrappedKeyProto.WrappedKey wrappedKey =
WrappedKeyProto.WrappedKey.parseFrom(keyFromDb.get().getWrappedKeyBytes());
return Optional.of(KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
}
/**
* Loads keys for all applications.
*
* @return All of the keys in a map keyed by package name.
* @throws IOException if there is an issue loading from the database.
* @throws InvalidKeyException if the backup key is not an appropriate key for unwrapping.
*/
public Map<String, SecretKey> getAll()
throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchPaddingException {
Map<String, TertiaryKey> tertiaries;
try {
tertiaries = mDatabase.getTertiaryKeysTable().getAllKeys(mSecondaryKey.getAlias());
} finally {
mDatabase.close();
}
Map<String, SecretKey> unwrappedKeys = new ArrayMap<>();
for (String applicationName : tertiaries.keySet()) {
WrappedKeyProto.WrappedKey wrappedKey =
WrappedKeyProto.WrappedKey.parseFrom(
tertiaries.get(applicationName).getWrappedKeyBytes());
unwrappedKeys.put(
applicationName, KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
}
return unwrappedKeys;
}
/**
* Adds all wrapped keys to the database.
*
* @throws IOException if an error occurred adding a wrapped key.
*/
public void putAll(Map<String, WrappedKeyProto.WrappedKey> wrappedKeysByApplicationName)
throws IOException {
TertiaryKeysTable tertiaryKeysTable = mDatabase.getTertiaryKeysTable();
try {
for (String applicationName : wrappedKeysByApplicationName.keySet()) {
byte[] keyBytes = getEncodedKey(wrappedKeysByApplicationName.get(applicationName));
long primaryKey =
tertiaryKeysTable.addKey(
new TertiaryKey(
mSecondaryKey.getAlias(), applicationName, keyBytes));
if (primaryKey == -1) {
throw new IOException("Failed to commit to db");
}
}
} finally {
mDatabase.close();
}
}
private static void checkApplicationName(String applicationName) {
checkArgument(!applicationName.isEmpty(), "applicationName must not be empty string.");
checkArgument(!applicationName.contains("/"), "applicationName must not contain slash.");
}
private byte[] getEncodedKey(WrappedKeyProto.WrappedKey key) throws IOException {
byte[] buffer = new byte[key.getSerializedSize()];
CodedOutputByteBufferNano out = CodedOutputByteBufferNano.newInstance(buffer);
key.writeTo(out);
return buffer;
}
}

View File

@@ -1,111 +0,0 @@
/*
* 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.kv;
import static com.android.internal.util.Preconditions.checkState;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.ChunkHasher;
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Builds a key value backup set from plaintext chunks. Computes a digest over the sorted SHA-256
* hashes of the chunks.
*/
public class DecryptedChunkKvOutput implements DecryptedChunkOutput {
@VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
private final ChunkHasher mChunkHasher;
private final List<KeyValuePairProto.KeyValuePair> mUnsortedPairs = new ArrayList<>();
private final List<ChunkHash> mUnsortedHashes = new ArrayList<>();
private boolean mClosed;
/** Constructs a new instance which computers the digest using the given hasher. */
public DecryptedChunkKvOutput(ChunkHasher chunkHasher) {
mChunkHasher = chunkHasher;
}
@Override
public DecryptedChunkOutput open() {
// As we don't have any resources there is nothing to open.
return this;
}
@Override
public void processChunk(byte[] plaintextBuffer, int length)
throws IOException, InvalidKeyException {
checkState(!mClosed, "Cannot process chunk after close()");
KeyValuePairProto.KeyValuePair kvPair = new KeyValuePairProto.KeyValuePair();
KeyValuePairProto.KeyValuePair.mergeFrom(kvPair, plaintextBuffer, 0, length);
mUnsortedPairs.add(kvPair);
// TODO(b/71492289): Update ChunkHasher to accept offset and length so we don't have to copy
// the buffer into a smaller array.
mUnsortedHashes.add(mChunkHasher.computeHash(Arrays.copyOf(plaintextBuffer, length)));
}
@Override
public void close() {
// As we don't have any resources there is nothing to close.
mClosed = true;
}
@Override
public byte[] getDigest() throws NoSuchAlgorithmException {
checkState(mClosed, "Must close() before getDigest()");
MessageDigest digest = getMessageDigest();
Collections.sort(mUnsortedHashes);
for (ChunkHash hash : mUnsortedHashes) {
digest.update(hash.getHash());
}
return digest.digest();
}
private static MessageDigest getMessageDigest() throws NoSuchAlgorithmException {
return MessageDigest.getInstance(DIGEST_ALGORITHM);
}
/**
* Returns the key value pairs from the backup, sorted lexicographically by key.
*
* <p>You must call {@link #close} first.
*/
public List<KeyValuePairProto.KeyValuePair> getPairs() {
checkState(mClosed, "Must close() before getPairs()");
Collections.sort(
mUnsortedPairs,
new Comparator<KeyValuePairProto.KeyValuePair>() {
@Override
public int compare(
KeyValuePairProto.KeyValuePair o1, KeyValuePairProto.KeyValuePair o2) {
return o1.key.compareTo(o2.key);
}
});
return mUnsortedPairs;
}
}

View File

@@ -1,77 +0,0 @@
/*
* 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.kv;
import static com.android.internal.util.Preconditions.checkArgument;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
/**
* Builds a {@link KeyValueListingProto.KeyValueListing}, which is a nano proto and so has no
* builder.
*/
public class KeyValueListingBuilder {
private final List<KeyValueListingProto.KeyValueEntry> mEntries = new ArrayList<>();
/** Adds a new pair entry to the listing. */
public KeyValueListingBuilder addPair(String key, ChunkHash hash) {
checkArgument(key.length() != 0, "Key must have non-zero length");
Objects.requireNonNull(hash, "Hash must not be null");
KeyValueListingProto.KeyValueEntry entry = new KeyValueListingProto.KeyValueEntry();
entry.key = key;
entry.hash = hash.getHash();
mEntries.add(entry);
return this;
}
/** Adds all pairs contained in a map, where the map is from key to hash. */
public KeyValueListingBuilder addAll(Map<String, ChunkHash> map) {
for (Entry<String, ChunkHash> entry : map.entrySet()) {
addPair(entry.getKey(), entry.getValue());
}
return this;
}
/** Returns a new listing containing all the pairs added so far. */
public KeyValueListingProto.KeyValueListing build() {
if (mEntries.size() == 0) {
return emptyListing();
}
KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
listing.entries = new KeyValueListingProto.KeyValueEntry[mEntries.size()];
mEntries.toArray(listing.entries);
return listing;
}
/** Returns a new listing which does not contain any pairs. */
public static KeyValueListingProto.KeyValueListing emptyListing() {
KeyValueListingProto.KeyValueListing listing = new KeyValueListingProto.KeyValueListing();
listing.entries = KeyValueListingProto.KeyValueEntry.emptyArray();
return listing;
}
}

View File

@@ -1,59 +0,0 @@
/*
* Copyright (C) 2018 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.storage;
import android.content.Context;
/**
* Backup encryption SQLite database. All instances are threadsafe.
*
* <p>The database is automatically opened when accessing one of the tables. After the caller is
* done they must call {@link #close()}.
*/
public class BackupEncryptionDb {
private final BackupEncryptionDbHelper mHelper;
/** A new instance, using the storage defined by {@code context}. */
public static BackupEncryptionDb newInstance(Context context) {
BackupEncryptionDbHelper helper = new BackupEncryptionDbHelper(context);
helper.setWriteAheadLoggingEnabled(true);
return new BackupEncryptionDb(helper);
}
private BackupEncryptionDb(BackupEncryptionDbHelper helper) {
mHelper = helper;
}
public TertiaryKeysTable getTertiaryKeysTable() {
return new TertiaryKeysTable(mHelper);
}
/** Deletes the database. */
public void clear() throws EncryptionDbException {
mHelper.resetDatabase();
}
/**
* Closes the database if it is open.
*
* <p>After calling this, the caller may access one of the tables again which will automatically
* reopen the database.
*/
public void close() {
mHelper.close();
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2018 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.storage;
import android.provider.BaseColumns;
/** Contract for the backup encryption database. Describes tables present. */
class BackupEncryptionDbContract {
/**
* Table containing tertiary keys belonging to the user. Tertiary keys are wrapped by a
* secondary key, which never leaves {@code AndroidKeyStore} (a provider for {@link
* java.security.KeyStore}). Each application has a tertiary key, which is used to encrypt the
* backup data.
*/
static class TertiaryKeysEntry implements BaseColumns {
static final String TABLE_NAME = "tertiary_keys";
/** Alias of the secondary key used to wrap the tertiary key. */
static final String COLUMN_NAME_SECONDARY_KEY_ALIAS = "secondary_key_alias";
/** Name of the package to which the tertiary key belongs. */
static final String COLUMN_NAME_PACKAGE_NAME = "package_name";
/** Encrypted bytes of the tertiary key. */
static final String COLUMN_NAME_WRAPPED_KEY_BYTES = "wrapped_key_bytes";
}
}

View File

@@ -1,102 +0,0 @@
/*
* Copyright (C) 2018 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.storage;
import static com.android.server.backup.encryption.storage.BackupEncryptionDbContract.TertiaryKeysEntry;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
/** Helper for creating an instance of the backup encryption database. */
class BackupEncryptionDbHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
static final String DATABASE_NAME = "backupencryption.db";
private static final String SQL_CREATE_TERTIARY_KEYS_ENTRY =
"CREATE TABLE "
+ TertiaryKeysEntry.TABLE_NAME
+ " ( "
+ TertiaryKeysEntry._ID
+ " INTEGER PRIMARY KEY,"
+ TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
+ " TEXT,"
+ TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
+ " TEXT,"
+ TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
+ " BLOB,"
+ "UNIQUE("
+ TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
+ ","
+ TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
+ "))";
private static final String SQL_DROP_TERTIARY_KEYS_ENTRY =
"DROP TABLE IF EXISTS " + TertiaryKeysEntry.TABLE_NAME;
BackupEncryptionDbHelper(Context context) {
super(context, DATABASE_NAME, /*factory=*/ null, DATABASE_VERSION);
}
public void resetDatabase() throws EncryptionDbException {
SQLiteDatabase db = getWritableDatabaseSafe();
db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
onCreate(db);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_TERTIARY_KEYS_ENTRY);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
onCreate(db);
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL(SQL_DROP_TERTIARY_KEYS_ENTRY);
onCreate(db);
}
/**
* Calls {@link #getWritableDatabase()}, but catches the unchecked {@link SQLiteException} and
* rethrows {@link EncryptionDbException}.
*/
public SQLiteDatabase getWritableDatabaseSafe() throws EncryptionDbException {
try {
return super.getWritableDatabase();
} catch (SQLiteException e) {
throw new EncryptionDbException(e);
}
}
/**
* Calls {@link #getReadableDatabase()}, but catches the unchecked {@link SQLiteException} and
* rethrows {@link EncryptionDbException}.
*/
public SQLiteDatabase getReadableDatabaseSafe() throws EncryptionDbException {
try {
return super.getReadableDatabase();
} catch (SQLiteException e) {
throw new EncryptionDbException(e);
}
}
}

View File

@@ -1,26 +0,0 @@
/*
* Copyright (C) 2018 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.storage;
import java.io.IOException;
/** Thrown when there is a problem reading or writing the encryption database. */
public class EncryptionDbException extends IOException {
public EncryptionDbException(Throwable cause) {
super(cause);
}
}

View File

@@ -1,52 +0,0 @@
/*
* Copyright (C) 2018 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.storage;
/** Wrapped bytes of a tertiary key. */
public class TertiaryKey {
private final String mSecondaryKeyAlias;
private final String mPackageName;
private final byte[] mWrappedKeyBytes;
/**
* Creates a new instance.
*
* @param secondaryKeyAlias Alias of the secondary used to wrap the key.
* @param packageName The package name of the app to which the key belongs.
* @param wrappedKeyBytes The wrapped key bytes.
*/
public TertiaryKey(String secondaryKeyAlias, String packageName, byte[] wrappedKeyBytes) {
mSecondaryKeyAlias = secondaryKeyAlias;
mPackageName = packageName;
mWrappedKeyBytes = wrappedKeyBytes;
}
/** Returns the alias of the secondary key used to wrap this tertiary key. */
public String getSecondaryKeyAlias() {
return mSecondaryKeyAlias;
}
/** Returns the package name of the application this key relates to. */
public String getPackageName() {
return mPackageName;
}
/** Returns the wrapped bytes of the key. */
public byte[] getWrappedKeyBytes() {
return mWrappedKeyBytes;
}
}

View File

@@ -1,134 +0,0 @@
/*
* Copyright (C) 2018 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.storage;
import static com.android.server.backup.encryption.storage.BackupEncryptionDbContract.TertiaryKeysEntry;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.ArrayMap;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/** Database table for storing and retrieving tertiary keys. */
public class TertiaryKeysTable {
private final BackupEncryptionDbHelper mHelper;
TertiaryKeysTable(BackupEncryptionDbHelper helper) {
mHelper = helper;
}
/**
* Adds the {@code tertiaryKey} to the database.
*
* @return The primary key of the inserted row if successful, -1 otherwise.
*/
public long addKey(TertiaryKey tertiaryKey) throws EncryptionDbException {
SQLiteDatabase db = mHelper.getWritableDatabaseSafe();
ContentValues values = new ContentValues();
values.put(
TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
tertiaryKey.getSecondaryKeyAlias());
values.put(TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME, tertiaryKey.getPackageName());
values.put(
TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES, tertiaryKey.getWrappedKeyBytes());
return db.replace(TertiaryKeysEntry.TABLE_NAME, /*nullColumnHack=*/ null, values);
}
/** Gets the key wrapped by {@code secondaryKeyAlias} for app with {@code packageName}. */
public Optional<TertiaryKey> getKey(String secondaryKeyAlias, String packageName)
throws EncryptionDbException {
SQLiteDatabase db = mHelper.getReadableDatabaseSafe();
String[] projection = {
TertiaryKeysEntry._ID,
TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME,
TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
};
String selection =
TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS
+ " = ? AND "
+ TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME
+ " = ?";
String[] selectionArguments = {secondaryKeyAlias, packageName};
try (Cursor cursor =
db.query(
TertiaryKeysEntry.TABLE_NAME,
projection,
selection,
selectionArguments,
/*groupBy=*/ null,
/*having=*/ null,
/*orderBy=*/ null)) {
int count = cursor.getCount();
if (count == 0) {
return Optional.empty();
}
cursor.moveToFirst();
byte[] wrappedKeyBytes =
cursor.getBlob(
cursor.getColumnIndexOrThrow(
TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES));
return Optional.of(new TertiaryKey(secondaryKeyAlias, packageName, wrappedKeyBytes));
}
}
/** Returns all keys wrapped with {@code tertiaryKeyAlias} as an unmodifiable map. */
public Map<String, TertiaryKey> getAllKeys(String secondaryKeyAlias)
throws EncryptionDbException {
SQLiteDatabase db = mHelper.getReadableDatabaseSafe();
String[] projection = {
TertiaryKeysEntry._ID,
TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS,
TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME,
TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES
};
String selection = TertiaryKeysEntry.COLUMN_NAME_SECONDARY_KEY_ALIAS + " = ?";
String[] selectionArguments = {secondaryKeyAlias};
Map<String, TertiaryKey> keysByPackageName = new ArrayMap<>();
try (Cursor cursor =
db.query(
TertiaryKeysEntry.TABLE_NAME,
projection,
selection,
selectionArguments,
/*groupBy=*/ null,
/*having=*/ null,
/*orderBy=*/ null)) {
while (cursor.moveToNext()) {
String packageName =
cursor.getString(
cursor.getColumnIndexOrThrow(
TertiaryKeysEntry.COLUMN_NAME_PACKAGE_NAME));
byte[] wrappedKeyBytes =
cursor.getBlob(
cursor.getColumnIndexOrThrow(
TertiaryKeysEntry.COLUMN_NAME_WRAPPED_KEY_BYTES));
keysByPackageName.put(
packageName,
new TertiaryKey(secondaryKeyAlias, packageName, wrappedKeyBytes));
}
}
return Collections.unmodifiableMap(keysByPackageName);
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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;
/**
* Error thrown when the server's active secondary key does not exist in the user's recoverable
* keychain. This means the backup data cannot be decrypted, and should be wiped.
*/
public class ActiveSecondaryNotInKeychainException extends Exception {
public ActiveSecondaryNotInKeychainException(String message) {
super(message);
}
}

View File

@@ -1,90 +0,0 @@
/*
* 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 java.util.Collections.unmodifiableList;
import android.annotation.Nullable;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.EncryptedChunk;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.crypto.SecretKey;
/** Task which reads data from some source, splits it into chunks and encrypts new chunks. */
public interface BackupEncrypter {
/** The algorithm which we use to compute the digest of the backup file plaintext. */
String MESSAGE_DIGEST_ALGORITHM = "SHA-256";
/**
* Splits the backup input into encrypted chunks and encrypts new chunks.
*
* @param secretKey Key used to encrypt backup.
* @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
* full backup. Should be {@code null} for a key-value backup.
* @param existingChunks Set of the SHA-256 Macs of chunks the server already has.
* @return a result containing an array of new encrypted chunks to upload, and an ordered
* listing of the chunks in the backup file.
* @throws IOException if a problem occurs reading from the backup data.
* @throws GeneralSecurityException if there is a problem encrypting the data.
*/
Result backup(
SecretKey secretKey,
@Nullable byte[] fingerprintMixerSalt,
Set<ChunkHash> existingChunks)
throws IOException, GeneralSecurityException;
/**
* The result of an incremental backup. Contains new encrypted chunks to upload, and an ordered
* list of the chunks in the backup file.
*/
class Result {
private final List<ChunkHash> mAllChunks;
private final List<EncryptedChunk> mNewChunks;
private final byte[] mDigest;
public Result(List<ChunkHash> allChunks, List<EncryptedChunk> newChunks, byte[] digest) {
mAllChunks = unmodifiableList(new ArrayList<>(allChunks));
mDigest = digest;
mNewChunks = unmodifiableList(new ArrayList<>(newChunks));
}
/**
* Returns an unmodifiable list of the hashes of all the chunks in the backup, in the order
* they appear in the plaintext.
*/
public List<ChunkHash> getAllChunks() {
return mAllChunks;
}
/** Returns an unmodifiable list of the new chunks in the backup. */
public List<EncryptedChunk> getNewChunks() {
return mNewChunks;
}
/** Returns the message digest of the backup. */
public byte[] getDigest() {
return mDigest;
}
}
}

View File

@@ -1,378 +0,0 @@
/*
* 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.util.Slog;
import android.util.SparseIntArray;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunkOrdering;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.ChunksMetadata;
import com.google.protobuf.nano.InvalidProtocolBufferNanoException;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Locale;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
/**
* A backup file consists of, in order:
*
* <ul>
* <li>A randomly ordered sequence of encrypted chunks
* <li>A plaintext {@link ChunksMetadata} proto, containing the bytes of an encrypted {@link
* ChunkOrdering} proto.
* <li>A 64-bit long denoting the offset of the file at which the ChunkOrdering proto starts.
* </ul>
*
* <p>This task decrypts such a blob and writes the plaintext to another file.
*
* <p>The backup file has two formats to indicate the boundaries of the chunks in the encrypted
* file. In {@link ChunksMetadataProto#EXPLICIT_STARTS} mode the chunk ordering contains the start
* positions of each chunk and the decryptor outputs the chunks in the order they appeared in the
* plaintext file. In {@link ChunksMetadataProto#INLINE_LENGTHS} mode the length of each encrypted
* chunk is prepended to the chunk in the file and the decryptor outputs the chunks in no specific
* order.
*
* <p>{@link ChunksMetadataProto#EXPLICIT_STARTS} is for use with full backup (Currently used for
* all backups as b/77188289 is not implemented yet), {@link ChunksMetadataProto#INLINE_LENGTHS}
* will be used for kv backup (once b/77188289 is implemented) to avoid re-uploading the chunk
* ordering (see b/70782620).
*/
public class BackupFileDecryptorTask {
private static final String TAG = "BackupFileDecryptorTask";
private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_NONCE_LENGTH_BYTES = 12;
private static final int GCM_TAG_LENGTH_BYTES = 16;
private static final int BITS_PER_BYTE = 8;
private static final String READ_MODE = "r";
private static final int BYTES_PER_LONG = 64 / BITS_PER_BYTE;
private final Cipher mCipher;
private final SecretKey mSecretKey;
/**
* A new instance.
*
* @param secretKey The tertiary key used to encrypt the backup blob.
*/
public BackupFileDecryptorTask(SecretKey secretKey)
throws NoSuchPaddingException, NoSuchAlgorithmException {
this.mCipher = Cipher.getInstance(CIPHER_ALGORITHM);
this.mSecretKey = secretKey;
}
/**
* Runs the task, reading the encrypted data from {@code input} and writing the plaintext data
* to {@code output}.
*
* @param inputFile The encrypted backup file.
* @param decryptedChunkOutput Unopened output to write the plaintext to, which this class will
* open and close during decryption.
* @throws IOException if an error occurred reading the encrypted file or writing the plaintext,
* or if one of the protos could not be deserialized.
*/
public void decryptFile(File inputFile, DecryptedChunkOutput decryptedChunkOutput)
throws IOException, EncryptedRestoreException, IllegalBlockSizeException,
BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException,
ShortBufferException, NoSuchAlgorithmException {
RandomAccessFile input = new RandomAccessFile(inputFile, READ_MODE);
long metadataOffset = getChunksMetadataOffset(input);
ChunksMetadataProto.ChunksMetadata chunksMetadata =
getChunksMetadata(input, metadataOffset);
ChunkOrdering chunkOrdering = decryptChunkOrdering(chunksMetadata);
if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED
|| chunksMetadata.chunkOrderingType == ChunksMetadataProto.EXPLICIT_STARTS) {
Slog.d(TAG, "Using explicit starts");
decryptFileWithExplicitStarts(
input, decryptedChunkOutput, chunkOrdering, metadataOffset);
} else if (chunksMetadata.chunkOrderingType == ChunksMetadataProto.INLINE_LENGTHS) {
Slog.d(TAG, "Using inline lengths");
decryptFileWithInlineLengths(input, decryptedChunkOutput, metadataOffset);
} else {
throw new UnsupportedEncryptedFileException(
"Unknown chunk ordering type:" + chunksMetadata.chunkOrderingType);
}
if (!Arrays.equals(decryptedChunkOutput.getDigest(), chunkOrdering.checksum)) {
throw new MessageDigestMismatchException("Checksums did not match");
}
}
private void decryptFileWithExplicitStarts(
RandomAccessFile input,
DecryptedChunkOutput decryptedChunkOutput,
ChunkOrdering chunkOrdering,
long metadataOffset)
throws IOException, InvalidKeyException, IllegalBlockSizeException,
InvalidAlgorithmParameterException, ShortBufferException, BadPaddingException,
NoSuchAlgorithmException {
SparseIntArray chunkLengthsByPosition =
getChunkLengths(chunkOrdering.starts, (int) metadataOffset);
int largestChunkLength = getLargestChunkLength(chunkLengthsByPosition);
byte[] encryptedChunkBuffer = new byte[largestChunkLength];
// largestChunkLength is 0 if the backup file contains zero chunks e.g. 0 kv pairs.
int plaintextBufferLength =
Math.max(0, largestChunkLength - GCM_NONCE_LENGTH_BYTES - GCM_TAG_LENGTH_BYTES);
byte[] plaintextChunkBuffer = new byte[plaintextBufferLength];
try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
for (int start : chunkOrdering.starts) {
int length = chunkLengthsByPosition.get(start);
input.seek(start);
input.readFully(encryptedChunkBuffer, 0, length);
int plaintextLength =
decryptChunk(encryptedChunkBuffer, length, plaintextChunkBuffer);
outputChunk(output, plaintextChunkBuffer, plaintextLength);
}
}
}
private void decryptFileWithInlineLengths(
RandomAccessFile input, DecryptedChunkOutput decryptedChunkOutput, long metadataOffset)
throws MalformedEncryptedFileException, IOException, IllegalBlockSizeException,
BadPaddingException, InvalidAlgorithmParameterException, ShortBufferException,
InvalidKeyException, NoSuchAlgorithmException {
input.seek(0);
try (DecryptedChunkOutput output = decryptedChunkOutput.open()) {
while (input.getFilePointer() < metadataOffset) {
long start = input.getFilePointer();
int encryptedChunkLength = input.readInt();
if (encryptedChunkLength <= 0) {
// If the length of the encrypted chunk is not positive we will not make
// progress reading the file and so will loop forever.
throw new MalformedEncryptedFileException(
"Encrypted chunk length not positive:" + encryptedChunkLength);
}
if (start + encryptedChunkLength > metadataOffset) {
throw new MalformedEncryptedFileException(
String.format(
Locale.US,
"Encrypted chunk longer (%d) than file (%d)",
encryptedChunkLength,
metadataOffset));
}
byte[] plaintextChunk = new byte[encryptedChunkLength];
byte[] plaintext =
new byte
[encryptedChunkLength
- GCM_NONCE_LENGTH_BYTES
- GCM_TAG_LENGTH_BYTES];
input.readFully(plaintextChunk);
int plaintextChunkLength =
decryptChunk(plaintextChunk, encryptedChunkLength, plaintext);
outputChunk(output, plaintext, plaintextChunkLength);
}
}
}
private void outputChunk(
DecryptedChunkOutput output, byte[] plaintextChunkBuffer, int plaintextLength)
throws IOException, InvalidKeyException, NoSuchAlgorithmException {
output.processChunk(plaintextChunkBuffer, plaintextLength);
}
/**
* Decrypts chunk and returns the length of the plaintext.
*
* @param encryptedChunkBuffer The encrypted data, prefixed by the nonce.
* @param encryptedChunkBufferLength The length of the encrypted chunk (including nonce).
* @param plaintextChunkBuffer The buffer into which to write the plaintext chunk.
* @return The length of the plaintext chunk.
*/
private int decryptChunk(
byte[] encryptedChunkBuffer,
int encryptedChunkBufferLength,
byte[] plaintextChunkBuffer)
throws InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException,
ShortBufferException, IllegalBlockSizeException {
mCipher.init(
Cipher.DECRYPT_MODE,
mSecretKey,
new GCMParameterSpec(
GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
encryptedChunkBuffer,
0,
GCM_NONCE_LENGTH_BYTES));
return mCipher.doFinal(
encryptedChunkBuffer,
GCM_NONCE_LENGTH_BYTES,
encryptedChunkBufferLength - GCM_NONCE_LENGTH_BYTES,
plaintextChunkBuffer);
}
/** Given all the lengths, returns the largest length. */
private int getLargestChunkLength(SparseIntArray lengths) {
int maxSeen = 0;
for (int i = 0; i < lengths.size(); i++) {
maxSeen = Math.max(maxSeen, lengths.valueAt(i));
}
return maxSeen;
}
/**
* From a list of the starting position of each chunk in the correct order of the backup data,
* calculates a mapping from start position to length of that chunk.
*
* @param starts The start positions of chunks, in order.
* @param chunkOrderingPosition Where the {@link ChunkOrdering} proto starts, used to calculate
* the length of the last chunk.
* @return The mapping.
*/
private SparseIntArray getChunkLengths(int[] starts, int chunkOrderingPosition) {
int[] boundaries = Arrays.copyOf(starts, starts.length + 1);
boundaries[boundaries.length - 1] = chunkOrderingPosition;
Arrays.sort(boundaries);
SparseIntArray lengths = new SparseIntArray();
for (int i = 0; i < boundaries.length - 1; i++) {
lengths.put(boundaries[i], boundaries[i + 1] - boundaries[i]);
}
return lengths;
}
/**
* Reads and decrypts the {@link ChunkOrdering} from the {@link ChunksMetadata}.
*
* @param metadata The metadata.
* @return The ordering.
* @throws InvalidProtocolBufferNanoException if there is an issue deserializing the proto.
*/
private ChunkOrdering decryptChunkOrdering(ChunksMetadata metadata)
throws InvalidProtocolBufferNanoException, InvalidAlgorithmParameterException,
InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
UnsupportedEncryptedFileException {
assertCryptoSupported(metadata);
mCipher.init(
Cipher.DECRYPT_MODE,
mSecretKey,
new GCMParameterSpec(
GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE,
metadata.chunkOrdering,
0,
GCM_NONCE_LENGTH_BYTES));
byte[] decrypted =
mCipher.doFinal(
metadata.chunkOrdering,
GCM_NONCE_LENGTH_BYTES,
metadata.chunkOrdering.length - GCM_NONCE_LENGTH_BYTES);
return ChunkOrdering.parseFrom(decrypted);
}
/**
* Asserts that the Cipher and MessageDigest algorithms in the backup metadata are supported.
* For now we only support SHA-256 for checksum and 256-bit AES/GCM/NoPadding for the Cipher.
*
* @param chunksMetadata The file metadata.
* @throws UnsupportedEncryptedFileException if any algorithm is unsupported.
*/
private void assertCryptoSupported(ChunksMetadata chunksMetadata)
throws UnsupportedEncryptedFileException {
if (chunksMetadata.checksumType != ChunksMetadataProto.SHA_256) {
// For now we only support SHA-256.
throw new UnsupportedEncryptedFileException(
"Unrecognized checksum type for backup (this version of backup only supports"
+ " SHA-256): "
+ chunksMetadata.checksumType);
}
if (chunksMetadata.cipherType != ChunksMetadataProto.AES_256_GCM) {
throw new UnsupportedEncryptedFileException(
"Unrecognized cipher type for backup (this version of backup only supports"
+ " AES-256-GCM: "
+ chunksMetadata.cipherType);
}
}
/**
* Reads the offset of the {@link ChunksMetadata} proto from the end of the file.
*
* @return The offset.
* @throws IOException if there is an error reading.
*/
private long getChunksMetadataOffset(RandomAccessFile input) throws IOException {
input.seek(input.length() - BYTES_PER_LONG);
return input.readLong();
}
/**
* Reads the {@link ChunksMetadata} proto from the given position in the file.
*
* @param input The encrypted file.
* @param position The position where the proto starts.
* @return The proto.
* @throws IOException if there is an issue reading the file or deserializing the proto.
*/
private ChunksMetadata getChunksMetadata(RandomAccessFile input, long position)
throws IOException, MalformedEncryptedFileException {
long length = input.length();
if (position >= length || position < 0) {
throw new MalformedEncryptedFileException(
String.format(
Locale.US,
"%d is not valid position for chunks metadata in file of %d bytes",
position,
length));
}
// Read chunk ordering bytes
input.seek(position);
long chunksMetadataLength = input.length() - BYTES_PER_LONG - position;
byte[] chunksMetadataBytes = new byte[(int) chunksMetadataLength];
input.readFully(chunksMetadataBytes);
try {
return ChunksMetadata.parseFrom(chunksMetadataBytes);
} catch (InvalidProtocolBufferNanoException e) {
throw new MalformedEncryptedFileException(
String.format(
Locale.US,
"Could not read chunks metadata at position %d of file of %d bytes",
position,
length));
}
}
}

View File

@@ -1,127 +0,0 @@
/*
* 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.util.Slog;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.ChunkEncryptor;
import com.android.server.backup.encryption.chunking.ChunkHasher;
import com.android.server.backup.encryption.chunking.EncryptedChunk;
import com.android.server.backup.encryption.chunking.cdc.ContentDefinedChunker;
import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
import com.android.server.backup.encryption.chunking.cdc.IsChunkBreakpoint;
import com.android.server.backup.encryption.chunking.cdc.RabinFingerprint64;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.crypto.SecretKey;
/**
* Splits backup data into variable-sized chunks using content-defined chunking, then encrypts the
* chunks. Given a hash of the SHA-256s of existing chunks, performs an incremental backup (i.e.,
* only encrypts new chunks).
*/
public class BackupStreamEncrypter implements BackupEncrypter {
private static final String TAG = "BackupStreamEncryptor";
private final InputStream mData;
private final int mMinChunkSizeBytes;
private final int mMaxChunkSizeBytes;
private final int mAverageChunkSizeBytes;
/**
* A new instance over the given distribution of chunk sizes.
*
* @param data The data to be backed up.
* @param minChunkSizeBytes The minimum chunk size. No chunk will be smaller than this.
* @param maxChunkSizeBytes The maximum chunk size. No chunk will be larger than this.
* @param averageChunkSizeBytes The average chunk size. The mean size of chunks will be roughly
* this (with a few tens of bytes of overhead for the initialization vector and message
* authentication code).
*/
public BackupStreamEncrypter(
InputStream data,
int minChunkSizeBytes,
int maxChunkSizeBytes,
int averageChunkSizeBytes) {
this.mData = data;
this.mMinChunkSizeBytes = minChunkSizeBytes;
this.mMaxChunkSizeBytes = maxChunkSizeBytes;
this.mAverageChunkSizeBytes = averageChunkSizeBytes;
}
@Override
public Result backup(
SecretKey secretKey, byte[] fingerprintMixerSalt, Set<ChunkHash> existingChunks)
throws IOException, GeneralSecurityException {
MessageDigest messageDigest =
MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
RabinFingerprint64 rabinFingerprint64 = new RabinFingerprint64();
FingerprintMixer fingerprintMixer = new FingerprintMixer(secretKey, fingerprintMixerSalt);
IsChunkBreakpoint isChunkBreakpoint =
new IsChunkBreakpoint(mAverageChunkSizeBytes - mMinChunkSizeBytes);
ContentDefinedChunker chunker =
new ContentDefinedChunker(
mMinChunkSizeBytes,
mMaxChunkSizeBytes,
rabinFingerprint64,
fingerprintMixer,
isChunkBreakpoint);
ChunkHasher chunkHasher = new ChunkHasher(secretKey);
ChunkEncryptor encryptor = new ChunkEncryptor(secretKey, new SecureRandom());
Set<ChunkHash> includedChunks = new HashSet<>();
// New chunks will be added only once to this list, even if they occur multiple times.
List<EncryptedChunk> newChunks = new ArrayList<>();
// All chunks (including multiple occurrences) will be added to the chunkListing.
List<ChunkHash> chunkListing = new ArrayList<>();
includedChunks.addAll(existingChunks);
chunker.chunkify(
mData,
chunk -> {
messageDigest.update(chunk);
ChunkHash key = chunkHasher.computeHash(chunk);
if (!includedChunks.contains(key)) {
newChunks.add(encryptor.encrypt(key, chunk));
includedChunks.add(key);
}
chunkListing.add(key);
});
Slog.i(
TAG,
String.format(
"Chunks: %d total, %d unique, %d new",
chunkListing.size(), new HashSet<>(chunkListing).size(), newChunks.size()));
return new Result(
Collections.unmodifiableList(chunkListing),
Collections.unmodifiableList(newChunks),
messageDigest.digest());
}
}

View File

@@ -1,71 +0,0 @@
/*
* 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.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.storage.BackupEncryptionDb;
import com.android.server.backup.encryption.storage.EncryptionDbException;
import java.io.IOException;
/**
* Task to clear local crypto state.
*
* <p>Needs to run whenever the user changes their backup account.
*/
public class ClearCryptoStateTask {
private static final String TAG = "ClearCryptoStateTask";
private final Context mContext;
private final CryptoSettings mCryptoSettings;
/**
* A new instance.
*
* @param context for finding local storage.
* @param cryptoSettings to clear
*/
public ClearCryptoStateTask(Context context, CryptoSettings cryptoSettings) {
mContext = context;
mCryptoSettings = cryptoSettings;
}
/** Deletes all local state for backup (not restore). */
public void run() {
Slog.d(TAG, "Clearing local crypto state.");
try {
BackupEncryptionDb.newInstance(mContext).clear();
} catch (EncryptionDbException e) {
Slog.e(TAG, "Error clearing encryption database", e);
}
mCryptoSettings.clearAllSettingsForBackup();
try {
ProtoStore.createChunkListingStore(mContext).deleteAllProtos();
} catch (IOException e) {
Slog.e(TAG, "Error clearing chunk listing store", e);
}
try {
ProtoStore.createKeyValueListingStore(mContext).deleteAllProtos();
} catch (IOException e) {
Slog.e(TAG, "Error clearing key-value store", e);
}
}
}

View File

@@ -1,56 +0,0 @@
/*
* 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 java.io.Closeable;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
/**
* Accepts the plaintext bytes of decrypted chunks and writes them to some output. Also keeps track
* of the message digest of the chunks.
*/
public interface DecryptedChunkOutput extends Closeable {
/**
* Opens whatever output the implementation chooses, ready to process chunks.
*
* @return {@code this}, to allow use with try-with-resources
*/
DecryptedChunkOutput open() throws IOException, NoSuchAlgorithmException;
/**
* Writes the plaintext bytes of chunk to whatever output the implementation chooses. Also
* updates the digest with the chunk.
*
* <p>You must call {@link #open()} before this method, and you may not call it after calling
* {@link Closeable#close()}.
*
* @param plaintextBuffer An array containing the bytes of the plaintext of the chunk, starting
* at index 0.
* @param length The length in bytes of the plaintext contained in {@code plaintextBuffer}.
*/
void processChunk(byte[] plaintextBuffer, int length)
throws IOException, InvalidKeyException, NoSuchAlgorithmException;
/**
* Returns the message digest of all the chunks processed by {@link #processChunk}.
*
* <p>You must call {@link Closeable#close()} before calling this method.
*/
byte[] getDigest() throws NoSuchAlgorithmException;
}

View File

@@ -1,243 +0,0 @@
/*
* 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.annotation.Nullable;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import android.util.Slog;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.BackupFileBuilder;
import com.android.server.backup.encryption.chunking.EncryptedChunk;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
/**
* Task which reads encrypted chunks from a {@link BackupEncrypter}, builds a backup file and
* uploads it to the server.
*/
@TargetApi(VERSION_CODES.P)
public class EncryptedBackupTask {
private static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_NONCE_LENGTH_BYTES = 12;
private static final int GCM_TAG_LENGTH_BYTES = 16;
private static final int BITS_PER_BYTE = 8;
private static final String TAG = "EncryptedBackupTask";
private final CryptoBackupServer mCryptoBackupServer;
private final SecureRandom mSecureRandom;
private final String mPackageName;
private final ByteArrayOutputStream mBackupDataOutput;
private final BackupEncrypter mBackupEncrypter;
private final AtomicBoolean mCancelled;
/** Creates a new instance which reads data from the given input stream. */
public EncryptedBackupTask(
CryptoBackupServer cryptoBackupServer,
SecureRandom secureRandom,
String packageName,
BackupEncrypter backupEncrypter) {
mCryptoBackupServer = cryptoBackupServer;
mSecureRandom = secureRandom;
mPackageName = packageName;
mBackupEncrypter = backupEncrypter;
mBackupDataOutput = new ByteArrayOutputStream();
mCancelled = new AtomicBoolean(false);
}
/**
* Creates a non-incremental backup file and uploads it to the server.
*
* @param fingerprintMixerSalt Fingerprint mixer salt used for content-defined chunking during a
* full backup. May be {@code null} for a key-value backup.
*/
public ChunksMetadataProto.ChunkListing performNonIncrementalBackup(
SecretKey tertiaryKey,
WrappedKeyProto.WrappedKey wrappedTertiaryKey,
@Nullable byte[] fingerprintMixerSalt)
throws IOException, GeneralSecurityException {
ChunksMetadataProto.ChunkListing newChunkListing =
performBackup(
tertiaryKey,
fingerprintMixerSalt,
BackupFileBuilder.createForNonIncremental(mBackupDataOutput),
new HashSet<>());
throwIfCancelled();
newChunkListing.documentId =
mCryptoBackupServer.uploadNonIncrementalBackup(
mPackageName, mBackupDataOutput.toByteArray(), wrappedTertiaryKey);
return newChunkListing;
}
/** Creates an incremental backup file and uploads it to the server. */
public ChunksMetadataProto.ChunkListing performIncrementalBackup(
SecretKey tertiaryKey,
WrappedKeyProto.WrappedKey wrappedTertiaryKey,
ChunksMetadataProto.ChunkListing oldChunkListing)
throws IOException, GeneralSecurityException {
ChunksMetadataProto.ChunkListing newChunkListing =
performBackup(
tertiaryKey,
oldChunkListing.fingerprintMixerSalt,
BackupFileBuilder.createForIncremental(mBackupDataOutput, oldChunkListing),
getChunkHashes(oldChunkListing));
throwIfCancelled();
String oldDocumentId = oldChunkListing.documentId;
Slog.v(TAG, "Old doc id: " + oldDocumentId);
newChunkListing.documentId =
mCryptoBackupServer.uploadIncrementalBackup(
mPackageName,
oldDocumentId,
mBackupDataOutput.toByteArray(),
wrappedTertiaryKey);
return newChunkListing;
}
/**
* 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.
*/
public void cancel() {
mCancelled.getAndSet(true);
}
private void throwIfCancelled() {
if (mCancelled.get()) {
throw new CancellationException("EncryptedBackupTask was cancelled");
}
}
private ChunksMetadataProto.ChunkListing performBackup(
SecretKey tertiaryKey,
@Nullable byte[] fingerprintMixerSalt,
BackupFileBuilder backupFileBuilder,
Set<ChunkHash> existingChunkHashes)
throws IOException, GeneralSecurityException {
BackupEncrypter.Result result =
mBackupEncrypter.backup(tertiaryKey, fingerprintMixerSalt, existingChunkHashes);
backupFileBuilder.writeChunks(result.getAllChunks(), buildChunkMap(result.getNewChunks()));
ChunksMetadataProto.ChunkOrdering chunkOrdering =
backupFileBuilder.getNewChunkOrdering(result.getDigest());
backupFileBuilder.finish(buildMetadata(tertiaryKey, chunkOrdering));
return backupFileBuilder.getNewChunkListing(fingerprintMixerSalt);
}
/** Returns a set containing the hashes of every chunk in the given listing. */
private static Set<ChunkHash> getChunkHashes(ChunksMetadataProto.ChunkListing chunkListing) {
Set<ChunkHash> hashes = new HashSet<>();
for (ChunksMetadataProto.Chunk chunk : chunkListing.chunks) {
hashes.add(new ChunkHash(chunk.hash));
}
return hashes;
}
/** Returns a map from chunk hash to chunk containing every chunk in the given list. */
private static Map<ChunkHash, EncryptedChunk> buildChunkMap(List<EncryptedChunk> chunks) {
Map<ChunkHash, EncryptedChunk> chunkMap = new HashMap<>();
for (EncryptedChunk chunk : chunks) {
chunkMap.put(chunk.key(), chunk);
}
return chunkMap;
}
private ChunksMetadataProto.ChunksMetadata buildMetadata(
SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException,
ShortBufferException, NoSuchPaddingException {
ChunksMetadataProto.ChunksMetadata metaData = new ChunksMetadataProto.ChunksMetadata();
metaData.cipherType = ChunksMetadataProto.AES_256_GCM;
metaData.checksumType = ChunksMetadataProto.SHA_256;
metaData.chunkOrdering = encryptChunkOrdering(tertiaryKey, chunkOrdering);
return metaData;
}
private byte[] encryptChunkOrdering(
SecretKey tertiaryKey, ChunksMetadataProto.ChunkOrdering chunkOrdering)
throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException,
NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, ShortBufferException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
byte[] nonce = generateNonce();
cipher.init(
Cipher.ENCRYPT_MODE,
tertiaryKey,
new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));
byte[] orderingBytes = ChunksMetadataProto.ChunkOrdering.toByteArray(chunkOrdering);
// We prepend the nonce to the ordering.
byte[] output =
Arrays.copyOf(
nonce,
GCM_NONCE_LENGTH_BYTES + orderingBytes.length + GCM_TAG_LENGTH_BYTES);
cipher.doFinal(
orderingBytes,
/*inputOffset=*/ 0,
/*inputLen=*/ orderingBytes.length,
output,
/*outputOffset=*/ GCM_NONCE_LENGTH_BYTES);
return output;
}
private byte[] generateNonce() {
byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
mSecureRandom.nextBytes(nonce);
return nonce;
}
}

View File

@@ -1,210 +0,0 @@
/*
* 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.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.Objects;
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 = Objects.requireNonNull(context);
mExecutorService = Objects.requireNonNull(executorService);
mCryptoBackupServer = Objects.requireNonNull(cryptoBackupServer);
mSecureRandom = Objects.requireNonNull(secureRandom);
mSecondaryKey = Objects.requireNonNull(secondaryKey);
mPackageName = Objects.requireNonNull(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();
}
}

View File

@@ -1,197 +0,0 @@
/*
* 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();
}
}

View File

@@ -1,137 +0,0 @@
/*
* 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();
}
}

View File

@@ -1,244 +0,0 @@
/*
* 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.annotation.Nullable;
import android.app.backup.BackupDataInput;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
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;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.util.Optional;
// TODO(b/141975695): Create a base class for EncryptedKvBackupTask and EncryptedFullBackupTask.
/** Performs encrypted key value backup, handling rotating the tertiary key as necessary. */
public class EncryptedKvBackupTask {
private static final String TAG = "EncryptedKvBackupTask";
private final TertiaryKeyManager mTertiaryKeyManager;
private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
private final ProtoStore<KeyValueListingProto.KeyValueListing> mKeyValueListingStore;
private final ProtoStore<ChunksMetadataProto.ChunkListing> mChunkListingStore;
private final KvBackupEncrypter mKvBackupEncrypter;
private final EncryptedBackupTask mEncryptedBackupTask;
private final String mPackageName;
/** Constructs new instances of {@link EncryptedKvBackupTask}. */
public static class EncryptedKvBackupTaskFactory {
/**
* Creates a new instance.
*
* <p>Either initializes encrypted backup or loads an existing secondary key as necessary.
*
* @param cryptoSettings to load secondary key state from
* @param fileDescriptor to read the backup data from
*/
public EncryptedKvBackupTask newInstance(
Context context,
SecureRandom secureRandom,
CryptoBackupServer cryptoBackupServer,
CryptoSettings cryptoSettings,
RecoverableKeyStoreSecondaryKeyManager
.RecoverableKeyStoreSecondaryKeyManagerProvider
recoverableSecondaryKeyManagerProvider,
ParcelFileDescriptor fileDescriptor,
String packageName)
throws IOException, UnrecoverableKeyException, LockScreenRequiredException,
InternalRecoveryServiceException, InvalidKeyException {
RecoverableKeyStoreSecondaryKey secondaryKey =
new InitializeRecoverableSecondaryKeyTask(
context,
cryptoSettings,
recoverableSecondaryKeyManagerProvider.get(),
cryptoBackupServer)
.run();
KvBackupEncrypter backupEncrypter =
new KvBackupEncrypter(new BackupDataInput(fileDescriptor.getFileDescriptor()));
TertiaryKeyManager tertiaryKeyManager =
new TertiaryKeyManager(
context,
secureRandom,
TertiaryKeyRotationScheduler.getInstance(context),
secondaryKey,
packageName);
return new EncryptedKvBackupTask(
tertiaryKeyManager,
ProtoStore.createKeyValueListingStore(context),
secondaryKey,
ProtoStore.createChunkListingStore(context),
backupEncrypter,
new EncryptedBackupTask(
cryptoBackupServer, secureRandom, packageName, backupEncrypter),
packageName);
}
}
@VisibleForTesting
EncryptedKvBackupTask(
TertiaryKeyManager tertiaryKeyManager,
ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore,
RecoverableKeyStoreSecondaryKey secondaryKey,
ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore,
KvBackupEncrypter kvBackupEncrypter,
EncryptedBackupTask encryptedBackupTask,
String packageName) {
mTertiaryKeyManager = tertiaryKeyManager;
mSecondaryKey = secondaryKey;
mKeyValueListingStore = keyValueListingStore;
mChunkListingStore = chunkListingStore;
mKvBackupEncrypter = kvBackupEncrypter;
mEncryptedBackupTask = encryptedBackupTask;
mPackageName = packageName;
}
/**
* Reads backup data from the file descriptor provided in the construtor, encrypts it and
* uploads it to the server.
*
* <p>The {@code incremental} flag indicates if the backup data provided is incremental or a
* complete set. Incremental backup is not possible if no previous crypto state exists, or the
* tertiary key must be rotated in the next backup. If the caller requests incremental backup
* but it is not possible, then the backup will not start and this method will throw {@link
* NonIncrementalBackupRequiredException}.
*
* <p>TODO(b/70704456): Update return code to indicate that we require non-incremental backup.
*
* @param incremental {@code true} if the data provided is a diff from the previous backup,
* {@code false} if it is a complete set
* @throws NonIncrementalBackupRequiredException if the caller provides an incremental backup but the task
* requires non-incremental backup
*/
public void performBackup(boolean incremental)
throws GeneralSecurityException, IOException, NoSuchMethodException,
InstantiationException, IllegalAccessException, InvocationTargetException,
NonIncrementalBackupRequiredException {
if (mTertiaryKeyManager.wasKeyRotated()) {
Slog.d(TAG, "Tertiary key is new so clearing package state.");
deleteListings(mPackageName);
}
Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
oldListings = getListingsAndEnsureConsistency(mPackageName);
if (oldListings.isPresent() && !incremental) {
Slog.d(
TAG,
"Non-incremental backup requested but incremental state existed, clearing it");
deleteListings(mPackageName);
oldListings = Optional.empty();
}
if (!oldListings.isPresent() && incremental) {
// If we don't have any state then we require a non-incremental backup, but this backup
// is incremental.
throw new NonIncrementalBackupRequiredException();
}
if (oldListings.isPresent()) {
mKvBackupEncrypter.setOldKeyValueListing(oldListings.get().first);
}
ChunksMetadataProto.ChunkListing newChunkListing;
if (oldListings.isPresent()) {
Slog.v(TAG, "Old listings existed, performing incremental backup");
newChunkListing =
mEncryptedBackupTask.performIncrementalBackup(
mTertiaryKeyManager.getKey(),
mTertiaryKeyManager.getWrappedKey(),
oldListings.get().second);
} else {
Slog.v(TAG, "Old listings did not exist, performing non-incremental backup");
// kv backups don't use this salt because they don't involve content-defined chunking.
byte[] fingerprintMixerSalt = null;
newChunkListing =
mEncryptedBackupTask.performNonIncrementalBackup(
mTertiaryKeyManager.getKey(),
mTertiaryKeyManager.getWrappedKey(),
fingerprintMixerSalt);
}
Slog.v(TAG, "Backup and upload succeeded, saving new listings");
saveListings(mPackageName, mKvBackupEncrypter.getNewKeyValueListing(), newChunkListing);
}
private Optional<Pair<KeyValueListingProto.KeyValueListing, ChunksMetadataProto.ChunkListing>>
getListingsAndEnsureConsistency(String packageName)
throws IOException, InvocationTargetException, NoSuchMethodException,
InstantiationException, IllegalAccessException {
Optional<KeyValueListingProto.KeyValueListing> keyValueListing =
mKeyValueListingStore.loadProto(packageName);
Optional<ChunksMetadataProto.ChunkListing> chunkListing =
mChunkListingStore.loadProto(packageName);
// Normally either both protos exist or neither exist, but we correct this just in case.
boolean bothPresent = keyValueListing.isPresent() && chunkListing.isPresent();
if (!bothPresent) {
Slog.d(
TAG,
"Both listing were not present, clearing state, key value="
+ keyValueListing.isPresent()
+ ", chunk="
+ chunkListing.isPresent());
deleteListings(packageName);
return Optional.empty();
}
return Optional.of(Pair.create(keyValueListing.get(), chunkListing.get()));
}
private void saveListings(
String packageName,
KeyValueListingProto.KeyValueListing keyValueListing,
ChunksMetadataProto.ChunkListing chunkListing) {
try {
mKeyValueListingStore.saveProto(packageName, keyValueListing);
mChunkListingStore.saveProto(packageName, chunkListing);
} catch (IOException e) {
// If a problem occurred while saving either listing then they may be inconsistent, so
// delete
// both.
Slog.w(TAG, "Unable to save listings, deleting both for consistency", e);
deleteListings(packageName);
}
}
private void deleteListings(String packageName) {
mKeyValueListingStore.deleteProto(packageName);
mChunkListingStore.deleteProto(packageName);
}
}

View File

@@ -1,139 +0,0 @@
/*
* 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.app.backup.BackupDataOutput;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.FullRestoreDownloader;
import com.android.server.backup.encryption.chunking.ChunkHasher;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import com.android.server.backup.encryption.keys.RestoreKeyFetcher;
import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.io.File;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
/**
* Performs a key value restore by downloading the backup set, decrypting it and writing it to the
* file provided by backup manager.
*/
public class EncryptedKvRestoreTask {
private static final String ENCRYPTED_FILE_NAME = "encrypted_kv";
private final File mTemporaryFolder;
private final ChunkHasher mChunkHasher;
private final FullRestoreToFileTask mFullRestoreToFileTask;
private final BackupFileDecryptorTask mBackupFileDecryptorTask;
/** Constructs new instances of the task. */
public static class EncryptedKvRestoreTaskFactory {
/**
* Constructs a new instance.
*
* <p>Fetches the appropriate secondary key and uses this to unwrap the tertiary key. Stores
* temporary files in {@link Context#getFilesDir()}.
*/
public EncryptedKvRestoreTask newInstance(
Context context,
RecoverableKeyStoreSecondaryKeyManager
.RecoverableKeyStoreSecondaryKeyManagerProvider
recoverableSecondaryKeyManagerProvider,
FullRestoreDownloader fullRestoreDownloader,
String secondaryKeyAlias,
WrappedKeyProto.WrappedKey wrappedTertiaryKey)
throws EncryptedRestoreException, NoSuchAlgorithmException, NoSuchPaddingException,
KeyException, InvalidAlgorithmParameterException {
SecretKey tertiaryKey =
RestoreKeyFetcher.unwrapTertiaryKey(
recoverableSecondaryKeyManagerProvider,
secondaryKeyAlias,
wrappedTertiaryKey);
return new EncryptedKvRestoreTask(
context.getFilesDir(),
new ChunkHasher(tertiaryKey),
new FullRestoreToFileTask(fullRestoreDownloader),
new BackupFileDecryptorTask(tertiaryKey));
}
}
@VisibleForTesting
EncryptedKvRestoreTask(
File temporaryFolder,
ChunkHasher chunkHasher,
FullRestoreToFileTask fullRestoreToFileTask,
BackupFileDecryptorTask backupFileDecryptorTask) {
checkArgument(
temporaryFolder.isDirectory(), "Temporary folder must be an existing directory");
mTemporaryFolder = temporaryFolder;
mChunkHasher = chunkHasher;
mFullRestoreToFileTask = fullRestoreToFileTask;
mBackupFileDecryptorTask = backupFileDecryptorTask;
}
/**
* Runs the restore, writing the pairs in lexicographical order to the given file descriptor.
*
* <p>This will block for the duration of the restore.
*
* @throws EncryptedRestoreException if there is a problem decrypting or verifying the backup
*/
public void getRestoreData(ParcelFileDescriptor output)
throws IOException, EncryptedRestoreException, BadPaddingException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException,
IllegalBlockSizeException, ShortBufferException, InvalidKeyException {
File encryptedFile = new File(mTemporaryFolder, ENCRYPTED_FILE_NAME);
try {
downloadDecryptAndWriteBackup(encryptedFile, output);
} finally {
encryptedFile.delete();
}
}
private void downloadDecryptAndWriteBackup(File encryptedFile, ParcelFileDescriptor output)
throws EncryptedRestoreException, IOException, BadPaddingException, InvalidKeyException,
NoSuchAlgorithmException, IllegalBlockSizeException, ShortBufferException,
InvalidAlgorithmParameterException {
mFullRestoreToFileTask.restoreToFile(encryptedFile);
DecryptedChunkKvOutput decryptedChunkKvOutput = new DecryptedChunkKvOutput(mChunkHasher);
mBackupFileDecryptorTask.decryptFile(encryptedFile, decryptedChunkKvOutput);
BackupDataOutput backupDataOutput = new BackupDataOutput(output.getFileDescriptor());
for (KeyValuePairProto.KeyValuePair pair : decryptedChunkKvOutput.getPairs()) {
backupDataOutput.writeEntityHeader(pair.key, pair.value.length);
backupDataOutput.writeEntityData(pair.value, pair.value.length);
}
}
}

View File

@@ -1,32 +0,0 @@
/*
* 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;
/** Wraps any exception related to encryption which occurs during restore. */
public class EncryptedRestoreException extends Exception {
public EncryptedRestoreException(String message) {
super(message);
}
public EncryptedRestoreException(Throwable cause) {
super(cause);
}
public EncryptedRestoreException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -1,87 +0,0 @@
/*
* 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 com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.FullRestoreDownloader;
import com.android.server.backup.encryption.FullRestoreDownloader.FinishType;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Reads a stream from a {@link FullRestoreDownloader} and writes it to a file for consumption by
* {@link BackupFileDecryptorTask}.
*/
public class FullRestoreToFileTask {
/**
* Maximum number of bytes which the framework can request from the full restore data stream in
* one call to {@link BackupTransport#getNextFullRestoreDataChunk}.
*/
public static final int MAX_BYTES_FULL_RESTORE_CHUNK = 1024 * 32;
/** Returned when the end of a backup stream has been reached. */
private static final int END_OF_STREAM = -1;
private final FullRestoreDownloader mFullRestoreDownloader;
private final int mBufferSize;
/**
* Constructs a new instance which reads from the given package wrapper, using a buffer of size
* {@link #MAX_BYTES_FULL_RESTORE_CHUNK}.
*/
public FullRestoreToFileTask(FullRestoreDownloader fullRestoreDownloader) {
this(fullRestoreDownloader, MAX_BYTES_FULL_RESTORE_CHUNK);
}
@VisibleForTesting
FullRestoreToFileTask(FullRestoreDownloader fullRestoreDownloader, int bufferSize) {
checkArgument(bufferSize > 0, "Buffer must have positive size");
this.mFullRestoreDownloader = fullRestoreDownloader;
this.mBufferSize = bufferSize;
}
/**
* Downloads the backup file from the server and writes it to the given file.
*
* <p>At the end of the download (success or failure), closes the connection and sends a
* Clearcut log.
*/
public void restoreToFile(File targetFile) throws IOException {
try (BufferedOutputStream outputStream =
new BufferedOutputStream(new FileOutputStream(targetFile))) {
byte[] buffer = new byte[mBufferSize];
int bytesRead = mFullRestoreDownloader.readNextChunk(buffer);
while (bytesRead != END_OF_STREAM) {
outputStream.write(buffer, /* off=*/ 0, bytesRead);
bytesRead = mFullRestoreDownloader.readNextChunk(buffer);
}
outputStream.flush();
mFullRestoreDownloader.finish(FinishType.FINISHED);
} catch (IOException e) {
mFullRestoreDownloader.finish(FinishType.TRANSFER_FAILURE);
throw e;
}
}
}

View File

@@ -1,157 +0,0 @@
/*
* 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.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.security.keystore.recovery.RecoveryController;
import android.util.Slog;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import java.security.InvalidKeyException;
import java.security.UnrecoverableKeyException;
import java.util.Collections;
import java.util.Optional;
/**
* Initializes the device for encrypted backup, through generating a secondary key, and setting its
* alias in the settings.
*
* <p>If the device is already initialized, this is a no-op.
*/
public class InitializeRecoverableSecondaryKeyTask {
private static final String TAG = "InitializeRecoverableSecondaryKeyTask";
private final Context mContext;
private final CryptoSettings mCryptoSettings;
private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
private final CryptoBackupServer mBackupServer;
/**
* A new instance.
*
* @param cryptoSettings Settings to store the active key alias.
* @param secondaryKeyManager Key manager to generate the new active secondary key.
* @param backupServer Server with which to sync the active key alias.
*/
public InitializeRecoverableSecondaryKeyTask(
Context context,
CryptoSettings cryptoSettings,
RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
CryptoBackupServer backupServer) {
mContext = context;
mCryptoSettings = cryptoSettings;
mSecondaryKeyManager = secondaryKeyManager;
mBackupServer = backupServer;
}
/**
* Initializes the device for encrypted backup, by generating a recoverable secondary key, then
* sending that alias to the backup server and saving it in local settings.
*
* <p>If there is already an active secondary key then does nothing. If the active secondary key
* is destroyed then throws {@link InvalidKeyException}.
*
* <p>If a key rotation is pending and able to finish (i.e., the new key has synced with the
* remote trusted hardware module), then it completes the rotation before returning the key.
*
* @return The active secondary key.
* @throws InvalidKeyException if the secondary key is in a bad state.
*/
public RecoverableKeyStoreSecondaryKey run()
throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
InternalRecoveryServiceException {
// Complete any pending key rotations
new RotateSecondaryKeyTask(
mContext,
mSecondaryKeyManager,
mBackupServer,
mCryptoSettings,
RecoveryController.getInstance(mContext))
.run();
return runInternal();
}
private RecoverableKeyStoreSecondaryKey runInternal()
throws InvalidKeyException, LockScreenRequiredException, UnrecoverableKeyException,
InternalRecoveryServiceException {
Optional<RecoverableKeyStoreSecondaryKey> maybeActiveKey = loadFromSetting();
if (maybeActiveKey.isPresent()) {
assertKeyNotDestroyed(maybeActiveKey.get());
Slog.d(TAG, "Secondary key already initialized: " + maybeActiveKey.get().getAlias());
return maybeActiveKey.get();
}
Slog.v(TAG, "Initializing for crypto: generating a secondary key.");
RecoverableKeyStoreSecondaryKey key = mSecondaryKeyManager.generate();
String alias = key.getAlias();
Slog.i(TAG, "Generated new secondary key " + alias);
// No tertiary keys yet as we are creating a brand new secondary (without rotation).
mBackupServer.setActiveSecondaryKeyAlias(alias, /*tertiaryKeys=*/ Collections.emptyMap());
Slog.v(TAG, "Successfully synced %s " + alias + " with server.");
mCryptoSettings.initializeWithKeyAlias(alias);
Slog.v(TAG, "Successfully saved " + alias + " as active secondary to disk.");
return key;
}
private void assertKeyNotDestroyed(RecoverableKeyStoreSecondaryKey key)
throws InvalidKeyException {
if (key.getStatus(mContext) == RecoverableKeyStoreSecondaryKey.Status.DESTROYED) {
throw new InvalidKeyException("Key destroyed: " + key.getAlias());
}
}
private Optional<RecoverableKeyStoreSecondaryKey> loadFromSetting()
throws InvalidKeyException, UnrecoverableKeyException,
InternalRecoveryServiceException {
// TODO: b/141856950.
if (!mCryptoSettings.getIsInitialized()) {
return Optional.empty();
}
Optional<String> maybeAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
if (!maybeAlias.isPresent()) {
throw new InvalidKeyException(
"Settings said crypto was initialized, but there was no active secondary"
+ " alias");
}
String alias = maybeAlias.get();
Optional<RecoverableKeyStoreSecondaryKey> key;
key = mSecondaryKeyManager.get(alias);
if (!key.isPresent()) {
throw new InvalidKeyException(
"Initialized with key but it was not in key store: " + alias);
}
return key;
}
}

View File

@@ -1,179 +0,0 @@
/*
* 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.checkState;
import android.annotation.Nullable;
import android.app.backup.BackupDataInput;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.ChunkEncryptor;
import com.android.server.backup.encryption.chunking.ChunkHasher;
import com.android.server.backup.encryption.chunking.EncryptedChunk;
import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
/**
* Reads key value backup data from an input, converts each pair into a chunk and encrypts the
* chunks.
*
* <p>The caller should pass in the key value listing from the previous backup, if there is one.
* This class emits chunks for both existing and new pairs, using the provided listing to
* determine the hashes of pairs that already exist. During the backup it computes the new listing,
* which the caller should store on disk and pass in at the start of the next backup.
*
* <p>Also computes the message digest, which is {@code SHA-256(chunk hashes sorted
* lexicographically)}.
*/
public class KvBackupEncrypter implements BackupEncrypter {
private final BackupDataInput mBackupDataInput;
private KeyValueListingProto.KeyValueListing mOldKeyValueListing;
@Nullable private KeyValueListingBuilder mNewKeyValueListing;
/**
* Constructs a new instance which reads data from the given input.
*
* <p>By default this performs non-incremental backup, call {@link #setOldKeyValueListing} to
* perform incremental backup.
*/
public KvBackupEncrypter(BackupDataInput backupDataInput) {
mBackupDataInput = backupDataInput;
mOldKeyValueListing = KeyValueListingBuilder.emptyListing();
}
/** Sets the old listing to perform incremental backup against. */
public void setOldKeyValueListing(KeyValueListingProto.KeyValueListing oldKeyValueListing) {
mOldKeyValueListing = oldKeyValueListing;
}
@Override
public Result backup(
SecretKey secretKey,
@Nullable byte[] unusedFingerprintMixerSalt,
Set<ChunkHash> unusedExistingChunks)
throws IOException, GeneralSecurityException {
ChunkHasher chunkHasher = new ChunkHasher(secretKey);
ChunkEncryptor chunkEncryptor = new ChunkEncryptor(secretKey, new SecureRandom());
mNewKeyValueListing = new KeyValueListingBuilder();
List<ChunkHash> allChunks = new ArrayList<>();
List<EncryptedChunk> newChunks = new ArrayList<>();
Map<String, ChunkHash> existingChunksToReuse = buildPairMap(mOldKeyValueListing);
while (mBackupDataInput.readNextHeader()) {
String key = mBackupDataInput.getKey();
Optional<byte[]> value = readEntireValue(mBackupDataInput);
// As this pair exists in the new backup, we don't need to add it from the previous
// backup.
existingChunksToReuse.remove(key);
// If the value is not present then this key has been deleted.
if (value.isPresent()) {
EncryptedChunk newChunk =
createEncryptedChunk(chunkHasher, chunkEncryptor, key, value.get());
allChunks.add(newChunk.key());
newChunks.add(newChunk);
mNewKeyValueListing.addPair(key, newChunk.key());
}
}
allChunks.addAll(existingChunksToReuse.values());
mNewKeyValueListing.addAll(existingChunksToReuse);
return new Result(allChunks, newChunks, createMessageDigest(allChunks));
}
/**
* Returns a listing containing the pairs in the new backup.
*
* <p>You must call {@link #backup} first.
*/
public KeyValueListingProto.KeyValueListing getNewKeyValueListing() {
checkState(mNewKeyValueListing != null, "Must call backup() first");
return mNewKeyValueListing.build();
}
private static Map<String, ChunkHash> buildPairMap(
KeyValueListingProto.KeyValueListing listing) {
Map<String, ChunkHash> map = new HashMap<>();
for (KeyValueListingProto.KeyValueEntry entry : listing.entries) {
map.put(entry.key, new ChunkHash(entry.hash));
}
return map;
}
private EncryptedChunk createEncryptedChunk(
ChunkHasher chunkHasher, ChunkEncryptor chunkEncryptor, String key, byte[] value)
throws InvalidKeyException, IllegalBlockSizeException {
KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair();
pair.key = key;
pair.value = Arrays.copyOf(value, value.length);
byte[] plaintext = KeyValuePairProto.KeyValuePair.toByteArray(pair);
return chunkEncryptor.encrypt(chunkHasher.computeHash(plaintext), plaintext);
}
private static byte[] createMessageDigest(List<ChunkHash> allChunks)
throws NoSuchAlgorithmException {
MessageDigest messageDigest =
MessageDigest.getInstance(BackupEncrypter.MESSAGE_DIGEST_ALGORITHM);
// TODO:b/141531271 Extract sorted chunks code to utility class
List<ChunkHash> sortedChunks = new ArrayList<>(allChunks);
Collections.sort(sortedChunks);
for (ChunkHash hash : sortedChunks) {
messageDigest.update(hash.getHash());
}
return messageDigest.digest();
}
private static Optional<byte[]> readEntireValue(BackupDataInput input) throws IOException {
// A negative data size indicates that this key should be deleted.
if (input.getDataSize() < 0) {
return Optional.empty();
}
byte[] value = new byte[input.getDataSize()];
int bytesRead = 0;
while (bytesRead < value.length) {
bytesRead += input.readEntityData(value, bytesRead, value.length - bytesRead);
}
return Optional.of(value);
}
}

View File

@@ -1,24 +0,0 @@
/*
* 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 we cannot parse the encrypted backup file. */
public class MalformedEncryptedFileException extends EncryptedRestoreException {
public MalformedEncryptedFileException(String message) {
super(message);
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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;
/**
* Error thrown if the message digest of the plaintext backup does not match that in the {@link
* com.android.server.backup.encryption.protos.ChunksMetadataProto.ChunkOrdering}.
*/
public class MessageDigestMismatchException extends EncryptedRestoreException {
public MessageDigestMismatchException(String message) {
super(message);
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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;
/**
* Error thrown if attempting to rotate key when there is no current active secondary key set
* locally. This means the device needs to re-initialize, asking the backup server what the active
* secondary key is.
*/
public class NoActiveSecondaryKeyException extends Exception {
public NoActiveSecondaryKeyException(String message) {
super(message);
}
}

View File

@@ -1,25 +0,0 @@
/*
* 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;
// TODO(141840878): Update documentation.
/**
* Exception thrown when the framework provides an incremental backup but the transport requires a
* non-incremental backup.
*/
public class NonIncrementalBackupRequiredException extends Exception {}

View File

@@ -1,269 +0,0 @@
/*
* 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 android.os.Build.VERSION_CODES.P;
import android.content.Context;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.RecoveryController;
import android.util.Slog;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.KeyWrapUtils;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyStore;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
/**
* Finishes a rotation for a {@link
* com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey}.
*/
public class RotateSecondaryKeyTask {
private static final String TAG = "RotateSecondaryKeyTask";
private final Context mContext;
private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
private final CryptoBackupServer mBackupServer;
private final CryptoSettings mCryptoSettings;
private final RecoveryController mRecoveryController;
/**
* A new instance.
*
* @param secondaryKeyManager For loading the currently active and next secondary key.
* @param backupServer For loading and storing tertiary keys and for setting active secondary
* key.
* @param cryptoSettings For checking the stored aliases for the next and active key.
* @param recoveryController For communicating with the Framework apis.
*/
public RotateSecondaryKeyTask(
Context context,
RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager,
CryptoBackupServer backupServer,
CryptoSettings cryptoSettings,
RecoveryController recoveryController) {
mContext = context;
mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager);
mCryptoSettings = Objects.requireNonNull(cryptoSettings);
mBackupServer = Objects.requireNonNull(backupServer);
mRecoveryController = Objects.requireNonNull(recoveryController);
}
/** Runs the task. */
public void run() {
// Never run more than one of these at the same time.
synchronized (RotateSecondaryKeyTask.class) {
runInternal();
}
}
private void runInternal() {
Optional<RecoverableKeyStoreSecondaryKey> maybeNextKey;
try {
maybeNextKey = getNextKey();
} catch (Exception e) {
Slog.e(TAG, "Error checking for next key", e);
return;
}
if (!maybeNextKey.isPresent()) {
Slog.d(TAG, "No secondary key rotation task pending. Exiting.");
return;
}
RecoverableKeyStoreSecondaryKey nextKey = maybeNextKey.get();
boolean isReady;
try {
isReady = isSecondaryKeyRotationReady(nextKey);
} catch (InternalRecoveryServiceException e) {
Slog.e(TAG, "Error encountered checking whether next secondary key is synced", e);
return;
}
if (!isReady) {
return;
}
try {
rotateToKey(nextKey);
} catch (Exception e) {
Slog.e(TAG, "Error trying to rotate to new secondary key", e);
}
}
private Optional<RecoverableKeyStoreSecondaryKey> getNextKey()
throws InternalRecoveryServiceException, UnrecoverableKeyException {
Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
if (!maybeNextAlias.isPresent()) {
return Optional.empty();
}
return mSecondaryKeyManager.get(maybeNextAlias.get());
}
private boolean isSecondaryKeyRotationReady(RecoverableKeyStoreSecondaryKey nextKey)
throws InternalRecoveryServiceException {
String nextAlias = nextKey.getAlias();
Slog.i(TAG, "Key rotation to " + nextAlias + " is pending. Checking key sync status.");
int status = mRecoveryController.getRecoveryStatus(nextAlias);
if (status == RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE) {
Slog.e(
TAG,
"Permanent failure to sync " + nextAlias + ". Cannot possibly rotate to it.");
mCryptoSettings.removeNextSecondaryKeyAlias();
return false;
}
if (status == RecoveryController.RECOVERY_STATUS_SYNCED) {
Slog.i(TAG, "Secondary key " + nextAlias + " has now synced! Commencing rotation.");
} else {
Slog.i(TAG, "Sync still pending for " + nextAlias);
}
return status == RecoveryController.RECOVERY_STATUS_SYNCED;
}
/**
* @throws ActiveSecondaryNotInKeychainException if the currently active secondary key is not in
* the keychain.
* @throws IOException if there is an IO issue communicating with the server or loading from
* disk.
* @throws NoActiveSecondaryKeyException if there is no active key set.
* @throws IllegalBlockSizeException if there is an issue decrypting a tertiary key.
* @throws InvalidKeyException if any of the secondary keys cannot be used for wrapping or
* unwrapping tertiary keys.
*/
private void rotateToKey(RecoverableKeyStoreSecondaryKey newSecondaryKey)
throws ActiveSecondaryNotInKeychainException, IOException,
NoActiveSecondaryKeyException, IllegalBlockSizeException, InvalidKeyException,
InternalRecoveryServiceException, UnrecoverableKeyException,
InvalidAlgorithmParameterException, NoSuchAlgorithmException,
NoSuchPaddingException {
RecoverableKeyStoreSecondaryKey activeSecondaryKey = getActiveSecondaryKey();
String activeSecondaryKeyAlias = activeSecondaryKey.getAlias();
String newSecondaryKeyAlias = newSecondaryKey.getAlias();
if (newSecondaryKeyAlias.equals(activeSecondaryKeyAlias)) {
Slog.i(TAG, activeSecondaryKeyAlias + " was already the active alias.");
return;
}
TertiaryKeyStore tertiaryKeyStore =
TertiaryKeyStore.newInstance(mContext, activeSecondaryKey);
Map<String, SecretKey> tertiaryKeys = tertiaryKeyStore.getAll();
if (tertiaryKeys.isEmpty()) {
Slog.i(
TAG,
"No tertiary keys for " + activeSecondaryKeyAlias + ". No need to rewrap. ");
mBackupServer.setActiveSecondaryKeyAlias(
newSecondaryKeyAlias, /*tertiaryKeys=*/ Collections.emptyMap());
} else {
Map<String, WrappedKeyProto.WrappedKey> rewrappedTertiaryKeys =
rewrapAll(newSecondaryKey, tertiaryKeys);
TertiaryKeyStore.newInstance(mContext, newSecondaryKey).putAll(rewrappedTertiaryKeys);
Slog.i(
TAG,
"Successfully rewrapped " + rewrappedTertiaryKeys.size() + " tertiary keys");
mBackupServer.setActiveSecondaryKeyAlias(newSecondaryKeyAlias, rewrappedTertiaryKeys);
Slog.i(
TAG,
"Successfully uploaded new set of tertiary keys to "
+ newSecondaryKeyAlias
+ " alias");
}
mCryptoSettings.setActiveSecondaryKeyAlias(newSecondaryKeyAlias);
mCryptoSettings.removeNextSecondaryKeyAlias();
try {
mRecoveryController.removeKey(activeSecondaryKeyAlias);
} catch (InternalRecoveryServiceException e) {
Slog.e(TAG, "Error removing old secondary key from RecoverableKeyStoreLoader", e);
}
}
private RecoverableKeyStoreSecondaryKey getActiveSecondaryKey()
throws NoActiveSecondaryKeyException, ActiveSecondaryNotInKeychainException,
InternalRecoveryServiceException, UnrecoverableKeyException {
Optional<String> activeSecondaryAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
if (!activeSecondaryAlias.isPresent()) {
Slog.i(
TAG,
"Was asked to rotate secondary key, but local config did not have a secondary "
+ "key alias set.");
throw new NoActiveSecondaryKeyException("No local active secondary key set.");
}
String activeSecondaryKeyAlias = activeSecondaryAlias.get();
Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
mSecondaryKeyManager.get(activeSecondaryKeyAlias);
if (!secondaryKey.isPresent()) {
throw new ActiveSecondaryNotInKeychainException(
String.format(
Locale.US,
"Had local active recoverable key alias of %s but key was not in"
+ " user's keychain.",
activeSecondaryKeyAlias));
}
return secondaryKey.get();
}
/**
* Rewraps all the tertiary keys.
*
* @param newSecondaryKey The secondary key with which to rewrap the tertiaries.
* @param tertiaryKeys The tertiary keys, by package name.
* @return The newly wrapped tertiary keys, by package name.
* @throws InvalidKeyException if any key is unusable.
* @throws IllegalBlockSizeException if could not decrypt.
*/
private Map<String, WrappedKeyProto.WrappedKey> rewrapAll(
RecoverableKeyStoreSecondaryKey newSecondaryKey, Map<String, SecretKey> tertiaryKeys)
throws InvalidKeyException, IllegalBlockSizeException, NoSuchPaddingException,
NoSuchAlgorithmException {
Map<String, WrappedKeyProto.WrappedKey> wrappedKeys = new HashMap<>();
for (String packageName : tertiaryKeys.keySet()) {
SecretKey tertiaryKey = tertiaryKeys.get(packageName);
wrappedKeys.put(
packageName, KeyWrapUtils.wrap(newSecondaryKey.getSecretKey(), tertiaryKey));
}
return wrappedKeys;
}
}

View File

@@ -1,24 +0,0 @@
/*
* 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.");
}
}

View File

@@ -1,105 +0,0 @@
/*
* 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.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.LockScreenRequiredException;
import android.util.Slog;
import com.android.internal.util.Preconditions;
import com.android.server.backup.encryption.CryptoSettings;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import java.security.UnrecoverableKeyException;
import java.util.Objects;
import java.util.Optional;
/**
* Starts rotating to a new secondary key. Cannot complete until the screen is unlocked and the new
* key is synced.
*/
public class StartSecondaryKeyRotationTask {
private static final String TAG = "BE-StSecondaryKeyRotTsk";
private final CryptoSettings mCryptoSettings;
private final RecoverableKeyStoreSecondaryKeyManager mSecondaryKeyManager;
public StartSecondaryKeyRotationTask(
CryptoSettings cryptoSettings,
RecoverableKeyStoreSecondaryKeyManager secondaryKeyManager) {
mCryptoSettings = Objects.requireNonNull(cryptoSettings);
mSecondaryKeyManager = Objects.requireNonNull(secondaryKeyManager);
}
/** Begin the key rotation */
public void run() {
Slog.i(TAG, "Attempting to initiate a secondary key rotation.");
Optional<String> maybeCurrentAlias = mCryptoSettings.getActiveSecondaryKeyAlias();
if (!maybeCurrentAlias.isPresent()) {
Slog.w(TAG, "No active current alias. Cannot trigger a secondary rotation.");
return;
}
String currentAlias = maybeCurrentAlias.get();
Optional<String> maybeNextAlias = mCryptoSettings.getNextSecondaryKeyAlias();
if (maybeNextAlias.isPresent()) {
String nextAlias = maybeNextAlias.get();
if (nextAlias.equals(currentAlias)) {
// Shouldn't be possible, but guard against accidentally deleting the active key.
Slog.e(TAG, "Was already trying to rotate to what is already the active key.");
} else {
Slog.w(TAG, "Was already rotating to another key. Cancelling that.");
try {
mSecondaryKeyManager.remove(nextAlias);
} catch (Exception e) {
Slog.wtf(TAG, "Could not remove old key", e);
}
}
mCryptoSettings.removeNextSecondaryKeyAlias();
}
RecoverableKeyStoreSecondaryKey newSecondaryKey;
try {
newSecondaryKey = mSecondaryKeyManager.generate();
} catch (LockScreenRequiredException e) {
Slog.e(TAG, "No lock screen is set - cannot generate a new key to rotate to.", e);
return;
} catch (InternalRecoveryServiceException e) {
Slog.e(TAG, "Internal error in Recovery Controller, failed to rotate key.", e);
return;
} catch (UnrecoverableKeyException e) {
Slog.e(TAG, "Failed to get key after generating, failed to rotate", e);
return;
}
String alias = newSecondaryKey.getAlias();
Slog.i(TAG, "Generated a new secondary key with alias '" + alias + "'.");
try {
mCryptoSettings.setNextSecondaryAlias(alias);
Slog.i(TAG, "Successfully set '" + alias + "' as next key to rotate to");
} catch (IllegalArgumentException e) {
Slog.e(TAG, "Unexpected error setting next alias", e);
try {
mSecondaryKeyManager.remove(alias);
} catch (Exception err) {
Slog.wtf(TAG, "Failed to remove generated key after encountering error", err);
}
}
}
}

View File

@@ -1,28 +0,0 @@
/*
* 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;
/**
* Thrown when the backup file provided by the server uses encryption algorithms this version of
* backup does not support. This could happen if the backup was created with a newer version of the
* code.
*/
public class UnsupportedEncryptedFileException extends EncryptedRestoreException {
public UnsupportedEncryptedFileException(String message) {
super(message);
}
}

View File

@@ -1,43 +0,0 @@
// 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_robolectric_test {
name: "BackupEncryptionRoboIntegTests",
srcs: [
"src/**/*.java",
],
java_resource_dirs: ["config"],
libs: [
"backup-encryption-protos",
"platform-test-annotations",
"testng",
"truth-prebuilt",
"BackupEncryptionRoboTests",
],
static_libs: [
"androidx.test.core",
"androidx.test.runner",
"androidx.test.rules",
],
instrumentation_for: "BackupEncryption",
}

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
coreApp="true"
package="com.android.server.backup.encryption.robointeg">
<application/>
</manifest>

View File

@@ -1,17 +0,0 @@
#
# 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.
#
sdk=NEWEST_SDK

View File

@@ -1,321 +0,0 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.os.ParcelFileDescriptor;
import android.security.keystore.recovery.InternalRecoveryServiceException;
import android.security.keystore.recovery.RecoveryController;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.backup.encryption.client.CryptoBackupServer;
import com.android.server.backup.encryption.keys.KeyWrapUtils;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.keys.TertiaryKeyRotationScheduler;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import com.android.server.backup.encryption.tasks.EncryptedFullBackupTask;
import com.android.server.backup.encryption.tasks.EncryptedFullRestoreTask;
import com.android.server.backup.encryption.tasks.EncryptedKvBackupTask;
import com.android.server.backup.encryption.tasks.EncryptedKvRestoreTask;
import com.android.server.testing.shadows.DataEntity;
import com.android.server.testing.shadows.ShadowBackupDataInput;
import com.android.server.testing.shadows.ShadowBackupDataOutput;
import com.android.server.testing.shadows.ShadowRecoveryController;
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Optional;
import java.util.Map;
import java.util.Set;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
@Config(
shadows = {
ShadowBackupDataInput.class,
ShadowBackupDataOutput.class,
ShadowRecoveryController.class
})
@RunWith(RobolectricTestRunner.class)
public class RoundTripTest {
private static final DataEntity[] KEY_VALUE_DATA = {
new DataEntity("test_key_1", "test_value_1"),
new DataEntity("test_key_2", "test_value_2"),
new DataEntity("test_key_3", "test_value_3")
};
/** Amount of data we want to round trip in this test */
private static final int TEST_DATA_SIZE = 1024 * 1024; // 1MB
/** Buffer size used when reading data from the restore task */
private static final int READ_BUFFER_SIZE = 1024; // 1024 byte buffer.
/** Key parameters used for the secondary encryption key */
private static final String KEY_ALGORITHM = "AES";
private static final int KEY_SIZE_BITS = 256;
/** Package name for our test package */
private static final String TEST_PACKAGE_NAME = "com.android.backup.test";
/** The name we use to refer to our secondary key */
private static final String TEST_KEY_ALIAS = "test/backup/KEY_ALIAS";
/** Original data used for comparison after round trip */
private final byte[] mOriginalData = new byte[TEST_DATA_SIZE];
/** App context, used to store the key data and chunk listings */
private Context mContext;
/** The secondary key we're using for the test */
private RecoverableKeyStoreSecondaryKey mSecondaryKey;
/** Source of random material which is considered non-predictable in its' generation */
private final SecureRandom mSecureRandom = new SecureRandom();
private RecoverableKeyStoreSecondaryKeyManager.RecoverableKeyStoreSecondaryKeyManagerProvider
mSecondaryKeyManagerProvider;
private DummyServer mDummyServer;
private RecoveryController mRecoveryController;
@Mock private ParcelFileDescriptor mParcelFileDescriptor;
@Before
public void setUp() throws NoSuchAlgorithmException, InternalRecoveryServiceException {
MockitoAnnotations.initMocks(this);
ShadowBackupDataInput.reset();
ShadowBackupDataOutput.reset();
mContext = ApplicationProvider.getApplicationContext();
mSecondaryKey = new RecoverableKeyStoreSecondaryKey(TEST_KEY_ALIAS, generateAesKey());
mDummyServer = new DummyServer();
mSecondaryKeyManagerProvider =
() ->
new RecoverableKeyStoreSecondaryKeyManager(
RecoveryController.getInstance(mContext), mSecureRandom);
fillBuffer(mOriginalData);
}
@Test
public void testFull_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
byte[] backupData = performFullBackup(mOriginalData);
assertThat(backupData).isNotEqualTo(mOriginalData);
byte[] restoredData = performFullRestore(backupData);
assertThat(restoredData).isEqualTo(mOriginalData);
}
@Test
public void testKeyValue_nonIncrementalBackupAndRestoreAreSuccessful() throws Exception {
byte[] backupData = performNonIncrementalKeyValueBackup(KEY_VALUE_DATA);
// Get the secondary key used to do backup.
Optional<RecoverableKeyStoreSecondaryKey> secondaryKey =
mSecondaryKeyManagerProvider.get().get(mDummyServer.mSecondaryKeyAlias);
assertThat(secondaryKey.isPresent()).isTrue();
Set<DataEntity> restoredData = performKeyValueRestore(backupData, secondaryKey.get());
assertThat(restoredData).containsExactly(KEY_VALUE_DATA).inOrder();
}
/** Perform a key/value backup and return the backed-up representation of the data */
private byte[] performNonIncrementalKeyValueBackup(DataEntity[] backupData)
throws Exception {
// Populate test key/value data.
for (DataEntity entity : backupData) {
ShadowBackupDataInput.addEntity(entity);
}
EncryptedKvBackupTask.EncryptedKvBackupTaskFactory backupTaskFactory =
new EncryptedKvBackupTask.EncryptedKvBackupTaskFactory();
EncryptedKvBackupTask backupTask =
backupTaskFactory.newInstance(
mContext,
mSecureRandom,
mDummyServer,
CryptoSettings.getInstance(mContext),
mSecondaryKeyManagerProvider,
mParcelFileDescriptor,
TEST_PACKAGE_NAME);
backupTask.performBackup(/* incremental */ false);
return mDummyServer.mStoredData;
}
/** Perform a full backup and return the backed-up representation of the data */
private byte[] performFullBackup(byte[] backupData) throws Exception {
DummyServer dummyServer = new DummyServer();
EncryptedFullBackupTask backupTask =
EncryptedFullBackupTask.newInstance(
mContext,
dummyServer,
mSecureRandom,
mSecondaryKey,
TEST_PACKAGE_NAME,
new ByteArrayInputStream(backupData));
backupTask.call();
return dummyServer.mStoredData;
}
private Set<DataEntity> performKeyValueRestore(
byte[] backupData, RecoverableKeyStoreSecondaryKey secondaryKey) throws Exception {
EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory restoreTaskFactory =
new EncryptedKvRestoreTask.EncryptedKvRestoreTaskFactory();
EncryptedKvRestoreTask restoreTask =
restoreTaskFactory.newInstance(
mContext,
mSecondaryKeyManagerProvider,
new FakeFullRestoreDownloader(backupData),
secondaryKey.getAlias(),
KeyWrapUtils.wrap(
secondaryKey.getSecretKey(), getTertiaryKey(secondaryKey)));
restoreTask.getRestoreData(mParcelFileDescriptor);
return ShadowBackupDataOutput.getEntities();
}
/** Perform a full restore and return the bytes obtained from the restore process */
private byte[] performFullRestore(byte[] backupData)
throws IOException, NoSuchAlgorithmException, NoSuchPaddingException,
InvalidAlgorithmParameterException, InvalidKeyException,
IllegalBlockSizeException {
ByteArrayOutputStream decryptedOutput = new ByteArrayOutputStream();
EncryptedFullRestoreTask restoreTask =
EncryptedFullRestoreTask.newInstance(
mContext,
new FakeFullRestoreDownloader(backupData),
getTertiaryKey(mSecondaryKey));
byte[] buffer = new byte[READ_BUFFER_SIZE];
int bytesRead = restoreTask.readNextChunk(buffer);
while (bytesRead != -1) {
decryptedOutput.write(buffer, 0, bytesRead);
bytesRead = restoreTask.readNextChunk(buffer);
}
return decryptedOutput.toByteArray();
}
/** Get the tertiary key for our test package from the key manager */
private SecretKey getTertiaryKey(RecoverableKeyStoreSecondaryKey secondaryKey)
throws IllegalBlockSizeException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, IOException, NoSuchPaddingException,
InvalidKeyException {
TertiaryKeyManager tertiaryKeyManager =
new TertiaryKeyManager(
mContext,
mSecureRandom,
TertiaryKeyRotationScheduler.getInstance(mContext),
secondaryKey,
TEST_PACKAGE_NAME);
return tertiaryKeyManager.getKey();
}
/** Fill a buffer with data in a predictable way */
private void fillBuffer(byte[] buffer) {
byte loopingCounter = 0;
for (int i = 0; i < buffer.length; i++) {
buffer[i] = loopingCounter;
loopingCounter++;
}
}
/** Generate a new, random, AES key */
public static SecretKey generateAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
keyGenerator.init(KEY_SIZE_BITS);
return keyGenerator.generateKey();
}
/**
* Dummy backup data endpoint. This stores the data so we can use it in subsequent test steps.
*/
private static class DummyServer implements CryptoBackupServer {
private static final String DUMMY_DOC_ID = "DummyDoc";
byte[] mStoredData = null;
String mSecondaryKeyAlias;
@Override
public String uploadIncrementalBackup(
String packageName,
String oldDocId,
byte[] diffScript,
WrappedKeyProto.WrappedKey tertiaryKey) {
throw new RuntimeException("Not Implemented");
}
@Override
public String uploadNonIncrementalBackup(
String packageName, byte[] data, WrappedKeyProto.WrappedKey tertiaryKey) {
assertThat(packageName).isEqualTo(TEST_PACKAGE_NAME);
mStoredData = data;
return DUMMY_DOC_ID;
}
@Override
public void setActiveSecondaryKeyAlias(
String keyAlias, Map<String, WrappedKeyProto.WrappedKey> tertiaryKeys) {
mSecondaryKeyAlias = keyAlias;
}
}
/** 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) {
// Do nothing.
}
}
}

View File

@@ -1,48 +0,0 @@
// Copyright (C) 2018 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 {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_robolectric_test {
name: "BackupEncryptionRoboTests",
srcs: [
"src/**/*.java",
// ":FrameworksServicesRoboShadows",
],
java_resource_dirs: ["config"],
libs: [
"backup-encryption-protos",
"platform-test-annotations",
"testng",
"truth-prebuilt",
],
static_libs: [
"androidx.test.core",
"androidx.test.runner",
"androidx.test.rules",
],
instrumentation_for: "BackupEncryption",
}
filegroup {
name: "BackupEncryptionRoboShadows",
srcs: ["src/com/android/server/testing/shadows/**/*.java"],
}

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
coreApp="true"
package="com.android.server.backup.encryption.robotests">
<application/>
</manifest>

View File

@@ -1,17 +0,0 @@
#
# 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.
#
sdk=NEWEST_SDK

View File

@@ -1,212 +0,0 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import static org.testng.Assert.assertThrows;
import android.app.Application;
import android.security.keystore.recovery.RecoveryController;
import androidx.test.core.app.ApplicationProvider;
import com.android.server.testing.shadows.ShadowRecoveryController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.util.Optional;
@RunWith(RobolectricTestRunner.class)
@Config(shadows = ShadowRecoveryController.class)
public class CryptoSettingsTest {
private static final String TEST_KEY_ALIAS =
"com.android.server.backup.encryption/keystore/08120c326b928ff34c73b9c58581da63";
private CryptoSettings mCryptoSettings;
private Application mApplication;
@Before
public void setUp() {
ShadowRecoveryController.reset();
mApplication = ApplicationProvider.getApplicationContext();
mCryptoSettings = CryptoSettings.getInstanceForTesting(mApplication);
}
@Test
public void getActiveSecondaryAlias_isInitiallyAbsent() {
assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse();
}
@Test
public void getActiveSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS);
assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
}
@Test
public void getNextSecondaryAlias_isInitiallyAbsent() {
assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
}
@Test
public void getNextSecondaryAlias_returnsAliasIfKeyIsInRecoveryController() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
}
@Test
public void isInitialized_isInitiallyFalse() {
assertThat(mCryptoSettings.getIsInitialized()).isFalse();
}
@Test
public void setActiveSecondaryAlias_throwsIfKeyIsNotInRecoveryController() {
assertThrows(
IllegalArgumentException.class,
() -> mCryptoSettings.setActiveSecondaryKeyAlias(TEST_KEY_ALIAS));
}
@Test
public void setNextSecondaryAlias_inRecoveryController_setsAlias() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
assertThat(mCryptoSettings.getNextSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
}
@Test
public void setNextSecondaryAlias_throwsIfKeyIsNotInRecoveryController() {
assertThrows(
IllegalArgumentException.class,
() -> mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS));
}
@Test
public void removeNextSecondaryAlias_removesIt() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.setNextSecondaryAlias(TEST_KEY_ALIAS);
mCryptoSettings.removeNextSecondaryKeyAlias();
assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
}
@Test
public void initializeWithKeyAlias_setsAsInitialized() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
assertThat(mCryptoSettings.getIsInitialized()).isTrue();
}
@Test
public void initializeWithKeyAlias_setsActiveAlias() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().get()).isEqualTo(TEST_KEY_ALIAS);
}
@Test
public void initializeWithKeyAlias_throwsIfKeyIsNotInRecoveryController() {
assertThrows(
IllegalArgumentException.class,
() -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS));
}
@Test
public void initializeWithKeyAlias_throwsIfAlreadyInitialized() throws Exception {
setAliasIsInRecoveryController(TEST_KEY_ALIAS);
mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS);
assertThrows(
IllegalStateException.class,
() -> mCryptoSettings.initializeWithKeyAlias(TEST_KEY_ALIAS));
}
@Test
public void getSecondaryLastRotated_returnsEmptyInitially() {
assertThat(mCryptoSettings.getSecondaryLastRotated()).isEqualTo(Optional.empty());
}
@Test
public void getSecondaryLastRotated_returnsTimestampAfterItIsSet() {
long timestamp = 1000001;
mCryptoSettings.setSecondaryLastRotated(timestamp);
assertThat(mCryptoSettings.getSecondaryLastRotated().get()).isEqualTo(timestamp);
}
@Test
public void getAncestralSecondaryKeyVersion_notSet_returnsOptionalAbsent() {
assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().isPresent()).isFalse();
}
@Test
public void getAncestralSecondaryKeyVersion_isSet_returnsSetValue() {
String secondaryKeyVersion = "some_secondary_key";
mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion);
assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get())
.isEqualTo(secondaryKeyVersion);
}
@Test
public void getAncestralSecondaryKeyVersion_isSetMultipleTimes_returnsLastSetValue() {
String secondaryKeyVersion1 = "some_secondary_key";
String secondaryKeyVersion2 = "another_secondary_key";
mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion1);
mCryptoSettings.setAncestralSecondaryKeyVersion(secondaryKeyVersion2);
assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get())
.isEqualTo(secondaryKeyVersion2);
}
@Test
public void clearAllSettingsForBackup_clearsStateForBackup() throws Exception {
String key1 = "key1";
String key2 = "key2";
String ancestralKey = "ancestral_key";
setAliasIsInRecoveryController(key1);
setAliasIsInRecoveryController(key2);
mCryptoSettings.setActiveSecondaryKeyAlias(key1);
mCryptoSettings.setNextSecondaryAlias(key2);
mCryptoSettings.setSecondaryLastRotated(100001);
mCryptoSettings.setAncestralSecondaryKeyVersion(ancestralKey);
mCryptoSettings.clearAllSettingsForBackup();
assertThat(mCryptoSettings.getActiveSecondaryKeyAlias().isPresent()).isFalse();
assertThat(mCryptoSettings.getNextSecondaryKeyAlias().isPresent()).isFalse();
assertThat(mCryptoSettings.getSecondaryLastRotated().isPresent()).isFalse();
assertThat(mCryptoSettings.getAncestralSecondaryKeyVersion().get()).isEqualTo(ancestralKey);
}
private void setAliasIsInRecoveryController(String alias) throws Exception {
RecoveryController recoveryController = RecoveryController.getInstance(mApplication);
recoveryController.generateKey(alias);
}
}

View File

@@ -1,75 +0,0 @@
/*
* 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 static com.google.common.truth.Truth.assertThat;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
@RunWith(RobolectricTestRunner.class)
public class StreamUtilsTest {
private static final int SOURCE_DATA_SIZE = 64;
private byte[] mSourceData;
private InputStream mSource;
private ByteArrayOutputStream mDestination;
@Before
public void setUp() {
mSourceData = new byte[SOURCE_DATA_SIZE];
for (byte i = 0; i < SOURCE_DATA_SIZE; i++) {
mSourceData[i] = i;
}
mSource = new ByteArrayInputStream(mSourceData);
mDestination = new ByteArrayOutputStream();
}
@Test
public void copyStream_copiesAllBytesIfAsked() throws IOException {
StreamUtils.copyStream(mSource, mDestination, mSourceData.length);
assertOutputHasBytes(mSourceData.length);
}
@Test
public void copyStream_stopsShortIfAsked() throws IOException {
StreamUtils.copyStream(mSource, mDestination, mSourceData.length - 10);
assertOutputHasBytes(mSourceData.length - 10);
}
@Test
public void copyStream_stopsShortIfAskedToCopyMoreThanAvailable() throws IOException {
StreamUtils.copyStream(mSource, mDestination, mSourceData.length + 10);
assertOutputHasBytes(mSourceData.length);
}
private void assertOutputHasBytes(int count) {
byte[] output = mDestination.toByteArray();
assertThat(output.length).isEqualTo(count);
for (int i = 0; i < count; i++) {
assertThat(output[i]).isEqualTo(mSourceData[i]);
}
}
}

View File

@@ -1,100 +0,0 @@
/*
* Copyright (C) 2018 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.chunk;
import static com.google.common.truth.Truth.assertThat;
import android.platform.test.annotations.Presubmit;
import com.google.common.primitives.Bytes;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.Arrays;
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ChunkHashTest {
private static final int HASH_LENGTH_BYTES = 256 / 8;
private static final byte[] TEST_HASH_1 = Arrays.copyOf(new byte[] {1}, HASH_LENGTH_BYTES);
private static final byte[] TEST_HASH_2 = Arrays.copyOf(new byte[] {2}, HASH_LENGTH_BYTES);
@Test
public void testGetHash_returnsHash() {
ChunkHash chunkHash = new ChunkHash(TEST_HASH_1);
byte[] hash = chunkHash.getHash();
assertThat(hash).asList().containsExactlyElementsIn(Bytes.asList(TEST_HASH_1)).inOrder();
}
@Test
public void testEquals() {
ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
ChunkHash equalChunkHash1 = new ChunkHash(TEST_HASH_1);
ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
assertThat(chunkHash1).isEqualTo(equalChunkHash1);
assertThat(chunkHash1).isNotEqualTo(chunkHash2);
}
@Test
public void testHashCode() {
ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
ChunkHash equalChunkHash1 = new ChunkHash(TEST_HASH_1);
ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
int hash1 = chunkHash1.hashCode();
int equalHash1 = equalChunkHash1.hashCode();
int hash2 = chunkHash2.hashCode();
assertThat(hash1).isEqualTo(equalHash1);
assertThat(hash1).isNotEqualTo(hash2);
}
@Test
public void testCompareTo_whenEqual_returnsZero() {
ChunkHash chunkHash = new ChunkHash(TEST_HASH_1);
ChunkHash equalChunkHash = new ChunkHash(TEST_HASH_1);
int result = chunkHash.compareTo(equalChunkHash);
assertThat(result).isEqualTo(0);
}
@Test
public void testCompareTo_whenArgumentGreater_returnsNegative() {
ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
int result = chunkHash1.compareTo(chunkHash2);
assertThat(result).isLessThan(0);
}
@Test
public void testCompareTo_whenArgumentSmaller_returnsPositive() {
ChunkHash chunkHash1 = new ChunkHash(TEST_HASH_1);
ChunkHash chunkHash2 = new ChunkHash(TEST_HASH_2);
int result = chunkHash2.compareTo(chunkHash1);
assertThat(result).isGreaterThan(0);
}
}

View File

@@ -1,114 +0,0 @@
/*
* 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.chunk;
import static com.google.common.truth.Truth.assertThat;
import android.platform.test.annotations.Presubmit;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.google.common.base.Charsets;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.Arrays;
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ChunkListingMapTest {
private static final ChunkHash CHUNK_A_HASH = getHash("CHUNK_A");
private static final ChunkHash CHUNK_B_HASH = getHash("CHUNK_B");
private static final ChunkHash CHUNK_C_HASH = getHash("CHUNK_C");
private static final int CHUNK_A_LENGTH = 256;
private static final int CHUNK_B_LENGTH = 1024;
private static final int CHUNK_C_LENGTH = 4055;
private static final int CHUNK_A_START = 0;
private static final int CHUNK_B_START = CHUNK_A_START + CHUNK_A_LENGTH;
private static final int CHUNK_C_START = CHUNK_B_START + CHUNK_B_LENGTH;
private ChunkListingMap mChunkListingMap;
@Before
public void setUp() {
mChunkListingMap = createFromFixture();
}
@Test
public void hasChunk_isTrueForExistingChunks() {
assertThat(mChunkListingMap.hasChunk(CHUNK_A_HASH)).isTrue();
assertThat(mChunkListingMap.hasChunk(CHUNK_B_HASH)).isTrue();
assertThat(mChunkListingMap.hasChunk(CHUNK_C_HASH)).isTrue();
}
@Test
public void hasChunk_isFalseForNonexistentChunks() {
assertThat(mChunkListingMap.hasChunk(getHash("CHUNK_D"))).isFalse();
assertThat(mChunkListingMap.hasChunk(getHash(""))).isFalse();
}
@Test
public void getChunkListing_hasCorrectLengths() {
assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getLength())
.isEqualTo(CHUNK_A_LENGTH);
assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getLength())
.isEqualTo(CHUNK_B_LENGTH);
assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getLength())
.isEqualTo(CHUNK_C_LENGTH);
}
@Test
public void getChunkListing_hasCorrectStarts() {
assertThat(mChunkListingMap.getChunkEntry(CHUNK_A_HASH).getStart())
.isEqualTo(CHUNK_A_START);
assertThat(mChunkListingMap.getChunkEntry(CHUNK_B_HASH).getStart())
.isEqualTo(CHUNK_B_START);
assertThat(mChunkListingMap.getChunkEntry(CHUNK_C_HASH).getStart())
.isEqualTo(CHUNK_C_START);
}
@Test
public void getChunkListing_isNullForNonExistentChunks() {
assertThat(mChunkListingMap.getChunkEntry(getHash("Hey"))).isNull();
}
private static ChunkListingMap createFromFixture() {
ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
chunkListing.chunks[0] = newChunk(CHUNK_A_HASH.getHash(), CHUNK_A_LENGTH);
chunkListing.chunks[1] = newChunk(CHUNK_B_HASH.getHash(), CHUNK_B_LENGTH);
chunkListing.chunks[2] = newChunk(CHUNK_C_HASH.getHash(), CHUNK_C_LENGTH);
return ChunkListingMap.fromProto(chunkListing);
}
private static ChunkHash getHash(String name) {
return new ChunkHash(
Arrays.copyOf(name.getBytes(Charsets.UTF_8), ChunkHash.HASH_LENGTH_BYTES));
}
public static ChunksMetadataProto.Chunk newChunk(byte[] hash, int length) {
ChunksMetadataProto.Chunk newChunk = new ChunksMetadataProto.Chunk();
newChunk.hash = Arrays.copyOf(hash, hash.length);
newChunk.length = length;
return newChunk;
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright (C) 2018 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.chunk;
import static com.google.common.truth.Truth.assertThat;
import android.platform.test.annotations.Presubmit;
import com.google.common.primitives.Bytes;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class EncryptedChunkOrderingTest {
private static final byte[] TEST_BYTE_ARRAY_1 = new byte[] {1, 2, 3, 4, 5};
private static final byte[] TEST_BYTE_ARRAY_2 = new byte[] {5, 4, 3, 2, 1};
@Test
public void testEncryptedChunkOrdering_returnsValue() {
EncryptedChunkOrdering encryptedChunkOrdering =
EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
byte[] bytes = encryptedChunkOrdering.encryptedChunkOrdering();
assertThat(bytes)
.asList()
.containsExactlyElementsIn(Bytes.asList(TEST_BYTE_ARRAY_1))
.inOrder();
}
@Test
public void testEquals() {
EncryptedChunkOrdering chunkOrdering1 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
EncryptedChunkOrdering equalChunkOrdering1 =
EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
EncryptedChunkOrdering chunkOrdering2 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_2);
assertThat(chunkOrdering1).isEqualTo(equalChunkOrdering1);
assertThat(chunkOrdering1).isNotEqualTo(chunkOrdering2);
}
@Test
public void testHashCode() {
EncryptedChunkOrdering chunkOrdering1 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
EncryptedChunkOrdering equalChunkOrdering1 =
EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_1);
EncryptedChunkOrdering chunkOrdering2 = EncryptedChunkOrdering.create(TEST_BYTE_ARRAY_2);
int hash1 = chunkOrdering1.hashCode();
int equalHash1 = equalChunkOrdering1.hashCode();
int hash2 = chunkOrdering2.hashCode();
assertThat(hash1).isEqualTo(equalHash1);
assertThat(hash1).isNotEqualTo(hash2);
}
}

View File

@@ -1,614 +0,0 @@
/*
* 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.chunking;
import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.AES_256_GCM;
import static com.android.server.backup.encryption.protos.nano.ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
import static com.android.server.backup.testing.CryptoTestUtils.newChunk;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static junit.framework.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.platform.test.annotations.Presubmit;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.testing.DiffScriptProcessor;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Files;
import com.google.common.primitives.Bytes;
import com.google.common.primitives.Longs;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class BackupFileBuilderTest {
private static final String TEST_DATA_1 =
"I'm already there or close to [T7-9/executive level] in terms of big-picture vision";
private static final String TEST_DATA_2 =
"I was known for Real Games and should have been brought in for advice";
private static final String TEST_DATA_3 =
"Pride is rooted in the delusional belief held by all humans in an unchanging self";
private static final byte[] TEST_FINGERPRINT_MIXER_SALT =
Arrays.copyOf(new byte[] {22}, ChunkHash.HASH_LENGTH_BYTES);
private static final ChunkHash TEST_HASH_1 =
new ChunkHash(Arrays.copyOf(new byte[] {0}, EncryptedChunk.KEY_LENGTH_BYTES));
private static final ChunkHash TEST_HASH_2 =
new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
private static final ChunkHash TEST_HASH_3 =
new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
private static final byte[] TEST_NONCE =
Arrays.copyOf(new byte[] {3}, EncryptedChunk.NONCE_LENGTH_BYTES);
private static final EncryptedChunk TEST_CHUNK_1 =
EncryptedChunk.create(TEST_HASH_1, TEST_NONCE, TEST_DATA_1.getBytes(UTF_8));
private static final EncryptedChunk TEST_CHUNK_2 =
EncryptedChunk.create(TEST_HASH_2, TEST_NONCE, TEST_DATA_2.getBytes(UTF_8));
private static final EncryptedChunk TEST_CHUNK_3 =
EncryptedChunk.create(TEST_HASH_3, TEST_NONCE, TEST_DATA_3.getBytes(UTF_8));
private static final byte[] TEST_CHECKSUM = {1, 2, 3, 4, 5, 6};
@Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();
private File mOldFile;
private ChunksMetadataProto.ChunkListing mOldChunkListing;
private EncryptedChunkEncoder mEncryptedChunkEncoder;
@Before
public void setUp() {
mEncryptedChunkEncoder = new LengthlessEncryptedChunkEncoder();
}
@Test
public void writeChunks_nonIncremental_writesCorrectRawData() throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
BackupFileBuilder backupFileBuilder = BackupFileBuilder.createForNonIncremental(output);
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
byte[] actual = output.toByteArray();
byte[] expected =
Bytes.concat(
TEST_CHUNK_1.nonce(),
TEST_CHUNK_1.encryptedBytes(),
TEST_CHUNK_2.nonce(),
TEST_CHUNK_2.encryptedBytes());
assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
}
@Test
public void writeChunks_nonIncrementalWithDuplicates_writesEachChunkOnlyOnce()
throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
BackupFileBuilder backupFileBuilder = BackupFileBuilder.createForNonIncremental(output);
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_1),
getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
byte[] actual = output.toByteArray();
byte[] expected =
Bytes.concat(
TEST_CHUNK_1.nonce(),
TEST_CHUNK_1.encryptedBytes(),
TEST_CHUNK_2.nonce(),
TEST_CHUNK_2.encryptedBytes());
assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
}
@Test
public void writeChunks_incremental_writesParsableDiffScript() throws Exception {
// We will insert chunk 2 in between chunks 1 and 3.
setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
getNewChunkMap(TEST_HASH_2));
backupFileBuilder.finish(getTestMetadata());
byte[] actual =
stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
byte[] expected =
Bytes.concat(
TEST_CHUNK_1.nonce(),
TEST_CHUNK_1.encryptedBytes(),
TEST_CHUNK_2.nonce(),
TEST_CHUNK_2.encryptedBytes(),
TEST_CHUNK_3.nonce(),
TEST_CHUNK_3.encryptedBytes());
assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
}
@Test
public void writeChunks_incrementalWithDuplicates_writesEachChunkOnlyOnce() throws Exception {
// We will insert chunk 2 twice in between chunks 1 and 3.
setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_2, TEST_HASH_3),
getNewChunkMap(TEST_HASH_2));
backupFileBuilder.finish(getTestMetadata());
byte[] actual =
stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
byte[] expected =
Bytes.concat(
TEST_CHUNK_1.nonce(),
TEST_CHUNK_1.encryptedBytes(),
TEST_CHUNK_2.nonce(),
TEST_CHUNK_2.encryptedBytes(),
TEST_CHUNK_3.nonce(),
TEST_CHUNK_3.encryptedBytes());
assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
}
@Test
public void writeChunks_writesChunksInOrderOfHash() throws Exception {
setUpOldBackupWithChunks(ImmutableList.of());
ByteArrayOutputStream diffOutputStream = new ByteArrayOutputStream();
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(diffOutputStream, mOldChunkListing);
// Write chunks out of order.
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_2, TEST_HASH_1),
getNewChunkMap(TEST_HASH_2, TEST_HASH_1));
backupFileBuilder.finish(getTestMetadata());
byte[] actual =
stripMetadataAndPositionFromOutput(parseDiffScript(diffOutputStream.toByteArray()));
byte[] expected =
Bytes.concat(
TEST_CHUNK_1.nonce(),
TEST_CHUNK_1.encryptedBytes(),
TEST_CHUNK_2.nonce(),
TEST_CHUNK_2.encryptedBytes());
assertThat(actual).asList().containsExactlyElementsIn(Bytes.asList(expected)).inOrder();
}
@Test
public void writeChunks_alreadyFlushed_throwsException() throws Exception {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
backupFileBuilder.finish(getTestMetadata());
assertThrows(
IllegalStateException.class,
() -> backupFileBuilder.writeChunks(ImmutableList.of(), getNewChunkMap()));
}
@Test
public void getNewChunkListing_hasChunksInOrderOfKey() throws Exception {
// We will insert chunk 2 in between chunks 1 and 3.
setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
// Write chunks out of order.
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2),
getNewChunkMap(TEST_HASH_2));
backupFileBuilder.finish(getTestMetadata());
ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
ChunksMetadataProto.ChunkListing actual =
backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
assertListingsEqual(actual, expected);
}
@Test
public void getNewChunkListing_writeChunksInTwoBatches_returnsListingContainingAllChunks()
throws Exception {
// We will insert chunk 2 in between chunks 1 and 3.
setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1, TEST_CHUNK_3));
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2), getNewChunkMap(TEST_HASH_2));
backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_3), getNewChunkMap(TEST_HASH_2));
backupFileBuilder.finish(getTestMetadata());
ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
ChunksMetadataProto.ChunkListing actual =
backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
assertListingsEqual(actual, expected);
}
@Test
public void getNewChunkListing_writeDuplicateChunks_writesEachChunkOnlyOnce() throws Exception {
// We will append [2][3][3][2] onto [1].
setUpOldBackupWithChunks(ImmutableList.of(TEST_CHUNK_1));
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_3),
getNewChunkMap(TEST_HASH_3, TEST_HASH_2));
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_3, TEST_HASH_2),
getNewChunkMap(TEST_HASH_3, TEST_HASH_2));
backupFileBuilder.finish(getTestMetadata());
ChunksMetadataProto.ChunkListing expected = expectedChunkListing();
ChunksMetadataProto.ChunkListing actual =
backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
assertListingsEqual(actual, expected);
}
@Test
public void getNewChunkListing_nonIncrementalWithNoSalt_doesNotThrowOnSerialisation() {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
// Does not throw.
ChunksMetadataProto.ChunkListing.toByteArray(newChunkListing);
}
@Test
public void getNewChunkListing_incrementalWithNoSalt_doesNotThrowOnSerialisation()
throws Exception {
setUpOldBackupWithChunks(ImmutableList.of());
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
// Does not throw.
ChunksMetadataProto.ChunkListing.toByteArray(newChunkListing);
}
@Test
public void getNewChunkListing_nonIncrementalWithNoSalt_hasEmptySalt() {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
assertThat(newChunkListing.fingerprintMixerSalt).isEmpty();
}
@Test
public void getNewChunkListing_incrementalWithNoSalt_hasEmptySalt() throws Exception {
setUpOldBackupWithChunks(ImmutableList.of());
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
assertThat(newChunkListing.fingerprintMixerSalt).isEmpty();
}
@Test
public void getNewChunkListing_nonIncrementalWithSalt_hasGivenSalt() {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
assertThat(newChunkListing.fingerprintMixerSalt).isEqualTo(TEST_FINGERPRINT_MIXER_SALT);
}
@Test
public void getNewChunkListing_incrementalWithSalt_hasGivenSalt() throws Exception {
setUpOldBackupWithChunks(ImmutableList.of());
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(TEST_FINGERPRINT_MIXER_SALT);
assertThat(newChunkListing.fingerprintMixerSalt).isEqualTo(TEST_FINGERPRINT_MIXER_SALT);
}
@Test
public void getNewChunkListing_nonIncremental_hasCorrectCipherTypeAndChunkOrderingType() {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForNonIncremental(new ByteArrayOutputStream());
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
assertThat(newChunkListing.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
assertThat(newChunkListing.chunkOrderingType)
.isEqualTo(ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED);
}
@Test
public void getNewChunkListing_incremental_hasCorrectCipherTypeAndChunkOrderingType()
throws Exception {
setUpOldBackupWithChunks(ImmutableList.of());
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), mOldChunkListing);
ChunksMetadataProto.ChunkListing newChunkListing =
backupFileBuilder.getNewChunkListing(/*fingerprintMixerSalt=*/ null);
assertThat(newChunkListing.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
assertThat(newChunkListing.chunkOrderingType)
.isEqualTo(ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED);
}
@Test
public void getNewChunkOrdering_chunksHaveCorrectStartPositions() throws Exception {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
// Write out of order by key to check that ordering is maintained.
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2),
getNewChunkMap(TEST_HASH_1, TEST_HASH_3, TEST_HASH_2));
backupFileBuilder.finish(getTestMetadata());
ChunksMetadataProto.ChunkOrdering actual =
backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
// The chunks are listed in the order they are written above, but the start positions are
// determined by the order in the encrypted blob (which is lexicographical by key).
int chunk1Start = 0;
int chunk2Start =
chunk1Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1);
int chunk3Start =
chunk2Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2);
int[] expected = {chunk1Start, chunk3Start, chunk2Start};
assertThat(actual.starts.length).isEqualTo(expected.length);
for (int i = 0; i < actual.starts.length; i++) {
assertThat(expected[i]).isEqualTo(actual.starts[i]);
}
}
@Test
public void getNewChunkOrdering_duplicateChunks_writesDuplicates() throws Exception {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2, TEST_HASH_2),
getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
backupFileBuilder.writeChunks(
ImmutableList.of(TEST_HASH_3, TEST_HASH_3), getNewChunkMap(TEST_HASH_3));
backupFileBuilder.finish(getTestMetadata());
ChunksMetadataProto.ChunkOrdering actual =
backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
int chunk1Start = 0;
int chunk2Start =
chunk1Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1);
int chunk3Start =
chunk2Start + mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2);
int[] expected = {chunk1Start, chunk2Start, chunk2Start, chunk3Start, chunk3Start};
assertThat(actual.starts.length).isEqualTo(expected.length);
for (int i = 0; i < actual.starts.length; i++) {
assertThat(expected[i]).isEqualTo(actual.starts[i]);
}
}
@Test
public void getNewChunkOrdering_returnsOrderingWithChecksum() throws Exception {
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
new ByteArrayOutputStream(), new ChunksMetadataProto.ChunkListing());
backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_1), getNewChunkMap(TEST_HASH_1));
backupFileBuilder.finish(getTestMetadata());
ChunksMetadataProto.ChunkOrdering actual =
backupFileBuilder.getNewChunkOrdering(TEST_CHECKSUM);
assertThat(actual.checksum).isEqualTo(TEST_CHECKSUM);
}
@Test
public void finish_writesMetadata() throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
BackupFileBuilder builder = BackupFileBuilder.createForNonIncremental(output);
ChunksMetadataProto.ChunksMetadata expectedMetadata = getTestMetadata();
builder.finish(expectedMetadata);
// The output is [metadata]+[long giving size of metadata].
byte[] metadataBytes =
Arrays.copyOfRange(output.toByteArray(), 0, output.size() - Long.BYTES);
ChunksMetadataProto.ChunksMetadata actualMetadata =
ChunksMetadataProto.ChunksMetadata.parseFrom(metadataBytes);
assertThat(actualMetadata.checksumType).isEqualTo(ChunksMetadataProto.SHA_256);
assertThat(actualMetadata.cipherType).isEqualTo(ChunksMetadataProto.AES_256_GCM);
}
@Test
public void finish_writesMetadataPosition() throws Exception {
ByteArrayOutputStream output = new ByteArrayOutputStream();
BackupFileBuilder builder = BackupFileBuilder.createForNonIncremental(output);
builder.writeChunks(
ImmutableList.of(TEST_HASH_1, TEST_HASH_2),
getNewChunkMap(TEST_HASH_1, TEST_HASH_2));
builder.writeChunks(ImmutableList.of(TEST_HASH_3), getNewChunkMap(TEST_HASH_3));
builder.finish(getTestMetadata());
long expectedPosition =
(long) mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_1)
+ mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_2)
+ mEncryptedChunkEncoder.getEncodedLengthOfChunk(TEST_CHUNK_3);
long actualPosition =
Longs.fromByteArray(
Arrays.copyOfRange(
output.toByteArray(), output.size() - Long.BYTES, output.size()));
assertThat(actualPosition).isEqualTo(expectedPosition);
}
@Test
public void finish_flushesOutputStream() throws Exception {
OutputStream diffOutputStream = mock(OutputStream.class);
BackupFileBuilder backupFileBuilder =
BackupFileBuilder.createForIncremental(
diffOutputStream, new ChunksMetadataProto.ChunkListing());
backupFileBuilder.writeChunks(ImmutableList.of(TEST_HASH_1), getNewChunkMap(TEST_HASH_1));
diffOutputStream.flush();
verify(diffOutputStream).flush();
}
private void setUpOldBackupWithChunks(List<EncryptedChunk> chunks) throws Exception {
mOldFile = mTemporaryFolder.newFile();
ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
chunkListing.fingerprintMixerSalt =
Arrays.copyOf(TEST_FINGERPRINT_MIXER_SALT, TEST_FINGERPRINT_MIXER_SALT.length);
chunkListing.cipherType = AES_256_GCM;
chunkListing.chunkOrderingType = CHUNK_ORDERING_TYPE_UNSPECIFIED;
List<ChunksMetadataProto.Chunk> knownChunks = new ArrayList<>();
try (FileOutputStream outputStream = new FileOutputStream(mOldFile)) {
for (EncryptedChunk chunk : chunks) {
// Chunks are encoded in the format [nonce]+[data].
outputStream.write(chunk.nonce());
outputStream.write(chunk.encryptedBytes());
knownChunks.add(createChunkFor(chunk));
}
outputStream.flush();
}
chunkListing.chunks = knownChunks.toArray(new ChunksMetadataProto.Chunk[0]);
mOldChunkListing = chunkListing;
}
private byte[] parseDiffScript(byte[] diffScript) throws Exception {
File newFile = mTemporaryFolder.newFile();
new DiffScriptProcessor(mOldFile, newFile).process(new ByteArrayInputStream(diffScript));
return Files.toByteArray(newFile);
}
private void assertListingsEqual(
ChunksMetadataProto.ChunkListing result, ChunksMetadataProto.ChunkListing expected) {
assertThat(result.chunks.length).isEqualTo(expected.chunks.length);
for (int i = 0; i < result.chunks.length; i++) {
assertWithMessage("Chunk " + i)
.that(result.chunks[i].length)
.isEqualTo(expected.chunks[i].length);
assertWithMessage("Chunk " + i)
.that(result.chunks[i].hash)
.isEqualTo(expected.chunks[i].hash);
}
}
private static ImmutableMap<ChunkHash, EncryptedChunk> getNewChunkMap(ChunkHash... hashes) {
ImmutableMap.Builder<ChunkHash, EncryptedChunk> builder = ImmutableMap.builder();
for (ChunkHash hash : hashes) {
if (TEST_HASH_1.equals(hash)) {
builder.put(TEST_HASH_1, TEST_CHUNK_1);
} else if (TEST_HASH_2.equals(hash)) {
builder.put(TEST_HASH_2, TEST_CHUNK_2);
} else if (TEST_HASH_3.equals(hash)) {
builder.put(TEST_HASH_3, TEST_CHUNK_3);
} else {
fail("Hash was not recognised: " + hash);
}
}
return builder.build();
}
private static ChunksMetadataProto.ChunksMetadata getTestMetadata() {
ChunksMetadataProto.ChunksMetadata metadata = new ChunksMetadataProto.ChunksMetadata();
metadata.checksumType = ChunksMetadataProto.SHA_256;
metadata.cipherType = AES_256_GCM;
return metadata;
}
private static byte[] stripMetadataAndPositionFromOutput(byte[] output) {
long metadataStart =
Longs.fromByteArray(
Arrays.copyOfRange(output, output.length - Long.BYTES, output.length));
return Arrays.copyOfRange(output, 0, (int) metadataStart);
}
private ChunksMetadataProto.ChunkListing expectedChunkListing() {
ChunksMetadataProto.ChunkListing chunkListing = new ChunksMetadataProto.ChunkListing();
chunkListing.fingerprintMixerSalt =
Arrays.copyOf(TEST_FINGERPRINT_MIXER_SALT, TEST_FINGERPRINT_MIXER_SALT.length);
chunkListing.cipherType = AES_256_GCM;
chunkListing.chunkOrderingType = CHUNK_ORDERING_TYPE_UNSPECIFIED;
chunkListing.chunks = new ChunksMetadataProto.Chunk[3];
chunkListing.chunks[0] = createChunkFor(TEST_CHUNK_1);
chunkListing.chunks[1] = createChunkFor(TEST_CHUNK_2);
chunkListing.chunks[2] = createChunkFor(TEST_CHUNK_3);
return chunkListing;
}
private ChunksMetadataProto.Chunk createChunkFor(EncryptedChunk encryptedChunk) {
byte[] chunkHash = encryptedChunk.key().getHash();
byte[] hashCopy = Arrays.copyOf(chunkHash, chunkHash.length);
return newChunk(hashCopy, mEncryptedChunkEncoder.getEncodedLengthOfChunk(encryptedChunk));
}
}

View File

@@ -1,57 +0,0 @@
/*
* Copyright (C) 2018 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.chunking;
import static org.junit.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
import android.platform.test.annotations.Presubmit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
/** Tests for {@link ByteRange}. */
@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ByteRangeTest {
@Test
public void getLength_includesEnd() throws Exception {
ByteRange byteRange = new ByteRange(5, 10);
int length = byteRange.getLength();
assertEquals(6, length);
}
@Test
public void constructor_rejectsNegativeStart() {
assertThrows(IllegalArgumentException.class, () -> new ByteRange(-1, 10));
}
@Test
public void constructor_rejectsEndBeforeStart() {
assertThrows(IllegalArgumentException.class, () -> new ByteRange(10, 9));
}
@Test
public void extend_withZeroLength_throwsException() {
ByteRange byteRange = new ByteRange(5, 10);
assertThrows(IllegalArgumentException.class, () -> byteRange.extend(0));
}
}

Some files were not shown because too many files have changed in this diff Show More