1 // Copyright 2017 Google Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.subtle; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.junit.Assert.assertArrayEquals; 21 import static org.junit.Assert.assertEquals; 22 import static org.junit.Assert.assertFalse; 23 import static org.junit.Assert.assertThrows; 24 25 import com.google.crypto.tink.Aead; 26 import com.google.crypto.tink.InsecureSecretKeyAccess; 27 import com.google.crypto.tink.aead.ChaCha20Poly1305Key; 28 import com.google.crypto.tink.aead.ChaCha20Poly1305Parameters; 29 import com.google.crypto.tink.config.TinkFips; 30 import com.google.crypto.tink.testing.TestUtil; 31 import com.google.crypto.tink.testing.TestUtil.BytesMutation; 32 import com.google.crypto.tink.testing.WycheproofTestUtil; 33 import com.google.crypto.tink.util.SecretBytes; 34 import com.google.gson.JsonArray; 35 import com.google.gson.JsonObject; 36 import java.security.GeneralSecurityException; 37 import java.security.InvalidKeyException; 38 import java.util.Arrays; 39 import java.util.HashSet; 40 import javax.crypto.AEADBadTagException; 41 import org.junit.Assume; 42 import org.junit.Test; 43 import org.junit.runner.RunWith; 44 import org.junit.runners.JUnit4; 45 46 /** Unit tests for ChaCha20Poly1305. */ 47 @RunWith(JUnit4.class) 48 public class ChaCha20Poly1305Test { 49 private static final int KEY_SIZE = 32; 50 createInstance(final byte[] key)51 public Aead createInstance(final byte[] key) throws GeneralSecurityException { 52 return new ChaCha20Poly1305(key); 53 } 54 55 @Test testSnufflePoly1305ThrowsIllegalArgExpWhenKeyLenIsGreaterThan32()56 public void testSnufflePoly1305ThrowsIllegalArgExpWhenKeyLenIsGreaterThan32() 57 throws InvalidKeyException { 58 Assume.assumeFalse(TinkFips.useOnlyFips()); 59 60 InvalidKeyException e = 61 assertThrows(InvalidKeyException.class, () -> createInstance(new byte[KEY_SIZE + 1])); 62 assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32."); 63 } 64 65 @Test testSnufflePoly1305ThrowsIllegalArgExpWhenKeyLenIsLessThan32()66 public void testSnufflePoly1305ThrowsIllegalArgExpWhenKeyLenIsLessThan32() 67 throws InvalidKeyException { 68 Assume.assumeFalse(TinkFips.useOnlyFips()); 69 70 InvalidKeyException e = 71 assertThrows(InvalidKeyException.class, () -> createInstance(new byte[KEY_SIZE - 1])); 72 assertThat(e).hasMessageThat().containsMatch("The key length in bytes must be 32."); 73 } 74 75 @Test testDecryptThrowsGeneralSecurityExpWhenCiphertextIsTooShort()76 public void testDecryptThrowsGeneralSecurityExpWhenCiphertextIsTooShort() 77 throws GeneralSecurityException { 78 Assume.assumeFalse(TinkFips.useOnlyFips()); 79 80 Aead cipher = createInstance(new byte[KEY_SIZE]); 81 GeneralSecurityException e = 82 assertThrows( 83 GeneralSecurityException.class, () -> cipher.decrypt(new byte[27], new byte[1])); 84 assertThat(e).hasMessageThat().containsMatch("ciphertext too short"); 85 } 86 87 @Test testEncryptDecrypt()88 public void testEncryptDecrypt() throws Exception { 89 Assume.assumeFalse(TinkFips.useOnlyFips()); 90 91 Aead aead = createInstance(Random.randBytes(KEY_SIZE)); 92 for (int i = 0; i < 100; i++) { 93 byte[] message = Random.randBytes(i); 94 byte[] aad = Random.randBytes(i); 95 byte[] ciphertext = aead.encrypt(message, aad); 96 byte[] decrypted = aead.decrypt(ciphertext, aad); 97 assertArrayEquals(message, decrypted); 98 } 99 } 100 101 @Test 102 /* BC had a bug, where GCM failed for messages of size > 8192 */ testLongMessages()103 public void testLongMessages() throws Exception { 104 Assume.assumeFalse(TinkFips.useOnlyFips()); 105 Assume.assumeFalse(TestUtil.isAndroid()); // Doesn't work on Android 106 107 int dataSize = 16; 108 while (dataSize <= (1 << 24)) { 109 byte[] plaintext = Random.randBytes(dataSize); 110 byte[] aad = Random.randBytes(dataSize / 3); 111 byte[] key = Random.randBytes(KEY_SIZE); 112 Aead aead = createInstance(key); 113 byte[] ciphertext = aead.encrypt(plaintext, aad); 114 byte[] decrypted = aead.decrypt(ciphertext, aad); 115 assertArrayEquals(plaintext, decrypted); 116 dataSize += 5 * dataSize / 11; 117 } 118 } 119 120 @Test testModifyCiphertext()121 public void testModifyCiphertext() throws Exception { 122 Assume.assumeFalse(TinkFips.useOnlyFips()); 123 124 byte[] key = Random.randBytes(KEY_SIZE); 125 Aead aead = createInstance(key); 126 byte[] aad = Random.randBytes(16); 127 byte[] message = Random.randBytes(32); 128 byte[] ciphertext = aead.encrypt(message, aad); 129 130 for (BytesMutation mutation : TestUtil.generateMutations(ciphertext)) { 131 assertThrows( 132 String.format( 133 "Decrypting modified ciphertext should fail : ciphertext = %s, aad = %s," 134 + " description = %s", 135 Hex.encode(mutation.value), Arrays.toString(aad), mutation.description), 136 GeneralSecurityException.class, 137 () -> { 138 byte[] unused = aead.decrypt(mutation.value, aad); 139 }); 140 } 141 142 // Modify AAD 143 for (int b = 0; b < aad.length; b++) { 144 for (int bit = 0; bit < 8; bit++) { 145 byte[] modified = Arrays.copyOf(aad, aad.length); 146 modified[b] ^= (byte) (1 << bit); 147 assertThrows( 148 AEADBadTagException.class, 149 () -> { 150 byte[] unused = aead.decrypt(ciphertext, modified); 151 }); 152 } 153 } 154 } 155 156 @Test testNullPlaintextOrCiphertext()157 public void testNullPlaintextOrCiphertext() throws Exception { 158 Assume.assumeFalse(TinkFips.useOnlyFips()); 159 160 Aead aead = createInstance(Random.randBytes(KEY_SIZE)); 161 byte[] aad = new byte[] {1, 2, 3}; 162 assertThrows( 163 NullPointerException.class, 164 () -> { 165 byte[] unused = aead.encrypt(null, aad); 166 }); 167 assertThrows( 168 NullPointerException.class, 169 () -> { 170 byte[] unused = aead.encrypt(null, null); 171 }); 172 assertThrows( 173 NullPointerException.class, 174 () -> { 175 byte[] unused = aead.decrypt(null, aad); 176 }); 177 assertThrows( 178 NullPointerException.class, 179 () -> { 180 byte[] unused = aead.decrypt(null, null); 181 }); 182 } 183 184 @Test testEmptyAssociatedData()185 public void testEmptyAssociatedData() throws Exception { 186 Assume.assumeFalse(TinkFips.useOnlyFips()); 187 188 byte[] aad = new byte[0]; 189 Aead aead = createInstance(Random.randBytes(KEY_SIZE)); 190 for (int messageSize = 0; messageSize < 75; messageSize++) { 191 byte[] message = Random.randBytes(messageSize); 192 { // encrypting with aad as a 0-length array 193 byte[] ciphertext = aead.encrypt(message, aad); 194 byte[] decrypted = aead.decrypt(ciphertext, aad); 195 assertArrayEquals(message, decrypted); 196 byte[] decrypted2 = aead.decrypt(ciphertext, null); 197 assertArrayEquals(message, decrypted2); 198 byte[] badAad = new byte[] {1, 2, 3}; 199 assertThrows( 200 AEADBadTagException.class, 201 () -> { 202 byte[] unused = aead.decrypt(ciphertext, badAad); 203 }); 204 } 205 { // encrypting with aad equal to null 206 byte[] ciphertext = aead.encrypt(message, null); 207 byte[] decrypted = aead.decrypt(ciphertext, aad); 208 assertArrayEquals(message, decrypted); 209 byte[] decrypted2 = aead.decrypt(ciphertext, null); 210 assertArrayEquals(message, decrypted2); 211 byte[] badAad = new byte[] {1, 2, 3}; 212 assertThrows( 213 AEADBadTagException.class, 214 () -> { 215 byte[] unused = aead.decrypt(ciphertext, badAad); 216 }); 217 } 218 } 219 } 220 221 /** 222 * This is a very simple test for the randomness of the nonce. The test simply checks that the 223 * multiple ciphertexts of the same message are distinct. 224 */ 225 @Test testRandomNonce()226 public void testRandomNonce() throws Exception { 227 Assume.assumeFalse(TinkFips.useOnlyFips()); 228 229 byte[] key = Random.randBytes(KEY_SIZE); 230 Aead aead = createInstance(key); 231 byte[] message = new byte[0]; 232 byte[] aad = new byte[0]; 233 HashSet<String> ciphertexts = new HashSet<String>(); 234 final int samples = 1 << 10; 235 for (int i = 0; i < samples; i++) { 236 byte[] ct = aead.encrypt(message, aad); 237 String ctHex = Hex.encode(ct); 238 assertFalse(ciphertexts.contains(ctHex)); 239 ciphertexts.add(ctHex); 240 } 241 assertEquals(samples, ciphertexts.size()); 242 } 243 244 @Test testWycheproofVectors()245 public void testWycheproofVectors() throws Exception { 246 Assume.assumeFalse(TinkFips.useOnlyFips()); 247 248 JsonObject json = 249 WycheproofTestUtil.readJson( 250 "../wycheproof/testvectors/chacha20_poly1305_test.json"); 251 int errors = 0; 252 JsonArray testGroups = json.getAsJsonArray("testGroups"); 253 for (int i = 0; i < testGroups.size(); i++) { 254 JsonObject group = testGroups.get(i).getAsJsonObject(); 255 JsonArray tests = group.getAsJsonArray("tests"); 256 for (int j = 0; j < tests.size(); j++) { 257 JsonObject testcase = tests.get(j).getAsJsonObject(); 258 String tcId = 259 String.format( 260 "testcase %d (%s)", 261 testcase.get("tcId").getAsInt(), testcase.get("comment").getAsString()); 262 byte[] iv = Hex.decode(testcase.get("iv").getAsString()); 263 byte[] key = Hex.decode(testcase.get("key").getAsString()); 264 byte[] msg = Hex.decode(testcase.get("msg").getAsString()); 265 byte[] aad = Hex.decode(testcase.get("aad").getAsString()); 266 byte[] ct = Hex.decode(testcase.get("ct").getAsString()); 267 byte[] tag = Hex.decode(testcase.get("tag").getAsString()); 268 byte[] ciphertext = Bytes.concat(iv, ct, tag); 269 // Result is one of "valid", "invalid", "acceptable". 270 // "valid" are test vectors with matching plaintext, ciphertext and tag. 271 // "invalid" are test vectors with invalid parameters or invalid ciphertext and tag. 272 // "acceptable" are test vectors with weak parameters or legacy formats. 273 String result = testcase.get("result").getAsString(); 274 try { 275 Aead aead = createInstance(key); 276 byte[] decrypted = aead.decrypt(ciphertext, aad); 277 boolean eq = TestUtil.arrayEquals(decrypted, msg); 278 if (result.equals("invalid")) { 279 System.out.printf( 280 "FAIL %s: accepting invalid ciphertext, cleartext: %s, decrypted: %s%n", 281 tcId, Hex.encode(msg), Hex.encode(decrypted)); 282 errors++; 283 } else { 284 if (!eq) { 285 System.out.printf( 286 "FAIL %s: incorrect decryption, result: %s, expected: %s%n", 287 tcId, Hex.encode(decrypted), Hex.encode(msg)); 288 errors++; 289 } 290 } 291 } catch (GeneralSecurityException ex) { 292 if (result.equals("valid")) { 293 System.out.printf("FAIL %s: cannot decrypt, exception %s%n", tcId, ex); 294 errors++; 295 } 296 } 297 } 298 } 299 assertEquals(0, errors); 300 } 301 302 @Test testFailIfFipsModuleNotAvailable()303 public void testFailIfFipsModuleNotAvailable() throws Exception { 304 Assume.assumeTrue(TinkFips.useOnlyFips()); 305 306 byte[] key = Random.randBytes(32); 307 assertThrows(GeneralSecurityException.class, () -> new ChaCha20Poly1305(key)); 308 } 309 310 /** 311 * Test case taken from Wycheproof (testcase 4). 312 * https://github.com/google/wycheproof/blob/b063b4aedae951c69df014cd25fa6d69ae9e8cb9/testvectors/chacha20_poly1305_test.json#L57 313 */ 314 @Test testWithChaCha20Poly1305Key_noPrefix_works()315 public void testWithChaCha20Poly1305Key_noPrefix_works() throws Exception { 316 Assume.assumeFalse(TinkFips.useOnlyFips()); 317 byte[] plaintext = Hex.decode("2a"); 318 byte[] associatedData = Hex.decode(""); 319 byte[] keyBytes = 320 Hex.decode("cc56b680552eb75008f5484b4cb803fa5063ebd6eab91f6ab6aef4916a766273"); 321 ChaCha20Poly1305Key key = 322 ChaCha20Poly1305Key.create(SecretBytes.copyFrom(keyBytes, InsecureSecretKeyAccess.get())); 323 Aead aead = ChaCha20Poly1305.create(key); 324 byte[] ciphertext = aead.encrypt(plaintext, associatedData); 325 assertThat(ciphertext).hasLength(29); // msg (1) + iv(12) + tag(16) 326 327 assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext); 328 329 byte[] fixedCiphertext = 330 Hex.decode("99e23ec48985bccdeeab60f13acac27dec0968801e9f6eded69d807522"); 331 assertThat(aead.decrypt(fixedCiphertext, associatedData)).isEqualTo(plaintext); 332 } 333 334 @Test testWithChaCha20Poly1305Key_tinkPrefix_works()335 public void testWithChaCha20Poly1305Key_tinkPrefix_works() throws Exception { 336 Assume.assumeFalse(TinkFips.useOnlyFips()); 337 byte[] plaintext = Hex.decode("2a"); 338 byte[] associatedData = Hex.decode(""); 339 byte[] keyBytes = 340 Hex.decode("cc56b680552eb75008f5484b4cb803fa5063ebd6eab91f6ab6aef4916a766273"); 341 ChaCha20Poly1305Key key = 342 ChaCha20Poly1305Key.create( 343 ChaCha20Poly1305Parameters.Variant.TINK, 344 SecretBytes.copyFrom(keyBytes, InsecureSecretKeyAccess.get()), 345 0x99887766); 346 Aead aead = ChaCha20Poly1305.create(key); 347 byte[] ciphertext = aead.encrypt(plaintext, associatedData); 348 assertThat(ciphertext).hasLength(34); // prefix(5) + msg(1) + iv(12) + tag(16) 349 350 assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext); 351 352 byte[] fixedCiphertext = 353 Hex.decode("019988776699e23ec48985bccdeeab60f13acac27dec0968801e9f6eded69d807522"); 354 assertThat(aead.decrypt(fixedCiphertext, associatedData)).isEqualTo(plaintext); 355 } 356 357 @Test testWithChaCha20Poly1305Key_crunchyPrefix_works()358 public void testWithChaCha20Poly1305Key_crunchyPrefix_works() throws Exception { 359 Assume.assumeFalse(TinkFips.useOnlyFips()); 360 byte[] plaintext = Hex.decode("2a"); 361 byte[] associatedData = Hex.decode(""); 362 byte[] keyBytes = 363 Hex.decode("cc56b680552eb75008f5484b4cb803fa5063ebd6eab91f6ab6aef4916a766273"); 364 ChaCha20Poly1305Key key = 365 ChaCha20Poly1305Key.create( 366 ChaCha20Poly1305Parameters.Variant.CRUNCHY, 367 SecretBytes.copyFrom(keyBytes, InsecureSecretKeyAccess.get()), 368 0x99887766); 369 Aead aead = ChaCha20Poly1305.create(key); 370 byte[] ciphertext = aead.encrypt(plaintext, associatedData); 371 assertThat(ciphertext).hasLength(34); // prefix(5) + msg(1) + iv(12) + tag(16) 372 373 assertThat(aead.decrypt(ciphertext, associatedData)).isEqualTo(plaintext); 374 375 byte[] fixedCiphertext = 376 Hex.decode("009988776699e23ec48985bccdeeab60f13acac27dec0968801e9f6eded69d807522"); 377 assertThat(aead.decrypt(fixedCiphertext, associatedData)).isEqualTo(plaintext); 378 } 379 } 380