Moves all backup chunk-model related classes over to the framework.
Includes the proto definition of ChunksMetadata and related classes.
Some additional changes (apart from style) were needed:
- EncryptedChunkOrdering was modified to be a non-AutoValue class, and
tests were added.
- Protos are now read from an InputStream manually, as any protos should not
be used directly in the platform.
- Helper classes are added for reading from ProtoInputStream.
Bug: 111386661,116575321
Test: atest RunFrameworksServicesRoboTests
Change-Id: I8b74ad059d72e305be7817f79f8c61aa50f7b268
diff --git a/core/proto/android/server/backup_chunks_metadata.proto b/core/proto/android/server/backup_chunks_metadata.proto
new file mode 100644
index 0000000..a375f02
--- /dev/null
+++ b/core/proto/android/server/backup_chunks_metadata.proto
@@ -0,0 +1,134 @@
+/*
+ * 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 com.android.server.backup.encryption.chunk;
+
+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;
+}
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/encryption/chunk/Chunk.java b/services/backup/java/com/android/server/backup/encryption/chunk/Chunk.java
new file mode 100644
index 0000000..5bec1a9
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/encryption/chunk/Chunk.java
@@ -0,0 +1,54 @@
+package com.android.server.backup.encryption.chunk;
+
+import android.util.proto.ProtoInputStream;
+
+import java.io.IOException;
+
+/**
+ * Information about a chunk entry in a protobuf. Only used for reading from a {@link
+ * ProtoInputStream}.
+ */
+public class Chunk {
+ /**
+ * Reads a Chunk from a {@link ProtoInputStream}. Expects the message to be of format {@link
+ * ChunksMetadataProto.Chunk}.
+ *
+ * @param inputStream currently at a {@link ChunksMetadataProto.Chunk} message.
+ * @throws IOException when the message is not structured as expected or a field can not be
+ * read.
+ */
+ static Chunk readFromProto(ProtoInputStream inputStream) throws IOException {
+ Chunk result = new Chunk();
+
+ while (inputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ switch (inputStream.getFieldNumber()) {
+ case (int) ChunksMetadataProto.Chunk.HASH:
+ result.mHash = inputStream.readBytes(ChunksMetadataProto.Chunk.HASH);
+ break;
+ case (int) ChunksMetadataProto.Chunk.LENGTH:
+ result.mLength = inputStream.readInt(ChunksMetadataProto.Chunk.LENGTH);
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ private int mLength;
+ private byte[] mHash;
+
+ /** Private constructor. This class should only be instantiated by calling readFromProto. */
+ private Chunk() {
+ // Set default values for fields in case they are not available in the proto.
+ mHash = new byte[]{};
+ mLength = 0;
+ }
+
+ public int getLength() {
+ return mLength;
+ }
+
+ public byte[] getHash() {
+ return mHash;
+ }
+}
\ No newline at end of file
diff --git a/services/backup/java/com/android/server/backup/encryption/chunk/ChunkListing.java b/services/backup/java/com/android/server/backup/encryption/chunk/ChunkListing.java
new file mode 100644
index 0000000..2d2e88a
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/encryption/chunk/ChunkListing.java
@@ -0,0 +1,108 @@
+/*
+ * 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 android.annotation.Nullable;
+import android.util.proto.ProtoInputStream;
+import java.io.IOException;
+import java.util.Collections;
+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 ChunkListing {
+ /**
+ * Reads a ChunkListing from a {@link ProtoInputStream}. Expects the message to be of format
+ * {@link ChunksMetadataProto.ChunkListing}.
+ *
+ * @param inputStream Currently at a {@link ChunksMetadataProto.ChunkListing} message.
+ * @throws IOException when the message is not structured as expected or a field can not be
+ * read.
+ */
+ public static ChunkListing readFromProto(ProtoInputStream inputStream) throws IOException {
+ Map<ChunkHash, Entry> entries = new HashMap();
+
+ long start = 0;
+
+ while (inputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+ if (inputStream.getFieldNumber() == (int) ChunksMetadataProto.ChunkListing.CHUNKS) {
+ long chunkToken = inputStream.start(ChunksMetadataProto.ChunkListing.CHUNKS);
+ Chunk chunk = Chunk.readFromProto(inputStream);
+ entries.put(new ChunkHash(chunk.getHash()), new Entry(start, chunk.getLength()));
+ start += chunk.getLength();
+ inputStream.end(chunkToken);
+ }
+ }
+
+ return new ChunkListing(entries);
+ }
+
+ private final Map<ChunkHash, Entry> mChunksByHash;
+
+ private ChunkListing(Map<ChunkHash, Entry> chunksByHash) {
+ mChunksByHash = Collections.unmodifiableMap(new HashMap<>(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);
+ }
+
+ /** Returns the number of chunks in this listing. */
+ public int getChunkCount() {
+ return mChunksByHash.size();
+ }
+
+ /** 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) {
+ mStart = start;
+ mLength = length;
+ }
+
+ /** 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;
+ }
+ }
+}
diff --git a/services/backup/java/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java b/services/backup/java/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java
new file mode 100644
index 0000000..3a6d1f6
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/encryption/chunk/EncryptedChunkOrdering.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+ 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;
+ }
+}
diff --git a/services/robotests/Android.mk b/services/robotests/Android.mk
index 8b59771..78c0be4 100644
--- a/services/robotests/Android.mk
+++ b/services/robotests/Android.mk
@@ -61,6 +61,7 @@
$(call all-Iaidl-files-under, $(INTERNAL_BACKUP)) \
$(call all-java-files-under, ../../core/java/android/app/backup) \
$(call all-Iaidl-files-under, ../../core/java/android/app/backup) \
+ $(call all-java-files-under, ../../core/java/android/util/proto) \
../../core/java/android/content/pm/PackageInfo.java \
../../core/java/android/app/IBackupAgent.aidl \
../../core/java/android/util/KeyValueSettingObserver.java \
diff --git a/services/robotests/src/com/android/server/backup/encryption/chunk/ChunkListingTest.java b/services/robotests/src/com/android/server/backup/encryption/chunk/ChunkListingTest.java
new file mode 100644
index 0000000..383bf1d
--- /dev/null
+++ b/services/robotests/src/com/android/server/backup/encryption/chunk/ChunkListingTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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 android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import com.android.internal.util.Preconditions;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+import com.google.common.base.Charsets;
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 26)
+// Include android.util.proto in addition to classes under test because the latest versions of
+// android.util.proto.Proto{Input|Output}Stream are not part of Robolectric.
+@SystemLoaderPackages({"com.android.server.backup", "android.util.proto"})
+@Presubmit
+public class ChunkListingTest {
+ private static final String CHUNK_A = "CHUNK_A";
+ private static final String CHUNK_B = "CHUNK_B";
+ private static final String CHUNK_C = "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 ChunkHash mChunkHashA;
+ private ChunkHash mChunkHashB;
+ private ChunkHash mChunkHashC;
+
+ @Before
+ public void setUp() throws Exception {
+ mChunkHashA = getHash(CHUNK_A);
+ mChunkHashB = getHash(CHUNK_B);
+ mChunkHashC = getHash(CHUNK_C);
+ }
+
+ @Test
+ public void testHasChunk_whenChunkInListing_returnsTrue() throws Exception {
+ byte[] chunkListingProto =
+ createChunkListingProto(
+ new ChunkHash[] {mChunkHashA, mChunkHashB, mChunkHashC},
+ new int[] {CHUNK_A_LENGTH, CHUNK_B_LENGTH, CHUNK_C_LENGTH});
+ ChunkListing chunkListing =
+ ChunkListing.readFromProto(
+ new ProtoInputStream(new ByteArrayInputStream(chunkListingProto)));
+
+ boolean chunkAInList = chunkListing.hasChunk(mChunkHashA);
+ boolean chunkBInList = chunkListing.hasChunk(mChunkHashB);
+ boolean chunkCInList = chunkListing.hasChunk(mChunkHashC);
+
+ assertThat(chunkAInList).isTrue();
+ assertThat(chunkBInList).isTrue();
+ assertThat(chunkCInList).isTrue();
+ }
+
+ @Test
+ public void testHasChunk_whenChunkNotInListing_returnsFalse() throws Exception {
+ byte[] chunkListingProto =
+ createChunkListingProto(
+ new ChunkHash[] {mChunkHashA, mChunkHashB},
+ new int[] {CHUNK_A_LENGTH, CHUNK_B_LENGTH});
+ ChunkListing chunkListing =
+ ChunkListing.readFromProto(
+ new ProtoInputStream(new ByteArrayInputStream(chunkListingProto)));
+ ChunkHash chunkHashEmpty = getHash("");
+
+ boolean chunkCInList = chunkListing.hasChunk(mChunkHashC);
+ boolean emptyChunkInList = chunkListing.hasChunk(chunkHashEmpty);
+
+ assertThat(chunkCInList).isFalse();
+ assertThat(emptyChunkInList).isFalse();
+ }
+
+ @Test
+ public void testGetChunkEntry_returnsEntryWithCorrectLength() throws Exception {
+ byte[] chunkListingProto =
+ createChunkListingProto(
+ new ChunkHash[] {mChunkHashA, mChunkHashB, mChunkHashC},
+ new int[] {CHUNK_A_LENGTH, CHUNK_B_LENGTH, CHUNK_C_LENGTH});
+ ChunkListing chunkListing =
+ ChunkListing.readFromProto(
+ new ProtoInputStream(new ByteArrayInputStream(chunkListingProto)));
+
+ ChunkListing.Entry entryA = chunkListing.getChunkEntry(mChunkHashA);
+ ChunkListing.Entry entryB = chunkListing.getChunkEntry(mChunkHashB);
+ ChunkListing.Entry entryC = chunkListing.getChunkEntry(mChunkHashC);
+
+ assertThat(entryA.getLength()).isEqualTo(CHUNK_A_LENGTH);
+ assertThat(entryB.getLength()).isEqualTo(CHUNK_B_LENGTH);
+ assertThat(entryC.getLength()).isEqualTo(CHUNK_C_LENGTH);
+ }
+
+ @Test
+ public void testGetChunkEntry_returnsEntryWithCorrectStart() throws Exception {
+ byte[] chunkListingProto =
+ createChunkListingProto(
+ new ChunkHash[] {mChunkHashA, mChunkHashB, mChunkHashC},
+ new int[] {CHUNK_A_LENGTH, CHUNK_B_LENGTH, CHUNK_C_LENGTH});
+ ChunkListing chunkListing =
+ ChunkListing.readFromProto(
+ new ProtoInputStream(new ByteArrayInputStream(chunkListingProto)));
+
+ ChunkListing.Entry entryA = chunkListing.getChunkEntry(mChunkHashA);
+ ChunkListing.Entry entryB = chunkListing.getChunkEntry(mChunkHashB);
+ ChunkListing.Entry entryC = chunkListing.getChunkEntry(mChunkHashC);
+
+ assertThat(entryA.getStart()).isEqualTo(0);
+ assertThat(entryB.getStart()).isEqualTo(CHUNK_A_LENGTH);
+ assertThat(entryC.getStart()).isEqualTo(CHUNK_A_LENGTH + CHUNK_B_LENGTH);
+ }
+
+ @Test
+ public void testGetChunkEntry_returnsNullForNonExistentChunk() throws Exception {
+ byte[] chunkListingProto =
+ createChunkListingProto(
+ new ChunkHash[] {mChunkHashA, mChunkHashB},
+ new int[] {CHUNK_A_LENGTH, CHUNK_B_LENGTH});
+ ChunkListing chunkListing =
+ ChunkListing.readFromProto(
+ new ProtoInputStream(new ByteArrayInputStream(chunkListingProto)));
+
+ ChunkListing.Entry chunkEntryNonexistentChunk = chunkListing.getChunkEntry(mChunkHashC);
+
+ assertThat(chunkEntryNonexistentChunk).isNull();
+ }
+
+ @Test
+ public void testReadFromProto_whenEmptyProto_returnsChunkListingWith0Chunks() throws Exception {
+ ProtoInputStream emptyProto = new ProtoInputStream(new ByteArrayInputStream(new byte[] {}));
+
+ ChunkListing chunkListing = ChunkListing.readFromProto(emptyProto);
+
+ assertThat(chunkListing.getChunkCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void testReadFromProto_returnsChunkListingWithCorrectSize() throws Exception {
+ byte[] chunkListingProto =
+ createChunkListingProto(
+ new ChunkHash[] {mChunkHashA, mChunkHashB, mChunkHashC},
+ new int[] {CHUNK_A_LENGTH, CHUNK_B_LENGTH, CHUNK_C_LENGTH});
+
+ ChunkListing chunkListing =
+ ChunkListing.readFromProto(
+ new ProtoInputStream(new ByteArrayInputStream(chunkListingProto)));
+
+ assertThat(chunkListing.getChunkCount()).isEqualTo(3);
+ }
+
+ private byte[] createChunkListingProto(ChunkHash[] hashes, int[] lengths) {
+ Preconditions.checkArgument(hashes.length == lengths.length);
+ ProtoOutputStream outputStream = new ProtoOutputStream();
+
+ for (int i = 0; i < hashes.length; ++i) {
+ writeToProtoOutputStream(outputStream, hashes[i], lengths[i]);
+ }
+ outputStream.flush();
+
+ return outputStream.getBytes();
+ }
+
+ private void writeToProtoOutputStream(ProtoOutputStream out, ChunkHash chunkHash, int length) {
+ long token = out.start(ChunksMetadataProto.ChunkListing.CHUNKS);
+ out.write(ChunksMetadataProto.Chunk.HASH, chunkHash.getHash());
+ out.write(ChunksMetadataProto.Chunk.LENGTH, length);
+ out.end(token);
+ }
+
+ private ChunkHash getHash(String name) {
+ return new ChunkHash(
+ Arrays.copyOf(name.getBytes(Charsets.UTF_8), ChunkHash.HASH_LENGTH_BYTES));
+ }
+}
diff --git a/services/robotests/src/com/android/server/backup/encryption/chunk/ChunkTest.java b/services/robotests/src/com/android/server/backup/encryption/chunk/ChunkTest.java
new file mode 100644
index 0000000..1dd7dc8
--- /dev/null
+++ b/services/robotests/src/com/android/server/backup/encryption/chunk/ChunkTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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 android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+import com.google.common.base.Charsets;
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 26)
+// Include android.util.proto in addition to classes under test because the latest versions of
+// android.util.proto.Proto{Input|Output}Stream are not part of Robolectric.
+@SystemLoaderPackages({"com.android.server.backup", "android.util.proto"})
+@Presubmit
+public class ChunkTest {
+ private static final String CHUNK_A = "CHUNK_A";
+ private static final int CHUNK_A_LENGTH = 256;
+
+ private ChunkHash mChunkHashA;
+
+ @Before
+ public void setUp() throws Exception {
+ mChunkHashA = getHash(CHUNK_A);
+ }
+
+ @Test
+ public void testReadFromProto_readsCorrectly() throws Exception {
+ ProtoOutputStream out = new ProtoOutputStream();
+ out.write(ChunksMetadataProto.Chunk.HASH, mChunkHashA.getHash());
+ out.write(ChunksMetadataProto.Chunk.LENGTH, CHUNK_A_LENGTH);
+ out.flush();
+ byte[] protoBytes = out.getBytes();
+
+ Chunk chunk =
+ Chunk.readFromProto(new ProtoInputStream(new ByteArrayInputStream(protoBytes)));
+
+ assertThat(chunk.getHash()).isEqualTo(mChunkHashA.getHash());
+ assertThat(chunk.getLength()).isEqualTo(CHUNK_A_LENGTH);
+ }
+
+ @Test
+ public void testReadFromProto_whenFieldsWrittenInReversedOrder_readsCorrectly()
+ throws Exception {
+ ProtoOutputStream out = new ProtoOutputStream();
+ // Write fields of Chunk proto in reverse order.
+ out.write(ChunksMetadataProto.Chunk.LENGTH, CHUNK_A_LENGTH);
+ out.write(ChunksMetadataProto.Chunk.HASH, mChunkHashA.getHash());
+ out.flush();
+ byte[] protoBytes = out.getBytes();
+
+ Chunk chunk =
+ Chunk.readFromProto(new ProtoInputStream(new ByteArrayInputStream(protoBytes)));
+
+ assertThat(chunk.getHash()).isEqualTo(mChunkHashA.getHash());
+ assertThat(chunk.getLength()).isEqualTo(CHUNK_A_LENGTH);
+ }
+
+ @Test
+ public void testReadFromProto_whenEmptyProto_returnsEmptyHash() throws Exception {
+ ProtoInputStream emptyProto = new ProtoInputStream(new ByteArrayInputStream(new byte[] {}));
+
+ Chunk chunk = Chunk.readFromProto(emptyProto);
+
+ assertThat(chunk.getHash()).asList().hasSize(0);
+ assertThat(chunk.getLength()).isEqualTo(0);
+ }
+
+ @Test
+ public void testReadFromProto_whenOnlyHashSet_returnsChunkWithOnlyHash() throws Exception {
+ ProtoOutputStream out = new ProtoOutputStream();
+ out.write(ChunksMetadataProto.Chunk.HASH, mChunkHashA.getHash());
+ out.flush();
+ byte[] protoBytes = out.getBytes();
+
+ Chunk chunk =
+ Chunk.readFromProto(new ProtoInputStream(new ByteArrayInputStream(protoBytes)));
+
+ assertThat(chunk.getHash()).isEqualTo(mChunkHashA.getHash());
+ assertThat(chunk.getLength()).isEqualTo(0);
+ }
+
+ @Test
+ public void testReadFromProto_whenOnlyLengthSet_returnsChunkWithOnlyLength() throws Exception {
+ ProtoOutputStream out = new ProtoOutputStream();
+ out.write(ChunksMetadataProto.Chunk.LENGTH, CHUNK_A_LENGTH);
+ out.flush();
+ byte[] protoBytes = out.getBytes();
+
+ Chunk chunk =
+ Chunk.readFromProto(new ProtoInputStream(new ByteArrayInputStream(protoBytes)));
+
+ assertThat(chunk.getHash()).isEqualTo(new byte[] {});
+ assertThat(chunk.getLength()).isEqualTo(CHUNK_A_LENGTH);
+ }
+
+ private ChunkHash getHash(String name) {
+ return new ChunkHash(
+ Arrays.copyOf(name.getBytes(Charsets.UTF_8), ChunkHash.HASH_LENGTH_BYTES));
+ }
+}
diff --git a/services/robotests/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java b/services/robotests/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java
new file mode 100644
index 0000000..1cd1528
--- /dev/null
+++ b/services/robotests/src/com/android/server/backup/encryption/chunk/EncryptedChunkOrderingTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderPackages;
+import com.google.common.primitives.Bytes;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.annotation.Config;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(manifest = Config.NONE, sdk = 26)
+@SystemLoaderPackages({"com.android.server.backup"})
+@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);
+ }
+}