summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java378
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java24
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java27
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java28
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java583
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java49
6 files changed, 1087 insertions, 2 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
new file mode 100644
index 000000000000..9bf148ddc901
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTask.java
@@ -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));
+ }
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
new file mode 100644
index 000000000000..78c370b0d548
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MalformedEncryptedFileException.java
@@ -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);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
new file mode 100644
index 000000000000..1e4f43b43e26
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/MessageDigestMismatchException.java
@@ -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);
+ }
+}
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
new file mode 100644
index 000000000000..9a97e3870d83
--- /dev/null
+++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/tasks/UnsupportedEncryptedFileException.java
@@ -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);
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
new file mode 100644
index 000000000000..07a6fd2d5b60
--- /dev/null
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/tasks/BackupFileDecryptorTaskTest.java
@@ -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);
+ }
+}
diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
index b9055cecd502..5cff53f817d4 100644
--- a/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
+++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/testing/CryptoTestUtils.java
@@ -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;
+ }
}