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

Commit 5dffe468 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Ports second part of the chunking code from gmscore to AOSP."

parents 56b402c4 84c66b8e
Loading
Loading
Loading
Loading
+90 −0
Original line number Diff line number Diff line
/*
 * 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.chunking;

import com.android.server.backup.encryption.chunk.ChunkHash;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

/** Encrypts chunks of a file using AES/GCM. */
public class ChunkEncryptor {
    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 final SecretKey mSecretKey;
    private final SecureRandom mSecureRandom;

    /**
     * A new instance using {@code mSecretKey} to encrypt chunks and {@code mSecureRandom} to
     * generate nonces.
     */
    public ChunkEncryptor(SecretKey secretKey, SecureRandom secureRandom) {
        this.mSecretKey = secretKey;
        this.mSecureRandom = secureRandom;
    }

    /**
     * Transforms {@code plaintext} into an {@link EncryptedChunk}.
     *
     * @param plaintextHash The hash of the plaintext to encrypt, to attach as the key of the chunk.
     * @param plaintext Bytes to encrypt.
     * @throws InvalidKeyException If the given secret key is not a valid AES key for decryption.
     * @throws IllegalBlockSizeException If the input data cannot be encrypted using
     *     AES/GCM/NoPadding. This should never be the case.
     */
    public EncryptedChunk encrypt(ChunkHash plaintextHash, byte[] plaintext)
            throws InvalidKeyException, IllegalBlockSizeException {
        byte[] nonce = generateNonce();
        Cipher cipher;
        try {
            cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(
                    Cipher.ENCRYPT_MODE,
                    mSecretKey,
                    new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, nonce));
        } catch (NoSuchAlgorithmException
                | NoSuchPaddingException
                | InvalidAlgorithmParameterException e) {
            // This can not happen - AES/GCM/NoPadding is supported.
            throw new AssertionError(e);
        }
        byte[] encryptedBytes;
        try {
            encryptedBytes = cipher.doFinal(plaintext);
        } catch (BadPaddingException e) {
            // This can not happen - BadPaddingException can only be thrown in decrypt mode.
            throw new AssertionError("Impossible: threw BadPaddingException in encrypt mode.");
        }

        return EncryptedChunk.create(/*key=*/ plaintextHash, nonce, encryptedBytes);
    }

    private byte[] generateNonce() {
        byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES];
        mSecureRandom.nextBytes(nonce);
        return nonce;
    }
}
+47 −0
Original line number Diff line number Diff line
/*
 * 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.chunking;

import com.android.server.backup.encryption.chunk.ChunkHash;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;

/** Computes the SHA-256 HMAC of a chunk of bytes. */
public class ChunkHasher {
    private static final String MAC_ALGORITHM = "HmacSHA256";

    private final SecretKey mSecretKey;

    /** Constructs a new hasher which computes the HMAC using the given secret key. */
    public ChunkHasher(SecretKey secretKey) {
        this.mSecretKey = secretKey;
    }

    /** Returns the SHA-256 over the given bytes. */
    public ChunkHash computeHash(byte[] plaintext) throws InvalidKeyException {
        try {
            Mac mac = Mac.getInstance(MAC_ALGORITHM);
            mac.init(mSecretKey);
            return new ChunkHash(mac.doFinal(plaintext));
        } catch (NoSuchAlgorithmException e) {
            // This can not happen - AES/GCM/NoPadding is available as part of the framework.
            throw new AssertionError(e);
        }
    }
}
+46 −0
Original line number Diff line number Diff line
/*
 * 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.chunking;

import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;

/** Splits an input stream into chunks, which are to be encrypted separately. */
public interface Chunker {
    /**
     * Splits the input stream into chunks.
     *
     * @param inputStream The input stream.
     * @param chunkConsumer A function that processes each chunk as it is produced.
     * @throws IOException If there is a problem reading the input stream.
     * @throws GeneralSecurityException if the consumer function throws an error.
     */
    void chunkify(InputStream inputStream, ChunkConsumer chunkConsumer)
            throws IOException, GeneralSecurityException;

    /** Function that consumes chunks. */
    interface ChunkConsumer {
        /**
         * Invoked for each chunk.
         *
         * @param chunk Plaintext bytes of chunk.
         * @throws GeneralSecurityException if there is an issue encrypting the chunk.
         */
        void accept(byte[] chunk) throws GeneralSecurityException;
    }
}
+52 −0
Original line number Diff line number Diff line
/*
 * 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.chunking;

import java.io.IOException;
import java.io.OutputStream;

/** Writes data straight to an output stream. */
public class RawBackupWriter implements BackupWriter {
    private final OutputStream outputStream;
    private long bytesWritten;

    /** Constructs a new writer which writes bytes to the given output stream. */
    public RawBackupWriter(OutputStream outputStream) {
        this.outputStream = outputStream;
    }

    @Override
    public void writeBytes(byte[] bytes) throws IOException {
        outputStream.write(bytes);
        bytesWritten += bytes.length;
    }

