/*
 * 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 org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;

import static java.nio.charset.StandardCharsets.UTF_8;

import android.platform.test.annotations.Presubmit;

import com.android.server.backup.encryption.chunk.ChunkHash;

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.RobolectricTestRunner;

import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

@RunWith(RobolectricTestRunner.class)
@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);
    }
}
