summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Bram Bonné <brambonne@google.com> 2019-07-04 14:56:05 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2019-07-04 14:56:05 +0000
commit65b475b64cceee1d18591749f87f0f57276bee14 (patch)
treeaf3f5f3664e621eedd8a3ebedaf21287934cc785
parent4dd6a45d3c77832633707cff562faf96fab03420 (diff)
parent8b8c2d68829e261deae4ccb688e54b79df4b7032 (diff)
Merge "Ports DecryptedChunkFileOutput and related classes."
-rw-r--r--services/backup/java/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java87
-rw-r--r--services/backup/java/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java2
-rw-r--r--services/backup/java/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java54
-rw-r--r--services/robotests/backup/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java134
4 files changed, 276 insertions, 1 deletions
diff --git a/services/backup/java/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java b/services/backup/java/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java
new file mode 100644
index 000000000000..ae2e150de4bc
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutput.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.chunking;
+
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Writes plaintext chunks to a file, building a digest of the plaintext of the resulting file. */
+public class DecryptedChunkFileOutput implements DecryptedChunkOutput {
+ @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";
+
+ private final File mOutputFile;
+ private final MessageDigest mMessageDigest;
+ @Nullable private FileOutputStream mFileOutputStream;
+ private boolean mClosed;
+ @Nullable private byte[] mDigest;
+
+ /**
+ * Constructs a new instance which writes chunks to the given file and uses the default message
+ * digest algorithm.
+ */
+ public DecryptedChunkFileOutput(File outputFile) {
+ mOutputFile = outputFile;
+ try {
+ mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError(
+ "Impossible condition: JCE thinks it does not support AES.", e);
+ }
+ }
+
+ @Override
+ public DecryptedChunkOutput open() throws IOException {
+ checkState(mFileOutputStream == null, "Cannot open twice");
+ mFileOutputStream = new FileOutputStream(mOutputFile);
+ return this;
+ }
+
+ @Override
+ public void processChunk(byte[] plaintextBuffer, int length) throws IOException {
+ checkState(mFileOutputStream != null, "Must open before processing chunks");
+ mFileOutputStream.write(plaintextBuffer, /*off=*/ 0, length);
+ mMessageDigest.update(plaintextBuffer, /*offset=*/ 0, length);
+ }
+
+ @Override
+ public byte[] getDigest() {
+ checkState(mClosed, "Must close before getting mDigest");
+
+ // After the first call to mDigest() the MessageDigest is reset, thus we must store the
+ // result.
+ if (mDigest == null) {
+ mDigest = mMessageDigest.digest();
+ }
+ return mDigest;
+ }
+
+ @Override
+ public void close() throws IOException {
+ mFileOutputStream.close();
+ mClosed = true;
+ }
+}
diff --git a/services/backup/java/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java b/services/backup/java/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
index ebf09dfd6ba6..a425c720b9b8 100644
--- a/services/backup/java/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
+++ b/services/backup/java/com/android/server/backup/encryption/keys/TertiaryKeyGenerator.java
@@ -35,7 +35,7 @@ public class TertiaryKeyGenerator {
mKeyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
mKeyGenerator.init(KEY_SIZE_BITS, secureRandom);
} catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(
+ throw new AssertionError(
"Impossible condition: JCE thinks it does not support AES.", e);
}
}
diff --git a/services/backup/java/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java b/services/backup/java/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
new file mode 100644
index 000000000000..e3df3c1eb96f
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/encryption/tasks/DecryptedChunkOutput.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.tasks;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+
+/**
+ * Accepts the plaintext bytes of decrypted chunks and writes them to some output. Also keeps track
+ * of the message digest of the chunks.
+ */
+public interface DecryptedChunkOutput extends Closeable {
+ /**
+ * Opens whatever output the implementation chooses, ready to process chunks.
+ *
+ * @return {@code this}, to allow use with try-with-resources
+ */
+ DecryptedChunkOutput open() throws IOException;
+
+ /**
+ * Writes the plaintext bytes of chunk to whatever output the implementation chooses. Also
+ * updates the digest with the chunk.
+ *
+ * <p>You must call {@link #open()} before this method, and you may not call it after calling
+ * {@link Closeable#close()}.
+ *
+ * @param plaintextBuffer An array containing the bytes of the plaintext of the chunk, starting
+ * at index 0.
+ * @param length The length in bytes of the plaintext contained in {@code plaintextBuffer}.
+ */
+ void processChunk(byte[] plaintextBuffer, int length) throws IOException, InvalidKeyException;
+
+ /**
+ * Returns the message digest of all the chunks processed by {@link #processChunk}.
+ *
+ * <p>You must call {@link Closeable#close()} before calling this method.
+ */
+ byte[] getDigest();
+}
diff --git a/services/robotests/backup/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java b/services/robotests/backup/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java
new file mode 100644
index 000000000000..823a63c22da4
--- /dev/null
+++ b/services/robotests/backup/src/com/android/server/backup/encryption/chunking/DecryptedChunkFileOutputTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.backup.encryption.chunking;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;
+
+import com.google.common.io.Files;
+import com.google.common.primitives.Bytes;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.MessageDigest;
+import java.util.Arrays;
+
+@RunWith(RobolectricTestRunner.class)
+@Presubmit
+public class DecryptedChunkFileOutputTest {
+ private static final byte[] TEST_CHUNK_1 = {1, 2, 3};
+ private static final byte[] TEST_CHUNK_2 = {4, 5, 6, 7, 8, 9, 10};
+ private static final int TEST_BUFFER_LENGTH =
+ Math.max(TEST_CHUNK_1.length, TEST_CHUNK_2.length);
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private File mOutputFile;
+ private DecryptedChunkFileOutput mDecryptedChunkFileOutput;
+
+ @Before
+ public void setUp() throws Exception {
+ mOutputFile = temporaryFolder.newFile();
+ mDecryptedChunkFileOutput = new DecryptedChunkFileOutput(mOutputFile);
+ }
+
+ @Test
+ public void open_returnsInstance() throws Exception {
+ DecryptedChunkOutput result = mDecryptedChunkFileOutput.open();
+ assertThat(result).isEqualTo(mDecryptedChunkFileOutput);
+ }
+
+ @Test
+ public void open_nonExistentOutputFolder_throwsException() throws Exception {
+ mDecryptedChunkFileOutput =
+ new DecryptedChunkFileOutput(
+ new File(temporaryFolder.newFolder(), "mOutput/directory"));
+ assertThrows(FileNotFoundException.class, () -> mDecryptedChunkFileOutput.open());
+ }
+
+ @Test
+ public void open_whenRunTwice_throwsException() throws Exception {
+ mDecryptedChunkFileOutput.open();
+ assertThrows(IllegalStateException.class, () -> mDecryptedChunkFileOutput.open());
+ }
+
+ @Test
+ public void processChunk_beforeOpen_throwsException() throws Exception {
+ assertThrows(IllegalStateException.class,
+ () -> mDecryptedChunkFileOutput.processChunk(new byte[0], 0));
+ }
+
+ @Test
+ public void processChunk_writesChunksToFile() throws Exception {
+ processTestChunks();
+
+ assertThat(Files.toByteArray(mOutputFile))
+ .isEqualTo(Bytes.concat(TEST_CHUNK_1, TEST_CHUNK_2));
+ }
+
+ @Test
+ public void getDigest_beforeClose_throws() throws Exception {
+ mDecryptedChunkFileOutput.open();
+ assertThrows(IllegalStateException.class, () -> mDecryptedChunkFileOutput.getDigest());
+ }
+
+ @Test
+ public void getDigest_returnsCorrectDigest() throws Exception {
+ processTestChunks();
+
+ byte[] actualDigest = mDecryptedChunkFileOutput.getDigest();
+
+ MessageDigest expectedDigest =
+ MessageDigest.getInstance(DecryptedChunkFileOutput.DIGEST_ALGORITHM);
+ expectedDigest.update(TEST_CHUNK_1);
+ expectedDigest.update(TEST_CHUNK_2);
+ assertThat(actualDigest).isEqualTo(expectedDigest.digest());
+ }
+
+ @Test
+ public void getDigest_whenRunTwice_returnsIdenticalDigestBothTimes() throws Exception {
+ processTestChunks();
+
+ byte[] digest1 = mDecryptedChunkFileOutput.getDigest();
+ byte[] digest2 = mDecryptedChunkFileOutput.getDigest();
+
+ assertThat(digest1).isEqualTo(digest2);
+ }
+
+ private void processTestChunks() throws IOException {
+ mDecryptedChunkFileOutput.open();
+ mDecryptedChunkFileOutput.processChunk(Arrays.copyOf(TEST_CHUNK_1, TEST_BUFFER_LENGTH),
+ TEST_CHUNK_1.length);
+ mDecryptedChunkFileOutput.processChunk(Arrays.copyOf(TEST_CHUNK_2, TEST_BUFFER_LENGTH),
+ TEST_CHUNK_2.length);
+ mDecryptedChunkFileOutput.close();
+ }
+}