    @Override
    public void writeChunk(long start, int length) throws IOException {
        throw new UnsupportedOperationException("RawBackupWriter cannot write existing chunks");
    }

    @Override
    public long getBytesWritten() {
        return bytesWritten;
    }

    @Override
    public void flush() throws IOException {
        outputStream.flush();
    }
}
+154 −0
Original line number Diff line number Diff line
/*
 * 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.chunking;

import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;

import android.platform.test.annotations.Presubmit;
import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.testing.FrameworkRobolectricTestRunner;
import com.android.server.testing.SystemLoaderPackages;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.annotation.Config;

@RunWith(FrameworkRobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = 26)
@SystemLoaderPackages({"com.android.server.backup"})
@Presubmit
public class ChunkEncryptorTest {
    private static final String MAC_ALGORITHM = "HmacSHA256";
    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 String CHUNK_PLAINTEXT =
            "A little Learning is a dang'rous Thing;\n"
                    + "Drink deep, or taste not the Pierian Spring:\n"
                    + "There shallow Draughts intoxicate the Brain,\n"
                    + "And drinking largely sobers us again.";
    private static final byte[] PLAINTEXT_BYTES = CHUNK_PLAINTEXT.getBytes(UTF_8);
    private static final byte[] NONCE_1 = "0123456789abc".getBytes(UTF_8);
    private static final byte[] NONCE_2 = "123456789abcd".getBytes(UTF_8);

    private static final byte[][] NONCES = new byte[][] {NONCE_1, NONCE_2};

    @Mock private SecureRandom mSecureRandomMock;
    private SecretKey mSecretKey;
    private ChunkHash mPlaintextHash;
    private ChunkEncryptor mChunkEncryptor;

    @Before
    public void setUp() throws Exception {
        mSecretKey = generateAesKey();
        ChunkHasher chunkHasher = new ChunkHasher(mSecretKey);
        mPlaintextHash = chunkHasher.computeHash(PLAINTEXT_BYTES);
        mSecureRandomMock = mock(SecureRandom.class);
        mChunkEncryptor = new ChunkEncryptor(mSecretKey, mSecureRandomMock);

        // Return NONCE_1, then NONCE_2 for invocations of mSecureRandomMock.nextBytes().
        doAnswer(
                        new Answer<Void>() {
                            private int mInvocation = 0;

                            @Override
                            public Void answer(InvocationOnMock invocation) {
                                byte[] nonceDestination = invocation.getArgument(0);
                                System.arraycopy(
                                        NONCES[this.mInvocation],
                                        0,
                                        nonceDestination,
                                        0,
                                        GCM_NONCE_LENGTH_BYTES);
                                this.mInvocation++;
                                return null;
                            }
                        })
                .when(mSecureRandomMock)
                .nextBytes(any(byte[].class));
    }

    @Test
    public void encrypt_withHash_resultContainsHashAsKey() throws Exception {
        EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        assertThat(chunk.key()).isEqualTo(mPlaintextHash);
    }

    @Test
    public void encrypt_generatesHmacOfPlaintext() throws Exception {
        EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        byte[] generatedHash = chunk.key().getHash();
        Mac mac = Mac.getInstance(MAC_ALGORITHM);
        mac.init(mSecretKey);
        byte[] plaintextHmac = mac.doFinal(PLAINTEXT_BYTES);
        assertThat(generatedHash).isEqualTo(plaintextHmac);
    }

    @Test
    public void encrypt_whenInvokedAgain_generatesNewNonce() throws Exception {
        EncryptedChunk chunk1 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        EncryptedChunk chunk2 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        assertThat(chunk1.nonce()).isNotEqualTo(chunk2.nonce());
    }

    @Test
    public void encrypt_whenInvokedAgain_generatesNewCiphertext() throws Exception {
        EncryptedChunk chunk1 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        EncryptedChunk chunk2 = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        assertThat(chunk1.encryptedBytes()).isNotEqualTo(chunk2.encryptedBytes());
    }

    @Test
    public void encrypt_generates12ByteNonce() throws Exception {
        EncryptedChunk encryptedChunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        byte[] nonce = encryptedChunk.nonce();
        assertThat(nonce).hasLength(GCM_NONCE_LENGTH_BYTES);
    }

    @Test
    public void encrypt_decryptedResultCorrespondsToPlaintext() throws Exception {
        EncryptedChunk chunk = mChunkEncryptor.encrypt(mPlaintextHash, PLAINTEXT_BYTES);

        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        cipher.init(
                Cipher.DECRYPT_MODE,
                mSecretKey,
                new GCMParameterSpec(GCM_TAG_LENGTH_BYTES * 8, chunk.nonce()));
        byte[] decrypted = cipher.doFinal(chunk.encryptedBytes());
        assertThat(decrypted).isEqualTo(PLAINTEXT_BYTES);
    }
}
Loading