blob: 19e3b28f85e7cba14a6460cee8e3e94e0616a373 [file] [log] [blame]
/*
* 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);
}
}