1 /*
  2  * Copyright (c) 2018, 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  * @build jdk.test.lib.Convert
 29  * @run main ChaCha20Poly1305ParamTest
 30  * @summary ChaCha20 Cipher Implementation (parameters)
 31  */
 32 
 33 import java.util.*;
 34 import java.io.IOException;
 35 import java.security.GeneralSecurityException;
 36 import javax.crypto.Cipher;
 37 import javax.crypto.SecretKey;
 38 import javax.crypto.spec.ChaCha20ParameterSpec;
 39 import javax.crypto.spec.SecretKeySpec;
 40 import javax.crypto.AEADBadTagException;
 41 import java.security.spec.AlgorithmParameterSpec;
 42 import java.security.AlgorithmParameters;
 43 import java.security.NoSuchAlgorithmException;
 44 import java.nio.ByteBuffer;
 45 import jdk.test.lib.Convert;
 46 
 47 public class ChaCha20Poly1305ParamTest {
 48     public static class TestData {
 49         public TestData(String name, String keyStr, String nonceStr, int ctr,
 50                 int dir, String inputStr, String aadStr, String outStr) {
 51             testName = Objects.requireNonNull(name);
 52             key = Convert.hexStringToByteArray(Objects.requireNonNull(keyStr));
 53             nonce = Convert.hexStringToByteArray(
 54                     Objects.requireNonNull(nonceStr));
 55             if ((counter = ctr) < 0) {
 56                 throw new IllegalArgumentException(
 57                         "counter must be 0 or greater");
 58             }
 59             direction = dir;
 60             if ((direction != Cipher.ENCRYPT_MODE) &&
 61                     (direction != Cipher.DECRYPT_MODE)) {
 62                 throw new IllegalArgumentException(
 63                         "Direction must be ENCRYPT_MODE or DECRYPT_MODE");
 64             }
 65             input = Convert.hexStringToByteArray(
 66                     Objects.requireNonNull(inputStr));
 67             aad = (aadStr != null) ?
 68                 Convert.hexStringToByteArray(aadStr) : null;
 69             expOutput = Convert.hexStringToByteArray(
 70                     Objects.requireNonNull(outStr));
 71         }
 72 
 73         public final String testName;
 74         public final byte[] key;
 75         public final byte[] nonce;
 76         public final int counter;
 77         public final int direction;
 78         public final byte[] input;
 79         public final byte[] aad;
 80         public final byte[] expOutput;
 81     }
 82 
 83     public static final List<TestData> aeadTestList =
 84             new LinkedList<TestData>() {{
 85         add(new TestData("RFC 7539 Sample AEAD Test Vector",
 86             "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f",
 87             "070000004041424344454647",
 88             1, Cipher.ENCRYPT_MODE,
 89             "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" +
 90             "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" +
 91             "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" +
 92             "637265656e20776f756c642062652069742e",
 93             "50515253c0c1c2c3c4c5c6c7",
 94             "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" +
 95             "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" +
 96             "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" +
 97             "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" +
 98             "0691"));
 99         add(new TestData("RFC 7539 A.5 Sample Decryption",
100             "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
101             "000000000102030405060708",
102             1, Cipher.DECRYPT_MODE,
103             "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" +
104             "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" +
105             "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" +
106             "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" +
107             "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" +
108             "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" +
109             "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" +
110             "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" +
111             "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38",
112             "f33388860000000000004e91",
113             "496e7465726e65742d4472616674732061726520647261667420646f63756d65" +
114             "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" +
115             "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" +
116             "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" +
117             "6e747320617420616e792074696d652e20497420697320696e617070726f7072" +
118             "6961746520746f2075736520496e7465726e65742d4472616674732061732072" +
119             "65666572656e6365206d6174657269616c206f7220746f206369746520746865" +
120             "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" +
121             "726573732e2fe2809d"));
122     }};
123 
124     // 12-byte nonce DER-encoded as an OCTET_STRING
125     public static final byte[] NONCE_OCTET_STR_12 = {
126         4, 12, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8
127     };
128 
129     // Invalid 16-byte nonce DER-encoded as an OCTET_STRING
130     public static final byte[] NONCE_OCTET_STR_16 = {
131         4, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
132     };
133 
134     // Throwaway key for default init tests
135     public static final SecretKey DEF_KEY = new SecretKeySpec(new byte[]
136         {
137              0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
138             16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31
139         }, "ChaCha20");
140 
141     public static void main(String args[]) throws Exception {
142         int testsPassed = 0;
143         int testNumber = 0;
144 
145         // Try some default initializations
146         testDefaultAlgParams("ChaCha20", Cipher.ENCRYPT_MODE, true);
147         testDefaultAlgParams("ChaCha20-Poly1305", Cipher.ENCRYPT_MODE, true);
148         testDefaultAlgParamSpec("ChaCha20", Cipher.ENCRYPT_MODE, true);
149         testDefaultAlgParamSpec("ChaCha20-Poly1305", Cipher.ENCRYPT_MODE, true);
150         testDefaultAlgParams("ChaCha20", Cipher.DECRYPT_MODE, false);
151         testDefaultAlgParams("ChaCha20-Poly1305", Cipher.DECRYPT_MODE, false);
152         testDefaultAlgParamSpec("ChaCha20", Cipher.DECRYPT_MODE, false);
153         testDefaultAlgParamSpec("ChaCha20-Poly1305", Cipher.DECRYPT_MODE,
154                 false);
155 
156         // Try (and hopefully fail) to create a ChaCha20 AlgorithmParameterSpec
157         System.out.println(
158                 "*** Test: Try to make ChaCha20 AlgorithmParameterSpec");
159         try {
160             ChaCha20ParameterSpec badChaCha20Spec =
161                     new ChaCha20ParameterSpec(NONCE_OCTET_STR_16, 1);
162             throw new RuntimeException("ChaCha20 AlgorithmParameterSpec " +
163                     "with 16 byte nonce should fail");
164         } catch (IllegalArgumentException iae) {
165             System.out.println("Caught expected exception: " + iae);
166         }
167 
168         // Try (and hopefully fail) to create a ChaCha20 AlgorithmParameters
169         System.out.println(
170                 "*** Test: Try to make ChaCha20 AlgorithmParameters");
171         try {
172             AlgorithmParameters apsNoChaCha20 =
173                     AlgorithmParameters.getInstance("ChaCha20");
174             throw new RuntimeException(
175                     "ChaCha20 AlgorithmParameters should fail");
176         } catch (NoSuchAlgorithmException nsae) {
177             System.out.println("Caught expected exception: " + nsae);
178         }
179 
180         // Create the AlgorithmParameters object from a valid encoding
181         System.out.println("*** Test: Create and init ChaCha20-Poly1305 APS");
182         AlgorithmParameters apsGood =
183                 AlgorithmParameters.getInstance("ChaCha20-Poly1305");
184         apsGood.init(NONCE_OCTET_STR_12);
185         System.out.println("Test Passed");
186 
187         // Pull an AlgorithmParameters object out of the initialized cipher
188         // and compare its value against the original.
189         System.out.println("*** Test: Init ChaCha20-Poly1305 Cipher using " +
190                 "AP, retrieve AP and compare");
191         Cipher cc20p1305 = Cipher.getInstance("ChaCha20-Poly1305");
192         cc20p1305.init(Cipher.ENCRYPT_MODE, DEF_KEY, apsGood);
193         AlgorithmParameters pulledParams = cc20p1305.getParameters();
194         byte[] apsGoodData = apsGood.getEncoded();
195         byte[] pulledParamsData = pulledParams.getEncoded();
196         if (!Arrays.equals(apsGoodData, pulledParamsData)) {
197             throw new RuntimeException(
198                 "Retrieved parameters do not match those used to init cipher");
199         }
200         System.out.println("Test Passed");
201 
202         // Try the same test with ChaCha20.  It should always be null.
203         System.out.println("*** Test: Init ChaCha20 Cipher using " +
204                 "AP, retrieve AP and compare");
205         Cipher cc20 = Cipher.getInstance("ChaCha20");
206         cc20.init(Cipher.ENCRYPT_MODE, DEF_KEY);
207         pulledParams = cc20.getParameters();
208         if (pulledParams != null) {
209             throw new RuntimeException("Unexpected non-null " +
210                     "AlgorithmParameters from ChaCha20 cipiher");
211         }
212         System.out.println("Test Passed");
213 
214         // Create and try to init using invalid encoding
215         AlgorithmParameters apsBad =
216                 AlgorithmParameters.getInstance("ChaCha20-Poly1305");
217         System.out.println("*** Test: Use invalid encoding scheme");
218         try {
219             apsBad.init(NONCE_OCTET_STR_12, "OraclePrivate");
220             throw new RuntimeException("Allowed unsupported encoding scheme: " +
221                     apsBad.getAlgorithm());
222         } catch (IOException iae) {
223             System.out.println("Caught expected exception: " + iae);
224         }
225 
226         // Try to init using supported scheme but invalid length
227         System.out.println("*** Test: Use supported scheme, nonce too large");
228         try {
229             apsBad.init(NONCE_OCTET_STR_16, "ASN.1");
230             throw new RuntimeException("Allowed invalid encoded length");
231         } catch (IOException ioe) {
232             System.out.println("Caught expected exception: " + ioe);
233         }
234 
235         System.out.println("----- AEAD Tests -----");
236         for (TestData test : aeadTestList) {
237             System.out.println("*** Test " + ++testNumber + ": " +
238                     test.testName);
239             if (runAEADTest(test)) {
240                 testsPassed++;
241             }
242         }
243         System.out.println();
244 
245         System.out.println("Total tests: " + testNumber +
246                 ", Passed: " + testsPassed + ", Failed: " +
247                 (testNumber - testsPassed));
248         if (testsPassed != testNumber) {
249             throw new RuntimeException("One or more tests failed.  " +
250                     "Check output for details");
251         }
252     }
253 
254     /**
255      * Attempt default inits with null AlgorithmParameters
256      *
257      * @param alg the algorithm (ChaCha20, ChaCha20-Poly1305)
258      * @param mode the Cipher mode (ENCRYPT_MODE, etc.)
259      */
260     private static void testDefaultAlgParams(String alg, int mode,
261             boolean shouldPass) {
262         byte[] ivOne = null, ivTwo = null;
263         System.out.println("Test default AlgorithmParameters: Cipher = " +
264                 alg + ", mode = " + mode);
265         try {
266             AlgorithmParameters params = null;
267             Cipher cipher = Cipher.getInstance(alg);
268             cipher.init(mode, DEF_KEY, params, null);
269             ivOne = cipher.getIV();
270             cipher.init(mode, DEF_KEY, params, null);
271             ivTwo = cipher.getIV();
272             if (!shouldPass) {
273                 throw new RuntimeException(
274                         "Did not receive expected exception");
275             }
276         } catch (GeneralSecurityException gse) {
277             if (shouldPass) {
278                 throw new RuntimeException(gse);
279             }
280             System.out.println("Caught expected exception: " + gse);
281             return;
282         }
283         if (Arrays.equals(ivOne, ivTwo)) {
284             throw new RuntimeException(
285                     "FAIL! Two inits generated same nonces");
286         } else {
287             System.out.println("IV 1:\n" + dumpHexBytes(ivOne, 16, "\n", " "));
288             System.out.println("IV 1:\n" + dumpHexBytes(ivTwo, 16, "\n", " "));
289         }
290     }
291 
292     /**
293      * Attempt default inits with null AlgorithmParameters
294      *
295      * @param alg the algorithm (ChaCha20, ChaCha20-Poly1305)
296      * @param mode the Cipher mode (ENCRYPT_MODE, etc.)
297      */
298     private static void testDefaultAlgParamSpec(String alg, int mode,
299             boolean shouldPass) {
300         byte[] ivOne = null, ivTwo = null;
301         System.out.println("Test default AlgorithmParameterSpec: Cipher = " +
302                 alg + ", mode = " + mode);
303         try {
304             AlgorithmParameterSpec params = null;
305             Cipher cipher = Cipher.getInstance(alg);
306             cipher.init(mode, DEF_KEY, params, null);
307             ivOne = cipher.getIV();
308             cipher.init(mode, DEF_KEY, params, null);
309             ivTwo = cipher.getIV();
310             if (!shouldPass) {
311                 throw new RuntimeException(
312                         "Did not receive expected exception");
313             }
314         } catch (GeneralSecurityException gse) {
315             if (shouldPass) {
316                 throw new RuntimeException(gse);
317             }
318             System.out.println("Caught expected exception: " + gse);
319             return;
320         }
321         if (Arrays.equals(ivOne, ivTwo)) {
322             throw new RuntimeException(
323                     "FAIL! Two inits generated same nonces");
324         } else {
325             System.out.println("IV 1:\n" + dumpHexBytes(ivOne, 16, "\n", " "));
326             System.out.println("IV 2:\n" + dumpHexBytes(ivTwo, 16, "\n", " "));
327         }
328     }
329 
330     private static boolean runAEADTest(TestData testData)
331             throws GeneralSecurityException, IOException {
332         boolean result = false;
333 
334         Cipher mambo = Cipher.getInstance("ChaCha20-Poly1305");
335         SecretKeySpec mamboKey = new SecretKeySpec(testData.key, "ChaCha20");
336         AlgorithmParameters mamboParams =
337                 AlgorithmParameters.getInstance("ChaCha20-Poly1305");
338 
339         // Put the nonce into ASN.1 ChaCha20-Poly1305 parameter format
340         byte[] derNonce = new byte[testData.nonce.length + 2];
341         derNonce[0] = 0x04;
342         derNonce[1] = (byte)testData.nonce.length;
343         System.arraycopy(testData.nonce, 0, derNonce, 2,
344                 testData.nonce.length);
345         mamboParams.init(derNonce);
346 
347         mambo.init(testData.direction, mamboKey, mamboParams);
348 
349         byte[] out = new byte[mambo.getOutputSize(testData.input.length)];
350         int outOff = 0;
351         try {
352             mambo.updateAAD(testData.aad);
353             outOff += mambo.update(testData.input, 0, testData.input.length,
354                     out, outOff);
355             outOff += mambo.doFinal(out, outOff);
356         } catch (AEADBadTagException abte) {
357             // If we get a bad tag or derive a tag mismatch, log it
358             // and register it as a failure
359             System.out.println("FAIL: " + abte);
360             return false;
361         }
362 
363         if (!Arrays.equals(out, testData.expOutput)) {
364             System.out.println("ERROR - Output Mismatch!");
365             System.out.println("Expected:\n" +
366                     dumpHexBytes(testData.expOutput, 16, "\n", " "));
367             System.out.println("Actual:\n" +
368                     dumpHexBytes(out, 16, "\n", " "));
369             System.out.println();
370         } else {
371             result = true;
372         }
373 
374         return result;
375     }
376 
377     /**
378      * Dump the hex bytes of a buffer into string form.
379      *
380      * @param data The array of bytes to dump to stdout.
381      * @param itemsPerLine The number of bytes to display per line
382      *      if the {@code lineDelim} character is blank then all bytes
383      *      will be printed on a single line.
384      * @param lineDelim The delimiter between lines
385      * @param itemDelim The delimiter between bytes
386      *
387      * @return The hexdump of the byte array
388      */
389     private static String dumpHexBytes(byte[] data, int itemsPerLine,
390             String lineDelim, String itemDelim) {
391         return dumpHexBytes(ByteBuffer.wrap(data), itemsPerLine, lineDelim,
392                 itemDelim);
393     }
394 
395     private static String dumpHexBytes(ByteBuffer data, int itemsPerLine,
396             String lineDelim, String itemDelim) {
397         StringBuilder sb = new StringBuilder();
398         if (data != null) {
399             data.mark();
400             int i = 0;
401             while (data.remaining() > 0) {
402                 if (i % itemsPerLine == 0 && i != 0) {
403                     sb.append(lineDelim);
404                 }
405                 sb.append(String.format("%02X", data.get())).append(itemDelim);
406                 i++;
407             }
408             data.reset();
409         }
410 
411         return sb.toString();
412     }
413 }
414