1 /*
  2  * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 
 24 /**
 25  * @test
 26  * @bug 8153029
 27  * @library /test/lib
 28  * @run main ChaCha20NoReuse
 29  * @summary ChaCha20 Cipher Implementation (key/nonce reuse protection)
 30  */
 31 
 32 import java.util.*;
 33 import javax.crypto.Cipher;
 34 import java.security.spec.AlgorithmParameterSpec;
 35 import javax.crypto.spec.ChaCha20ParameterSpec;
 36 import javax.crypto.spec.IvParameterSpec;
 37 import javax.crypto.spec.SecretKeySpec;
 38 import javax.crypto.AEADBadTagException;
 39 import javax.crypto.SecretKey;
 40 import java.security.InvalidKeyException;
 41 
 42 public class ChaCha20NoReuse {
 43 
 44     private static final String ALG_CC20 = "ChaCha20";
 45     private static final String ALG_CC20_P1305 = "ChaCha20-Poly1305";
 46 
 47     /**
 48      * Basic TestMethod interface definition.
 49      */
 50     public interface TestMethod {
 51         /**
 52          * Runs the actual test case
 53          *
 54          * @param algorithm the algorithm to use (e.g. ChaCha20, etc.)
 55          *
 56          * @return true if the test passes, false otherwise.
 57          */
 58         boolean run(String algorithm);
 59 
 60         /**
 61          * Check if this TestMethod can be run for this algorithm.  Some tests
 62          * are specific to ChaCha20 or ChaCha20-Poly1305, so this method
 63          * can be used to determine if a given Cipher type is appropriate.
 64          *
 65          * @param algorithm the algorithm to use.
 66          *
 67          * @return true if this test can be run on this algorithm,
 68          * false otherwise.
 69          */
 70         boolean isValid(String algorithm);
 71     }
 72 
 73     public static class TestData {
 74         public TestData(String name, String keyStr, String nonceStr, int ctr,
 75                 int dir, String inputStr, String aadStr, String outStr) {
 76             testName = Objects.requireNonNull(name);
 77             Hex.Decoder decoder = Hex.decoder();
 78             key = decoder.decode(keyStr);
 79             nonce = decoder.decode(nonceStr);
 80             if ((counter = ctr) < 0) {
 81                 throw new IllegalArgumentException(
 82                         "counter must be 0 or greater");
 83             }
 84             direction = dir;
 85             if ((direction != Cipher.ENCRYPT_MODE) &&
 86                     (direction != Cipher.DECRYPT_MODE)) {
 87                 throw new IllegalArgumentException(
 88                         "Direction must be ENCRYPT_MODE or DECRYPT_MODE");
 89             }
 90             input = decoder.decode(inputStr);
 91             aad = (aadStr != null) ? decoder.decode(aadStr) : null;
 92             expOutput = decoder.decode(outStr);
 93         }
 94 
 95         public final String testName;
 96         public final byte[] key;
 97         public final byte[] nonce;
 98         public final int counter;
 99         public final int direction;
100         public final byte[] input;
101         public final byte[] aad;
102         public final byte[] expOutput;
103     }
104 
105     public static final List<TestData> testList = new LinkedList<TestData>() {{
106         add(new TestData("RFC 7539 Sample Test Vector [ENCRYPT]",
107             "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
108             "000000000000004a00000000",
109             1, Cipher.ENCRYPT_MODE,
110             "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" +
111             "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" +
112             "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" +
113             "637265656e20776f756c642062652069742e",
114             null,
115             "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b" +
116             "f91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d8" +
117             "07ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab7793736" +
118             "5af90bbf74a35be6b40b8eedf2785e42874d"));
119         add(new TestData("RFC 7539 Sample Test Vector [DECRYPT]",
120             "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
121             "000000000000004a00000000",
122             1, Cipher.DECRYPT_MODE,
123             "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b" +
124             "f91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d8" +
125             "07ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab7793736" +
126             "5af90bbf74a35be6b40b8eedf2785e42874d",
127             null,
128             "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" +
129             "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" +
130             "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" +
131             "637265656e20776f756c642062652069742e"));
132     }};
133 
134     public static final List<TestData> aeadTestList =
135             new LinkedList<TestData>() {{
136         add(new TestData("RFC 7539 Sample AEAD Test Vector",
137             "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
138             "070000004041424344454647",
139             1, Cipher.ENCRYPT_MODE,
140             "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" +
141             "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" +
142             "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" +
143             "637265656e20776f756c642062652069742e",
144             "50515253c0c1c2c3c4c5c6c7",
145             "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" +
146             "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" +
147             "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" +
148             "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" +
149             "0691"));
150         add(new TestData("RFC 7539 A.5 Sample Decryption",
151             "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
152             "000000000102030405060708",
153             1, Cipher.DECRYPT_MODE,
154             "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" +
155             "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" +
156             "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" +
157             "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" +
158             "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" +
159             "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" +
160             "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" +
161             "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" +
162             "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38",
163             "f33388860000000000004e91",
164             "496e7465726e65742d4472616674732061726520647261667420646f63756d65" +
165             "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" +
166             "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" +
167             "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" +
168             "6e747320617420616e792074696d652e20497420697320696e617070726f7072" +
169             "6961746520746f2075736520496e7465726e65742d4472616674732061732072" +
170             "65666572656e6365206d6174657269616c206f7220746f206369746520746865" +
171             "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" +
172             "726573732e2fe2809d"));
173     }};
174 
175     /**
176      * Make sure we do not use this Cipher object without initializing it
177      * at all
178      */
179     public static final TestMethod noInitTest = new TestMethod() {
180         @Override
181         public boolean isValid(String algorithm) {
182             return true;        // Valid for all algs
183         }
184 
185         @Override
186         public boolean run(String algorithm) {
187             System.out.println("----- No Init Test [" + algorithm +
188                     "] -----");
189             try {
190                 Cipher cipher = Cipher.getInstance(algorithm);
191                 TestData testData;
192                 switch (algorithm) {
193                     case ALG_CC20:
194                         testData = testList.get(0);
195                         break;
196                     case ALG_CC20_P1305:
197                         testData = aeadTestList.get(0);
198                         break;
199                     default:
200                         throw new IllegalArgumentException(
201                                 "Unsupported cipher type: " + algorithm);
202                 }
203 
204                 // Attempting to use the cipher without initializing it
205                 // should throw an IllegalStateException
206                 try {
207                     if (algorithm.equals(ALG_CC20_P1305)) {
208                         cipher.updateAAD(testData.aad);
209                     }
210                     cipher.doFinal(testData.input);
211                     throw new RuntimeException(
212                             "Expected IllegalStateException not thrown");
213                 } catch (IllegalStateException ise) {
214                     // Do nothing, this is what we expected to happen
215                 }
216             } catch (Exception exc) {
217                 System.out.println("Unexpected exception: " + exc);
218                 exc.printStackTrace();
219                 return false;
220             }
221 
222             return true;
223         }
224     };
225 
226     /**
227      * Make sure we don't allow a double init using the same parameters
228      */
229     public static final TestMethod doubleInitTest = new TestMethod() {
230         @Override
231         public boolean isValid(String algorithm) {
232             return true;        // Valid for all algs
233         }
234 
235         @Override
236         public boolean run(String algorithm) {
237             System.out.println("----- Double Init Test [" + algorithm +
238                     "] -----");
239             try {
240                 AlgorithmParameterSpec spec;
241                 Cipher cipher = Cipher.getInstance(algorithm);
242                 TestData testData;
243                 switch (algorithm) {
244                     case ALG_CC20:
245                         testData = testList.get(0);
246                         spec = new ChaCha20ParameterSpec(testData.nonce,
247                                 testData.counter);
248                         break;
249                     case ALG_CC20_P1305:
250                         testData = aeadTestList.get(0);
251                         spec = new IvParameterSpec(testData.nonce);
252                         break;
253                     default:
254                         throw new IllegalArgumentException(
255                                 "Unsupported cipher type: " + algorithm);
256                 }
257                 SecretKey key = new SecretKeySpec(testData.key, ALG_CC20);
258 
259                 // Initialize the first time, this should work.
260                 cipher.init(testData.direction, key, spec);
261 
262                 // Immediately initializing a second time with the same
263                 // parameters should fail
264                 try {
265                     cipher.init(testData.direction, key, spec);
266                     throw new RuntimeException(
267                             "Expected InvalidKeyException not thrown");
268                 } catch (InvalidKeyException ike) {
269                     // Do nothing, this is what we expected to happen
270                 }
271             } catch (Exception exc) {
272                 System.out.println("Unexpected exception: " + exc);
273                 exc.printStackTrace();
274                 return false;
275             }
276 
277             return true;
278         }
279     };
280 
281     /**
282      * Attempt to run two full encryption operations without an init in
283      * between.
284      */
285     public static final TestMethod encTwiceNoInit = new TestMethod() {
286         @Override
287         public boolean isValid(String algorithm) {
288             return true;        // Valid for all algs
289         }
290 
291         @Override
292         public boolean run(String algorithm) {
293             System.out.println("----- Encrypt second time without init [" +
294                     algorithm + "] -----");
295             try {
296                 AlgorithmParameterSpec spec;
297                 Cipher cipher = Cipher.getInstance(algorithm);
298                 TestData testData;
299                 switch (algorithm) {
300                     case ALG_CC20:
301                         testData = testList.get(0);
302                         spec = new ChaCha20ParameterSpec(testData.nonce,
303                                 testData.counter);
304                         break;
305                     case ALG_CC20_P1305:
306                         testData = aeadTestList.get(0);
307                         spec = new IvParameterSpec(testData.nonce);
308                         break;
309                     default:
310                         throw new IllegalArgumentException(
311                                 "Unsupported cipher type: " + algorithm);
312                 }
313                 SecretKey key = new SecretKeySpec(testData.key, ALG_CC20);
314 
315                 // Initialize and encrypt
316                 cipher.init(testData.direction, key, spec);
317                 if (algorithm.equals(ALG_CC20_P1305)) {
318                     cipher.updateAAD(testData.aad);
319                 }
320                 cipher.doFinal(testData.input);
321                 System.out.println("First encryption complete");
322 
323                 // Now attempt to encrypt again without changing the key/IV
324                 // This should fail.
325                 try {
326                     if (algorithm.equals(ALG_CC20_P1305)) {
327                        cipher.updateAAD(testData.aad);
328                     }
329                     cipher.doFinal(testData.input);
330                     throw new RuntimeException(
331                             "Expected IllegalStateException not thrown");
332                 } catch (IllegalStateException ise) {
333                     // Do nothing, this is what we expected to happen
334                 }
335             } catch (Exception exc) {
336                 System.out.println("Unexpected exception: " + exc);
337                 exc.printStackTrace();
338                 return false;
339             }
340 
341             return true;
342         }
343     };
344 
345     /**
346      * Attempt to run two full decryption operations without an init in
347      * between.
348      */
349     public static final TestMethod decTwiceNoInit = new TestMethod() {
350         @Override
351         public boolean isValid(String algorithm) {
352             return true;        // Valid for all algs
353         }
354 
355         @Override
356         public boolean run(String algorithm) {
357             System.out.println("----- Decrypt second time without init [" +
358                     algorithm + "] -----");
359             try {
360                 AlgorithmParameterSpec spec;
361                 Cipher cipher = Cipher.getInstance(algorithm);
362                 TestData testData;
363                 switch (algorithm) {
364                     case ALG_CC20:
365                         testData = testList.get(1);
366                         spec = new ChaCha20ParameterSpec(testData.nonce,
367                                 testData.counter);
368                         break;
369                     case ALG_CC20_P1305:
370                         testData = aeadTestList.get(1);
371                         spec = new IvParameterSpec(testData.nonce);
372                         break;
373                     default:
374                         throw new IllegalArgumentException(
375                                 "Unsupported cipher type: " + algorithm);
376                 }
377                 SecretKey key = new SecretKeySpec(testData.key, ALG_CC20);
378 
379                 // Initialize and encrypt
380                 cipher.init(testData.direction, key, spec);
381                 if (algorithm.equals(ALG_CC20_P1305)) {
382                     cipher.updateAAD(testData.aad);
383                 }
384                 cipher.doFinal(testData.input);
385                 System.out.println("First decryption complete");
386 
387                 // Now attempt to encrypt again without changing the key/IV
388                 // This should fail.
389                 try {
390                     if (algorithm.equals(ALG_CC20_P1305)) {
391                         cipher.updateAAD(testData.aad);
392                     }
393                     cipher.doFinal(testData.input);
394                     throw new RuntimeException(
395                             "Expected IllegalStateException not thrown");
396                 } catch (IllegalStateException ise) {
397                     // Do nothing, this is what we expected to happen
398                 }
399             } catch (Exception exc) {
400                 System.out.println("Unexpected exception: " + exc);
401                 exc.printStackTrace();
402                 return false;
403             }
404 
405             return true;
406         }
407     };
408 
409     /**
410      * Perform an AEAD decryption with corrupted data so the tag does not
411      * match.  Then attempt to reuse the cipher without initialization.
412      */
413     public static final TestMethod decFailNoInit = new TestMethod() {
414         @Override
415         public boolean isValid(String algorithm) {
416             return algorithm.equals(ALG_CC20_P1305);
417         }
418 
419         @Override
420         public boolean run(String algorithm) {
421             System.out.println(
422                     "----- Fail decryption, try again with no init [" +
423                     algorithm + "] -----");
424             try {
425                 TestData testData = aeadTestList.get(1);
426                 AlgorithmParameterSpec spec =
427                         new IvParameterSpec(testData.nonce);
428                 byte[] corruptInput = testData.input.clone();
429                 corruptInput[0]++;      // Corrupt the ciphertext
430                 SecretKey key = new SecretKeySpec(testData.key, ALG_CC20);
431                 Cipher cipher = Cipher.getInstance(algorithm);
432 
433                 try {
434                     // Initialize and encrypt
435                     cipher.init(testData.direction, key, spec);
436                     cipher.updateAAD(testData.aad);
437                     cipher.doFinal(corruptInput);
438                     throw new RuntimeException(
439                             "Expected AEADBadTagException not thrown");
440                 } catch (AEADBadTagException abte) {
441                     System.out.println("Expected decryption failure occurred");
442                 }
443 
444                 // Make sure that despite the exception, the Cipher object is
445                 // not in a state that would leave it initialized and able
446                 // to process future decryption operations without init.
447                 try {
448                     cipher.updateAAD(testData.aad);
449                     cipher.doFinal(testData.input);
450                     throw new RuntimeException(
451                             "Expected IllegalStateException not thrown");
452                 } catch (IllegalStateException ise) {
453                     // Do nothing, this is what we expected to happen
454                 }
455             } catch (Exception exc) {
456                 System.out.println("Unexpected exception: " + exc);
457                 exc.printStackTrace();
458                 return false;
459             }
460 
461             return true;
462         }
463     };
464 
465     /**
466      * Encrypt once successfully, then attempt to init with the same
467      * key and nonce.
468      */
469     public static final TestMethod encTwiceInitSameParams = new TestMethod() {
470         @Override
471         public boolean isValid(String algorithm) {
472             return true;        // Valid for all algs
473         }
474 
475         @Override
476         public boolean run(String algorithm) {
477             System.out.println("----- Encrypt, then init with same params [" +
478                     algorithm + "] -----");
479             try {
480                 AlgorithmParameterSpec spec;
481                 Cipher cipher = Cipher.getInstance(algorithm);
482                 TestData testData;
483                 switch (algorithm) {
484                     case ALG_CC20:
485                         testData = testList.get(0);
486                         spec = new ChaCha20ParameterSpec(testData.nonce,
487                                 testData.counter);
488                         break;
489                     case ALG_CC20_P1305:
490                         testData = aeadTestList.get(0);
491                         spec = new IvParameterSpec(testData.nonce);
492                         break;
493                     default:
494                         throw new IllegalArgumentException(
495                                 "Unsupported cipher type: " + algorithm);
496                 }
497                 SecretKey key = new SecretKeySpec(testData.key, ALG_CC20);
498 
499                 // Initialize then encrypt
500                 cipher.init(testData.direction, key, spec);
501                 if (algorithm.equals(ALG_CC20_P1305)) {
502                     cipher.updateAAD(testData.aad);
503                 }
504                 cipher.doFinal(testData.input);
505                 System.out.println("First encryption complete");
506 
507                 // Initializing after the completed encryption with
508                 // the same key and nonce should fail.
509                 try {
510                     cipher.init(testData.direction, key, spec);
511                     throw new RuntimeException(
512                             "Expected InvalidKeyException not thrown");
513                 } catch (InvalidKeyException ike) {
514                     // Do nothing, this is what we expected to happen
515                 }
516             } catch (Exception exc) {
517                 System.out.println("Unexpected exception: " + exc);
518                 exc.printStackTrace();
519                 return false;
520             }
521 
522             return true;
523         }
524     };
525 
526     /**
527      * Decrypt once successfully, then attempt to init with the same
528      * key and nonce.
529      */
530     public static final TestMethod decTwiceInitSameParams = new TestMethod() {
531         @Override
532         public boolean isValid(String algorithm) {
533             return true;        // Valid for all algs
534         }
535 
536         @Override
537         public boolean run(String algorithm) {
538             System.out.println("----- Decrypt, then init with same params [" +
539                     algorithm + "] -----");
540             try {
541                 AlgorithmParameterSpec spec;
542                 Cipher cipher = Cipher.getInstance(algorithm);
543                 TestData testData;
544                 switch (algorithm) {
545                     case ALG_CC20:
546                         testData = testList.get(1);
547                         spec = new ChaCha20ParameterSpec(testData.nonce,
548                                 testData.counter);
549                         break;
550                     case ALG_CC20_P1305:
551                         testData = aeadTestList.get(1);
552                         spec = new IvParameterSpec(testData.nonce);
553                         break;
554                     default:
555                         throw new IllegalArgumentException(
556                                 "Unsupported cipher type: " + algorithm);
557                 }
558                 SecretKey key = new SecretKeySpec(testData.key, ALG_CC20);
559 
560                 // Initialize then decrypt
561                 cipher.init(testData.direction, key, spec);
562                 if (algorithm.equals(ALG_CC20_P1305)) {
563                     cipher.updateAAD(testData.aad);
564                 }
565                 cipher.doFinal(testData.input);
566                 System.out.println("First decryption complete");
567 
568                 // Initializing after the completed decryption with
569                 // the same key and nonce should fail.
570                 try {
571                     cipher.init(testData.direction, key, spec);
572                     throw new RuntimeException(
573                             "Expected InvalidKeyException not thrown");
574                 } catch (InvalidKeyException ike) {
575                     // Do nothing, this is what we expected to happen
576                 }
577             } catch (Exception exc) {
578                 System.out.println("Unexpected exception: " + exc);
579                 exc.printStackTrace();
580                 return false;
581             }
582 
583             return true;
584         }
585     };
586 
587     public static final List<String> algList =
588             Arrays.asList(ALG_CC20, ALG_CC20_P1305);
589 
590     public static final List<TestMethod> testMethodList =
591             Arrays.asList(noInitTest, doubleInitTest, encTwiceNoInit,
592                     decTwiceNoInit, decFailNoInit, encTwiceInitSameParams,
593                     decTwiceInitSameParams);
594 
595     public static void main(String args[]) throws Exception {
596         int testsPassed = 0;
597         int testNumber = 0;
598 
599         for (TestMethod tm : testMethodList) {
600             for (String alg : algList) {
601                 if (tm.isValid(alg)) {
602                     testNumber++;
603                     boolean result = tm.run(alg);
604                     System.out.println("Result: " + (result ? "PASS" : "FAIL"));
605                     if (result) {
606                         testsPassed++;
607                     }
608                 }
609             }
610         }
611 
612         System.out.println("Total Tests: " + testNumber +
613                 ", Tests passed: " + testsPassed);
614         if (testsPassed < testNumber) {
615             throw new RuntimeException(
616                     "Not all tests passed.  See output for failure info");
617         }
618     }
619 }
620