Merge changes I6b5a6bb4,Ib5dc3789
* changes: Move transport client code back to the main target Delete backup encryption code
This commit is contained in:
@@ -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"],
|
||||
}
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
include /services/backup/OWNERS
|
||||
@@ -1 +0,0 @@
|
||||
-keep class com.android.server.backup.encryption
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"],
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user