• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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