Import BackupFileDecryptorTask
Bug: 111386661 Test: make RunBackupEncryptionRoboTests Change-Id: I411ab1055203b6963726c9ca171b47b52cac83c8
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
/*
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.server.backup.encryption.tasks;
|
||||
|
||||
/** Exception thrown when we cannot parse the encrypted backup file. */
|
||||
public class MalformedEncryptedFileException extends EncryptedRestoreException {
|
||||
public MalformedEncryptedFileException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
/*
|
||||
* 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.server.backup.testing.CryptoTestUtils.generateAesKey;
|
||||
import static com.android.server.backup.testing.CryptoTestUtils.newChunkOrdering;
|
||||
import static com.android.server.backup.testing.CryptoTestUtils.newChunksMetadata;
|
||||
import static com.android.server.backup.testing.CryptoTestUtils.newPair;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.testng.Assert.assertThrows;
|
||||
import static org.testng.Assert.expectThrows;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
|
||||
import com.android.server.backup.encryption.chunk.ChunkHash;
|
||||
import com.android.server.backup.encryption.chunking.ChunkHasher;
|
||||
import com.android.server.backup.encryption.chunking.DecryptedChunkFileOutput;
|
||||
import com.android.server.backup.encryption.chunking.EncryptedChunk;
|
||||
import com.android.server.backup.encryption.chunking.cdc.FingerprintMixer;
|
||||
import com.android.server.backup.encryption.kv.DecryptedChunkKvOutput;
|
||||
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.android.server.backup.encryption.protos.nano.KeyValuePairProto.KeyValuePair;
|
||||
import com.android.server.backup.encryption.tasks.BackupEncrypter.Result;
|
||||
import com.android.server.backup.testing.CryptoTestUtils;
|
||||
import com.android.server.testing.shadows.ShadowBackupDataInput;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.protobuf.nano.MessageNano;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.crypto.AEADBadTagException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
@Config(shadows = {ShadowBackupDataInput.class})
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Presubmit
|
||||
public class BackupFileDecryptorTaskTest {
|
||||
private static final String READ_WRITE_MODE = "rw";
|
||||
private static final int BYTES_PER_KILOBYTE = 1024;
|
||||
private static final int MIN_CHUNK_SIZE_BYTES = 2 * BYTES_PER_KILOBYTE;
|
||||
private static final int AVERAGE_CHUNK_SIZE_BYTES = 4 * BYTES_PER_KILOBYTE;
|
||||
private static final int MAX_CHUNK_SIZE_BYTES = 64 * BYTES_PER_KILOBYTE;
|
||||
private static final int BACKUP_DATA_SIZE_BYTES = 60 * BYTES_PER_KILOBYTE;
|
||||
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 int CHECKSUM_LENGTH_BYTES = 256 / BITS_PER_BYTE;
|
||||
@Nullable private static final FileDescriptor NULL_FILE_DESCRIPTOR = null;
|
||||
|
||||
private static final Set<KeyValuePair> TEST_KV_DATA = new HashSet<>();
|
||||
|
||||
static {
|
||||
TEST_KV_DATA.add(newPair("key1", "value1"));
|
||||
TEST_KV_DATA.add(newPair("key2", "value2"));
|
||||
}
|
||||
|
||||
@Rule public final TemporaryFolder mTemporaryFolder = new TemporaryFolder();
|
||||
|
||||
private SecretKey mTertiaryKey;
|
||||
private SecretKey mChunkEncryptionKey;
|
||||
private File mInputFile;
|
||||
private File mOutputFile;
|
||||
private DecryptedChunkOutput mFileOutput;
|
||||
private DecryptedChunkKvOutput mKvOutput;
|
||||
private Random mRandom;
|
||||
private BackupFileDecryptorTask mTask;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
mRandom = new Random();
|
||||
mTertiaryKey = generateAesKey();
|
||||
// In good situations it's always the same. We allow changing it for testing when somehow it
|
||||
// has become mismatched that we throw an error.
|
||||
mChunkEncryptionKey = mTertiaryKey;
|
||||
mInputFile = mTemporaryFolder.newFile();
|
||||
mOutputFile = mTemporaryFolder.newFile();
|
||||
mFileOutput = new DecryptedChunkFileOutput(mOutputFile);
|
||||
mKvOutput = new DecryptedChunkKvOutput(new ChunkHasher(mTertiaryKey));
|
||||
mTask = new BackupFileDecryptorTask(mTertiaryKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForNonExistentInput() throws Exception {
|
||||
assertThrows(
|
||||
FileNotFoundException.class,
|
||||
() ->
|
||||
mTask.decryptFile(
|
||||
new File(mTemporaryFolder.newFolder(), "nonexistent"),
|
||||
mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForDirectoryInputFile() throws Exception {
|
||||
assertThrows(
|
||||
FileNotFoundException.class,
|
||||
() -> mTask.decryptFile(mTemporaryFolder.newFolder(), mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withExplicitStarts_decryptsEncryptedData() throws Exception {
|
||||
byte[] backupData = randomData(BACKUP_DATA_SIZE_BYTES);
|
||||
createEncryptedFileUsingExplicitStarts(backupData);
|
||||
|
||||
mTask.decryptFile(mInputFile, mFileOutput);
|
||||
|
||||
assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withInlineLengths_decryptsEncryptedData() throws Exception {
|
||||
createEncryptedFileUsingInlineLengths(
|
||||
TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
|
||||
mTask.decryptFile(mInputFile, mKvOutput);
|
||||
assertThat(asMap(mKvOutput.getPairs())).containsExactlyEntriesIn(asMap(TEST_KV_DATA));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withNoChunkOrderingType_decryptsUsingExplicitStarts() throws Exception {
|
||||
byte[] backupData = randomData(BACKUP_DATA_SIZE_BYTES);
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
backupData,
|
||||
chunkOrdering -> chunkOrdering,
|
||||
chunksMetadata -> {
|
||||
ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
|
||||
metadata.chunkOrderingType =
|
||||
ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;
|
||||
return metadata;
|
||||
});
|
||||
|
||||
mTask.decryptFile(mInputFile, mFileOutput);
|
||||
|
||||
assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withInlineLengths_throwsForZeroLengths() throws Exception {
|
||||
createEncryptedFileUsingInlineLengths(
|
||||
TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
|
||||
|
||||
// Set the length of the first chunk to zero.
|
||||
RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
|
||||
raf.seek(0);
|
||||
raf.writeInt(0);
|
||||
|
||||
assertThrows(
|
||||
MalformedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mKvOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withInlineLengths_throwsForLongLengths() throws Exception {
|
||||
createEncryptedFileUsingInlineLengths(
|
||||
TEST_KV_DATA, chunkOrdering -> chunkOrdering, chunksMetadata -> chunksMetadata);
|
||||
|
||||
// Set the length of the first chunk to zero.
|
||||
RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
|
||||
raf.seek(0);
|
||||
raf.writeInt((int) mInputFile.length());
|
||||
|
||||
assertThrows(
|
||||
MalformedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mKvOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForBadKey() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
|
||||
|
||||
assertThrows(
|
||||
AEADBadTagException.class,
|
||||
() ->
|
||||
new BackupFileDecryptorTask(generateAesKey())
|
||||
.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withExplicitStarts_throwsForMangledOrdering() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
randomData(BACKUP_DATA_SIZE_BYTES),
|
||||
chunkOrdering -> {
|
||||
ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
|
||||
Arrays.sort(ordering.starts);
|
||||
return ordering;
|
||||
});
|
||||
|
||||
assertThrows(
|
||||
MessageDigestMismatchException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_withExplicitStarts_noChunks_returnsNoData() throws Exception {
|
||||
byte[] backupData = randomData(/*length=*/ 0);
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
backupData,
|
||||
chunkOrdering -> {
|
||||
ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
|
||||
ordering.starts = new int[0];
|
||||
return ordering;
|
||||
});
|
||||
|
||||
mTask.decryptFile(mInputFile, mFileOutput);
|
||||
|
||||
assertThat(Files.readAllBytes(Paths.get(mOutputFile.toURI()))).isEqualTo(backupData);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForMismatchedChecksum() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
randomData(BACKUP_DATA_SIZE_BYTES),
|
||||
chunkOrdering -> {
|
||||
ChunkOrdering ordering = CryptoTestUtils.clone(chunkOrdering);
|
||||
ordering.checksum =
|
||||
Arrays.copyOf(randomData(CHECKSUM_LENGTH_BYTES), CHECKSUM_LENGTH_BYTES);
|
||||
return ordering;
|
||||
});
|
||||
|
||||
assertThrows(
|
||||
MessageDigestMismatchException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForBadChunksMetadataOffset() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
|
||||
|
||||
// Replace the metadata with all 1s.
|
||||
RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
|
||||
raf.seek(raf.length() - Long.BYTES);
|
||||
int metadataOffset = (int) raf.readLong();
|
||||
int metadataLength = (int) raf.length() - metadataOffset - Long.BYTES;
|
||||
|
||||
byte[] allOnes = new byte[metadataLength];
|
||||
Arrays.fill(allOnes, (byte) 1);
|
||||
|
||||
raf.seek(metadataOffset);
|
||||
raf.write(allOnes, /*off=*/ 0, metadataLength);
|
||||
|
||||
MalformedEncryptedFileException thrown =
|
||||
expectThrows(
|
||||
MalformedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"Could not read chunks metadata at position "
|
||||
+ metadataOffset
|
||||
+ " of file of "
|
||||
+ raf.length()
|
||||
+ " bytes");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForChunksMetadataOffsetBeyondEndOfFile() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
|
||||
|
||||
RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
|
||||
raf.seek(raf.length() - Long.BYTES);
|
||||
raf.writeLong(raf.length());
|
||||
|
||||
MalformedEncryptedFileException thrown =
|
||||
expectThrows(
|
||||
MalformedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
raf.length()
|
||||
+ " is not valid position for chunks metadata in file of "
|
||||
+ raf.length()
|
||||
+ " bytes");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForChunksMetadataOffsetBeforeBeginningOfFile() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
|
||||
|
||||
RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
|
||||
raf.seek(raf.length() - Long.BYTES);
|
||||
raf.writeLong(-1);
|
||||
|
||||
MalformedEncryptedFileException thrown =
|
||||
expectThrows(
|
||||
MalformedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"-1 is not valid position for chunks metadata in file of "
|
||||
+ raf.length()
|
||||
+ " bytes");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForMangledChunks() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
|
||||
|
||||
// Mess up some bits in a random byte
|
||||
RandomAccessFile raf = new RandomAccessFile(mInputFile, READ_WRITE_MODE);
|
||||
raf.seek(50);
|
||||
byte fiftiethByte = raf.readByte();
|
||||
raf.seek(50);
|
||||
raf.write(~fiftiethByte);
|
||||
|
||||
assertThrows(AEADBadTagException.class, () -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForBadChunkEncryptionKey() throws Exception {
|
||||
mChunkEncryptionKey = generateAesKey();
|
||||
|
||||
createEncryptedFileUsingExplicitStarts(randomData(BACKUP_DATA_SIZE_BYTES));
|
||||
|
||||
assertThrows(AEADBadTagException.class, () -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForUnsupportedCipherType() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
randomData(BACKUP_DATA_SIZE_BYTES),
|
||||
chunkOrdering -> chunkOrdering,
|
||||
chunksMetadata -> {
|
||||
ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
|
||||
metadata.cipherType = ChunksMetadataProto.UNKNOWN_CIPHER_TYPE;
|
||||
return metadata;
|
||||
});
|
||||
|
||||
assertThrows(
|
||||
UnsupportedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decryptFile_throwsForUnsupportedMessageDigestType() throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
randomData(BACKUP_DATA_SIZE_BYTES),
|
||||
chunkOrdering -> chunkOrdering,
|
||||
chunksMetadata -> {
|
||||
ChunksMetadata metadata = CryptoTestUtils.clone(chunksMetadata);
|
||||
metadata.checksumType = ChunksMetadataProto.UNKNOWN_CHECKSUM_TYPE;
|
||||
return metadata;
|
||||
});
|
||||
|
||||
assertThrows(
|
||||
UnsupportedEncryptedFileException.class,
|
||||
() -> mTask.decryptFile(mInputFile, mFileOutput));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encrypted backup file from the given data.
|
||||
*
|
||||
* @param data The plaintext content.
|
||||
*/
|
||||
private void createEncryptedFileUsingExplicitStarts(byte[] data) throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(data, chunkOrdering -> chunkOrdering);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encrypted backup file from the given data.
|
||||
*
|
||||
* @param data The plaintext content.
|
||||
* @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
|
||||
*/
|
||||
private void createEncryptedFileUsingExplicitStarts(
|
||||
byte[] data, Transformer<ChunkOrdering> chunkOrderingTransformer) throws Exception {
|
||||
createEncryptedFileUsingExplicitStarts(
|
||||
data, chunkOrderingTransformer, chunksMetadata -> chunksMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encrypted backup file from the given data in mode {@link
|
||||
* ChunksMetadataProto#EXPLICIT_STARTS}.
|
||||
*
|
||||
* @param data The plaintext content.
|
||||
* @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
|
||||
* @param chunksMetadataTransformer Transforms the metadata before it's written.
|
||||
*/
|
||||
private void createEncryptedFileUsingExplicitStarts(
|
||||
byte[] data,
|
||||
Transformer<ChunkOrdering> chunkOrderingTransformer,
|
||||
Transformer<ChunksMetadata> chunksMetadataTransformer)
|
||||
throws Exception {
|
||||
Result result = backupFullData(data);
|
||||
|
||||
ArrayList<EncryptedChunk> chunks = new ArrayList<>(result.getNewChunks());
|
||||
Collections.shuffle(chunks);
|
||||
HashMap<ChunkHash, Integer> startPositions = new HashMap<>();
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(mInputFile);
|
||||
DataOutputStream dos = new DataOutputStream(fos)) {
|
||||
int position = 0;
|
||||
|
||||
for (EncryptedChunk chunk : chunks) {
|
||||
startPositions.put(chunk.key(), position);
|
||||
dos.write(chunk.nonce());
|
||||
dos.write(chunk.encryptedBytes());
|
||||
position += chunk.nonce().length + chunk.encryptedBytes().length;
|
||||
}
|
||||
|
||||
int[] starts = new int[chunks.size()];
|
||||
List<ChunkHash> chunkListing = result.getAllChunks();
|
||||
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
starts[i] = startPositions.get(chunkListing.get(i));
|
||||
}
|
||||
|
||||
ChunkOrdering chunkOrdering = newChunkOrdering(starts, result.getDigest());
|
||||
chunkOrdering = chunkOrderingTransformer.accept(chunkOrdering);
|
||||
|
||||
ChunksMetadata metadata =
|
||||
newChunksMetadata(
|
||||
ChunksMetadataProto.AES_256_GCM,
|
||||
ChunksMetadataProto.SHA_256,
|
||||
ChunksMetadataProto.EXPLICIT_STARTS,
|
||||
encrypt(chunkOrdering));
|
||||
metadata = chunksMetadataTransformer.accept(metadata);
|
||||
|
||||
dos.write(MessageNano.toByteArray(metadata));
|
||||
dos.writeLong(position);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an encrypted backup file from the given data in mode {@link
|
||||
* ChunksMetadataProto#INLINE_LENGTHS}.
|
||||
*
|
||||
* @param data The plaintext key value pairs to back up.
|
||||
* @param chunkOrderingTransformer Transforms the ordering before it's encrypted.
|
||||
* @param chunksMetadataTransformer Transforms the metadata before it's written.
|
||||
*/
|
||||
private void createEncryptedFileUsingInlineLengths(
|
||||
Set<KeyValuePair> data,
|
||||
Transformer<ChunkOrdering> chunkOrderingTransformer,
|
||||
Transformer<ChunksMetadata> chunksMetadataTransformer)
|
||||
throws Exception {
|
||||
Result result = backupKvData(data);
|
||||
|
||||
List<EncryptedChunk> chunks = new ArrayList<>(result.getNewChunks());
|
||||
System.out.println("we have chunk count " + chunks.size());
|
||||
Collections.shuffle(chunks);
|
||||
|
||||
try (FileOutputStream fos = new FileOutputStream(mInputFile);
|
||||
DataOutputStream dos = new DataOutputStream(fos)) {
|
||||
for (EncryptedChunk chunk : chunks) {
|
||||
dos.writeInt(chunk.nonce().length + chunk.encryptedBytes().length);
|
||||
dos.write(chunk.nonce());
|
||||
dos.write(chunk.encryptedBytes());
|
||||
}
|
||||
|
||||
ChunkOrdering chunkOrdering = newChunkOrdering(null, result.getDigest());
|
||||
chunkOrdering = chunkOrderingTransformer.accept(chunkOrdering);
|
||||
|
||||
ChunksMetadata metadata =
|
||||
newChunksMetadata(
|
||||
ChunksMetadataProto.AES_256_GCM,
|
||||
ChunksMetadataProto.SHA_256,
|
||||
ChunksMetadataProto.INLINE_LENGTHS,
|
||||
encrypt(chunkOrdering));
|
||||
metadata = chunksMetadataTransformer.accept(metadata);
|
||||
|
||||
int metadataStart = dos.size();
|
||||
dos.write(MessageNano.toByteArray(metadata));
|
||||
dos.writeLong(metadataStart);
|
||||
}
|
||||
}
|
||||
|
||||
/** Performs a full backup of the given data, and returns the chunks. */
|
||||
private BackupEncrypter.Result backupFullData(byte[] data) throws Exception {
|
||||
BackupStreamEncrypter encrypter =
|
||||
new BackupStreamEncrypter(
|
||||
new ByteArrayInputStream(data),
|
||||
MIN_CHUNK_SIZE_BYTES,
|
||||
MAX_CHUNK_SIZE_BYTES,
|
||||
AVERAGE_CHUNK_SIZE_BYTES);
|
||||
return encrypter.backup(
|
||||
mChunkEncryptionKey,
|
||||
randomData(FingerprintMixer.SALT_LENGTH_BYTES),
|
||||
new HashSet<>());
|
||||
}
|
||||
|
||||
private Result backupKvData(Set<KeyValuePair> data) throws Exception {
|
||||
ShadowBackupDataInput.reset();
|
||||
for (KeyValuePair pair : data) {
|
||||
ShadowBackupDataInput.addEntity(pair.key, pair.value);
|
||||
}
|
||||
KvBackupEncrypter encrypter =
|
||||
new KvBackupEncrypter(new BackupDataInput(NULL_FILE_DESCRIPTOR));
|
||||
return encrypter.backup(
|
||||
mChunkEncryptionKey,
|
||||
randomData(FingerprintMixer.SALT_LENGTH_BYTES),
|
||||
Collections.EMPTY_SET);
|
||||
}
|
||||
|
||||
/** Encrypts {@code chunkOrdering} using {@link #mTertiaryKey}. */
|
||||
private byte[] encrypt(ChunkOrdering chunkOrdering) throws Exception {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
byte[] nonce = randomData(GCM_NONCE_LENGTH_BYTES);
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
mTertiaryKey,
|
||||
new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * BITS_PER_BYTE, nonce));
|
||||
byte[] nanoBytes = MessageNano.toByteArray(chunkOrdering);
|
||||
byte[] encryptedBytes = cipher.doFinal(nanoBytes);
|
||||
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
out.write(nonce);
|
||||
out.write(encryptedBytes);
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns {@code length} random bytes. */
|
||||
private byte[] randomData(int length) {
|
||||
byte[] data = new byte[length];
|
||||
mRandom.nextBytes(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static ImmutableMap<String, String> asMap(Collection<KeyValuePair> pairs) {
|
||||
ImmutableMap.Builder<String, String> map = ImmutableMap.builder();
|
||||
for (KeyValuePair pair : pairs) {
|
||||
map.put(pair.key, new String(pair.value, Charset.forName("UTF-8")));
|
||||
}
|
||||
return map.build();
|
||||
}
|
||||
|
||||
private interface Transformer<T> {
|
||||
T accept(T t);
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ package com.android.server.backup.testing;
|
||||
|
||||
import com.android.server.backup.encryption.chunk.ChunkHash;
|
||||
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
|
||||
import com.android.server.backup.encryption.protos.nano.KeyValuePairProto;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Random;
|
||||
@@ -86,11 +88,33 @@ public class CryptoTestUtils {
|
||||
public static ChunksMetadataProto.ChunkOrdering newChunkOrdering(
|
||||
int[] starts, byte[] checksum) {
|
||||
ChunksMetadataProto.ChunkOrdering chunkOrdering = new ChunksMetadataProto.ChunkOrdering();
|
||||
chunkOrdering.starts = Arrays.copyOf(starts, starts.length);
|
||||
chunkOrdering.checksum = Arrays.copyOf(checksum, checksum.length);
|
||||
chunkOrdering.starts = starts == null ? null : Arrays.copyOf(starts, starts.length);
|
||||
chunkOrdering.checksum =
|
||||
checksum == null ? checksum : Arrays.copyOf(checksum, checksum.length);
|
||||
return chunkOrdering;
|
||||
}
|
||||
|
||||
public static ChunksMetadataProto.ChunksMetadata newChunksMetadata(
|
||||
int cipherType, int checksumType, int chunkOrderingType, byte[] chunkOrdering) {
|
||||
ChunksMetadataProto.ChunksMetadata metadata = new ChunksMetadataProto.ChunksMetadata();
|
||||
metadata.cipherType = cipherType;
|
||||
metadata.checksumType = checksumType;
|
||||
metadata.chunkOrdering = Arrays.copyOf(chunkOrdering, chunkOrdering.length);
|
||||
metadata.chunkOrderingType = chunkOrderingType;
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static KeyValuePairProto.KeyValuePair newPair(String key, String value) {
|
||||
return newPair(key, value.getBytes(Charset.forName("UTF-8")));
|
||||
}
|
||||
|
||||
public static KeyValuePairProto.KeyValuePair newPair(String key, byte[] value) {
|
||||
KeyValuePairProto.KeyValuePair newPair = new KeyValuePairProto.KeyValuePair();
|
||||
newPair.key = key;
|
||||
newPair.value = value;
|
||||
return newPair;
|
||||
}
|
||||
|
||||
public static ChunksMetadataProto.ChunkListing clone(
|
||||
ChunksMetadataProto.ChunkListing original) {
|
||||
ChunksMetadataProto.Chunk[] clonedChunks;
|
||||
@@ -114,4 +138,25 @@ public class CryptoTestUtils {
|
||||
public static ChunksMetadataProto.Chunk clone(ChunksMetadataProto.Chunk original) {
|
||||
return newChunk(original.hash, original.length);
|
||||
}
|
||||
|
||||
public static ChunksMetadataProto.ChunksMetadata clone(
|
||||
ChunksMetadataProto.ChunksMetadata original) {
|
||||
ChunksMetadataProto.ChunksMetadata cloneMetadata = new ChunksMetadataProto.ChunksMetadata();
|
||||
cloneMetadata.chunkOrderingType = original.chunkOrderingType;
|
||||
cloneMetadata.chunkOrdering =
|
||||
original.chunkOrdering == null
|
||||
? null
|
||||
: Arrays.copyOf(original.chunkOrdering, original.chunkOrdering.length);
|
||||
cloneMetadata.checksumType = original.checksumType;
|
||||
cloneMetadata.cipherType = original.cipherType;
|
||||
return cloneMetadata;
|
||||
}
|
||||
|
||||
public static ChunksMetadataProto.ChunkOrdering clone(
|
||||
ChunksMetadataProto.ChunkOrdering original) {
|
||||
ChunksMetadataProto.ChunkOrdering clone = new ChunksMetadataProto.ChunkOrdering();
|
||||
clone.starts = Arrays.copyOf(original.starts, original.starts.length);
|
||||
clone.checksum = Arrays.copyOf(original.checksum, original.checksum.length);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user