Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 65b475b6 authored by Bram Bonné's avatar Bram Bonné Committed by Android (Google) Code Review
Browse files

Merge "Ports DecryptedChunkFileOutput and related classes."

parents 4dd6a45d 8b8c2d68
Loading
Loading
Loading
Loading
+87 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.backup.encryption.chunking;

import static com.android.internal.util.Preconditions.checkState;

import android.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.tasks.DecryptedChunkOutput;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/** Writes plaintext chunks to a file, building a digest of the plaintext of the resulting file. */
public class DecryptedChunkFileOutput implements DecryptedChunkOutput {
    @VisibleForTesting static final String DIGEST_ALGORITHM = "SHA-256";

    private final File mOutputFile;
    private final MessageDigest mMessageDigest;
    @Nullable private FileOutputStream mFileOutputStream;
    private boolean mClosed;
    @Nullable private byte[] mDigest;

    /**
     * Constructs a new instance which writes chunks to the given file and uses the default message
     * digest algorithm.
     */
    public DecryptedChunkFileOutput(File outputFile) {
        mOutputFile = outputFile;
        try {
            mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            throw new AssertionError(
                    "Impossible condition: JCE thinks it does not support AES.", e);
        }
    }

    @Override
    public DecryptedChunkOutput open() throws IOException {
        checkState(mFileOutputStream == null, "Cannot open twice");
        mFileOutputStream = new FileOutputStream(mOutputFile);
        return this;
    }

    @Override
    public void processChunk(byte[] plaintextBuffer, int length) throws IOException {
        checkState(mFileOutputStream != null, "Must open before processing chunks");
        mFileOutputStream.write(plaintextBuffer, /*off=*/ 0, length);
        mMessageDigest.update(plaintextBuffer, /*offset=*/ 0, length);
    }

    @Override
    public byte[] getDigest() {
        checkState(mClosed, "Must close before getting mDigest");

        // After the first call to mDigest() the MessageDigest is reset, thus we must store the
        // result.
        if (mDigest == null) {
            mDigest = mMessageDigest.digest();
        }
        return mDigest;
    }

    @Override
    public void close() throws IOException {
        mFileOutputStream.close();
        mClosed = true;
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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);
        }
    }
+54 −0
Original line number Diff line number Diff line
/*
 * 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();
}
+134 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}