# HG changeset patch # User jnimeh # Date 1527775510 25200 # Node ID 25d711fca885fcfe10339064b172ea9643cd32f0 # Parent c75f3cdeb48c5c6e92eac1b722bc451deb026026 8153029: ChaCha20 Cipher Implementation Summary: Add the ChaCha20 and ChaCha20-Poly1305 Cipher implementations Reviewed-by: mullan diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Cipher.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Cipher.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,1389 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.crypto.provider; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.*; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; +import java.util.Objects; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.*; +import sun.security.util.DerValue; + +/** + * Implementation of the ChaCha20 cipher, as described in RFC 7539. + * + * @since 11 + */ +abstract class ChaCha20Cipher extends CipherSpi { + // Mode constants + private static final int MODE_NONE = 0; + private static final int MODE_AEAD = 1; + + // Constants used in setting up the initial state + private static final int STATE_CONST_0 = 0x61707865; + private static final int STATE_CONST_1 = 0x3320646e; + private static final int STATE_CONST_2 = 0x79622d32; + private static final int STATE_CONST_3 = 0x6b206574; + + // The keystream block size in bytes and as integers + private static final int KEYSTREAM_SIZE = 64; + private static final int KS_SIZE_INTS = KEYSTREAM_SIZE / Integer.BYTES; + private static final int CIPHERBUF_BASE = 1024; + + // The initialization state of the cipher + private boolean initialized; + + // The mode of operation for this object + protected int mode; + + // The direction (encrypt vs. decrypt) for the data flow + private int direction; + + // Has all AAD data been provided (i.e. have we called our first update) + private boolean aadDone = false; + + // The key's encoding in bytes for this object + private byte[] keyBytes; + + // The nonce used for this object + private byte[] nonce; + + // The counter + private static final long MAX_UINT32 = 0x00000000FFFFFFFFL; + private long finalCounterValue; + private long counter; + + // Two arrays, both implemented as 16-element integer arrays: + // The base state, created at initialization time, and a working + // state which is a clone of the start state, and is then modified + // with the counter and the ChaCha20 block function. + private final int[] startState = new int[KS_SIZE_INTS]; + private final byte[] keyStream = new byte[KEYSTREAM_SIZE]; + + // The offset into the current keystream + private int keyStrOffset; + + // AEAD-related fields and constants + private static final int TAG_LENGTH = 16; + private long aadLen; + private long dataLen; + + // Have a buffer of zero padding that can be read all or in part + // by the authenticator. + private static final byte[] padBuf = new byte[TAG_LENGTH]; + + // Create a buffer for holding the AAD and Ciphertext lengths + private final byte[] lenBuf = new byte[TAG_LENGTH]; + + // The authenticator (Poly1305) when running in AEAD mode + protected String authAlgName; + private Poly1305 authenticator; + + // The underlying engine for doing the ChaCha20/Poly1305 work + private ChaChaEngine engine; + + // Use this VarHandle for converting the state elements into little-endian + // integer values for the ChaCha20 block function. + private static final VarHandle asIntLittleEndian = + MethodHandles.byteArrayViewVarHandle(int[].class, + ByteOrder.LITTLE_ENDIAN); + + // Use this VarHandle for converting the AAD and data lengths into + // little-endian long values for AEAD tag computations. + private static final VarHandle asLongLittleEndian = + MethodHandles.byteArrayViewVarHandle(long[].class, + ByteOrder.LITTLE_ENDIAN); + + // Use this for pulling in 8 bytes at a time as longs for XOR operations + private static final VarHandle asLongView = + MethodHandles.byteArrayViewVarHandle(long[].class, + ByteOrder.nativeOrder()); + + /** + * Default constructor. + */ + protected ChaCha20Cipher() { + } + + /** + * Set the mode of operation. Since this is a stream cipher, there + * is no mode of operation in the block-cipher sense of things. The + * protected {@code mode} field will only accept a value of {@code None} + * (case-insensitive). + * + * @param mode The mode value + * + * @throws NoSuchAlgorithmException if a mode of operation besides + * {@code None} is provided. + */ + @Override + protected void engineSetMode(String mode) throws NoSuchAlgorithmException { + if (mode.equalsIgnoreCase("None") == false) { + throw new NoSuchAlgorithmException("Mode must be None"); + } + } + + /** + * Set the padding scheme. Padding schemes do not make sense with stream + * ciphers, but allow {@code NoPadding}. See JCE spec. + * + * @param padding The padding type. The only allowed value is + * {@code NoPadding} case insensitive). + * + * @throws NoSuchPaddingException if a padding scheme besides + * {@code NoPadding} is provided. + */ + @Override + protected void engineSetPadding(String padding) + throws NoSuchPaddingException { + if (padding.equalsIgnoreCase("NoPadding") == false) { + throw new NoSuchPaddingException("Padding must be NoPadding"); + } + } + + /** + * Returns the block size. For a stream cipher like ChaCha20, this + * value will always be zero. + * + * @return This method always returns 0. See the JCE Specification. + */ + @Override + protected int engineGetBlockSize() { + return 0; + } + + /** + * Get the output size based on an input length. In simple stream-cipher + * mode, the output size will equal the input size. For ChaCha20-Poly1305 + * for encryption the output size will be the sum of the input length + * and tag length. For decryption, the output size will be the input + * length less the tag length or zero, whichever is larger. + * + * @param inputLen the length in bytes of the input + * + * @return the output length in bytes. + */ + @Override + protected int engineGetOutputSize(int inputLen) { + int outLen = 0; + + if (mode == MODE_NONE) { + outLen = inputLen; + } else if (mode == MODE_AEAD) { + outLen = (direction == Cipher.ENCRYPT_MODE) ? + Math.addExact(inputLen, TAG_LENGTH) : + Integer.max(inputLen - TAG_LENGTH, 0); + } + + return outLen; + } + + /** + * Get the nonce value used. + * + * @return the nonce bytes. For ChaCha20 this will be a 12-byte value. + */ + @Override + protected byte[] engineGetIV() { + return nonce.clone(); + } + + /** + * Get the algorithm parameters for this cipher. For the ChaCha20 + * cipher, this will always return {@code null} as there currently is + * no {@code AlgorithmParameters} implementation for ChaCha20. For + * ChaCha20-Poly1305, a {@code ChaCha20Poly1305Parameters} object will be + * created and initialized with the configured nonce value and returned + * to the caller. + * + * @return a {@code null} value if the ChaCha20 cipher is used (mode is + * MODE_NONE), or a {@code ChaCha20Poly1305Parameters} object containing + * the nonce if the mode is MODE_AEAD. + */ + @Override + protected AlgorithmParameters engineGetParameters() { + AlgorithmParameters params = null; + if (mode == MODE_AEAD) { + try { + // Force the 12-byte nonce into a DER-encoded OCTET_STRING + byte[] derNonce = new byte[nonce.length + 2]; + derNonce[0] = 0x04; // OCTET_STRING tag + derNonce[1] = (byte)nonce.length; // 12-byte length; + System.arraycopy(nonce, 0, derNonce, 2, nonce.length); + params = AlgorithmParameters.getInstance("ChaCha20-Poly1305"); + params.init(derNonce); + } catch (NoSuchAlgorithmException | IOException exc) { + throw new RuntimeException(exc); + } + } + + return params; + } + + /** + * Initialize the engine using a key and secure random implementation. If + * a SecureRandom object is provided it will be used to create a random + * nonce value. If the {@code random} parameter is null an internal + * secure random source will be used to create the random nonce. + * The counter value will be set to 1. + * + * @param opmode the type of operation to do. This value may not be + * {@code Cipher.DECRYPT_MODE} or {@code Cipher.UNWRAP_MODE} mode + * because it must generate random parameters like the nonce. + * @param key a 256-bit key suitable for ChaCha20 + * @param random a {@code SecureRandom} implementation used to create the + * random nonce. If {@code null} is used for the random object, + * then an internal secure random source will be used to create the + * nonce. + * + * @throws UnsupportedOperationException if the mode of operation + * is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE} + * (currently unsupported). + * @throws InvalidKeyException if the key is of the wrong type or is + * not 256-bits in length. This will also be thrown if the opmode + * parameter is {@code Cipher.DECRYPT_MODE}. + * {@code Cipher.UNWRAP_MODE} would normally be disallowed in this + * context but it is preempted by the UOE case above. + */ + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + if (opmode != Cipher.DECRYPT_MODE) { + byte[] newNonce = createRandomNonce(random); + counter = 1; + init(opmode, key, newNonce); + } else { + throw new InvalidKeyException("Default parameter generation " + + "disallowed in DECRYPT and UNWRAP modes"); + } + } + + /** + * Initialize the engine using a key and secure random implementation. + * + * @param opmode the type of operation to do. This value must be either + * {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE} + * @param key a 256-bit key suitable for ChaCha20 + * @param params a {@code ChaCha20ParameterSpec} that will provide + * the nonce and initial block counter value. + * @param random a {@code SecureRandom} implementation, this parameter + * is not used in this form of the initializer. + * + * @throws UnsupportedOperationException if the mode of operation + * is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE} + * (currently unsupported). + * @throws InvalidKeyException if the key is of the wrong type or is + * not 256-bits in length. This will also be thrown if the opmode + * parameter is not {@code Cipher.ENCRYPT_MODE} or + * {@code Cipher.DECRYPT_MODE} (excepting the UOE case above). + * @throws InvalidAlgorithmParameterException if {@code params} is + * not a {@code ChaCha20ParameterSpec} + * @throws NullPointerException if {@code params} is {@code null} + */ + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameterSpec params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + + // If AlgorithmParameterSpec is null, then treat this like an init + // of the form (int, Key, SecureRandom) + if (params == null) { + engineInit(opmode, key, random); + return; + } + + // We will ignore the secure random implementation and use the nonce + // from the AlgorithmParameterSpec instead. + byte[] newNonce = null; + switch (mode) { + case MODE_NONE: + if (!(params instanceof ChaCha20ParameterSpec)) { + throw new InvalidAlgorithmParameterException( + "ChaCha20 algorithm requires ChaCha20ParameterSpec"); + } + ChaCha20ParameterSpec chaParams = (ChaCha20ParameterSpec)params; + newNonce = chaParams.getNonce(); + counter = ((long)chaParams.getCounter()) & 0x00000000FFFFFFFFL; + break; + case MODE_AEAD: + if (!(params instanceof IvParameterSpec)) { + throw new InvalidAlgorithmParameterException( + "ChaCha20-Poly1305 requires IvParameterSpec"); + } + IvParameterSpec ivParams = (IvParameterSpec)params; + newNonce = ivParams.getIV(); + if (newNonce.length != 12) { + throw new InvalidAlgorithmParameterException( + "ChaCha20-Poly1305 nonce must be 12 bytes in length"); + } + break; + default: + // Should never happen + throw new RuntimeException("ChaCha20 in unsupported mode"); + } + init(opmode, key, newNonce); + } + + /** + * Initialize the engine using the {@code AlgorithmParameter} initialization + * format. This cipher does supports initialization with + * {@code AlgorithmParameter} objects for ChaCha20-Poly1305 but not for + * ChaCha20 as a simple stream cipher. In the latter case, it will throw + * an {@code InvalidAlgorithmParameterException} if the value is non-null. + * If a null value is supplied for the {@code params} field + * the cipher will be initialized with the counter value set to 1 and + * a random nonce. If {@code null} is used for the random object, + * then an internal secure random source will be used to create the + * nonce. + * + * @param opmode the type of operation to do. This value must be either + * {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE} + * @param key a 256-bit key suitable for ChaCha20 + * @param params a {@code null} value if the algorithm is ChaCha20, or + * the appropriate {@code AlgorithmParameters} object containing the + * nonce information if the algorithm is ChaCha20-Poly1305. + * @param random a {@code SecureRandom} implementation, may be {@code null}. + * + * @throws UnsupportedOperationException if the mode of operation + * is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE} + * (currently unsupported). + * @throws InvalidKeyException if the key is of the wrong type or is + * not 256-bits in length. This will also be thrown if the opmode + * parameter is not {@code Cipher.ENCRYPT_MODE} or + * {@code Cipher.DECRYPT_MODE} (excepting the UOE case above). + * @throws InvalidAlgorithmParameterException if {@code params} is + * non-null and the algorithm is ChaCha20. This exception will be + * also thrown if the algorithm is ChaCha20-Poly1305 and an incorrect + * {@code AlgorithmParameters} object is supplied. + */ + @Override + protected void engineInit(int opmode, Key key, + AlgorithmParameters params, SecureRandom random) + throws InvalidKeyException, InvalidAlgorithmParameterException { + + // If AlgorithmParameters is null, then treat this like an init + // of the form (int, Key, SecureRandom) + if (params == null) { + engineInit(opmode, key, random); + return; + } + + byte[] newNonce = null; + switch (mode) { + case MODE_NONE: + throw new InvalidAlgorithmParameterException( + "AlgorithmParameters not supported"); + case MODE_AEAD: + String paramAlg = params.getAlgorithm(); + if (!paramAlg.equalsIgnoreCase("ChaCha20-Poly1305")) { + throw new InvalidAlgorithmParameterException( + "Invalid parameter type: " + paramAlg); + } + try { + DerValue dv = new DerValue(params.getEncoded()); + newNonce = dv.getOctetString(); + if (newNonce.length != 12) { + throw new InvalidAlgorithmParameterException( + "ChaCha20-Poly1305 nonce must be " + + "12 bytes in length"); + } + } catch (IOException ioe) { + throw new InvalidAlgorithmParameterException(ioe); + } + break; + default: + throw new RuntimeException("Invalid mode: " + mode); + } + + // If after all the above processing we still don't have a nonce value + // then supply a random one provided a random source has been given. + if (newNonce == null) { + newNonce = createRandomNonce(random); + } + + // Continue with initialization + init(opmode, key, newNonce); + } + + /** + * Update additional authenticated data (AAD). + * + * @param src the byte array containing the authentication data. + * @param offset the starting offset in the buffer to update. + * @param len the amount of authentication data to update. + * + * @throws IllegalStateException if the cipher has not been initialized, + * {@code engineUpdate} has been called, or the cipher is running + * in a non-AEAD mode of operation. It will also throw this + * exception if the submitted AAD would overflow a 64-bit length + * counter. + */ + @Override + protected void engineUpdateAAD(byte[] src, int offset, int len) { + if (!initialized) { + // We know that the cipher has not been initialized if the key + // is still null. + throw new IllegalStateException( + "Attempted to update AAD on uninitialized Cipher"); + } else if (aadDone) { + // No AAD updates allowed after the PT/CT update method is called + throw new IllegalStateException("Attempted to update AAD on " + + "Cipher after plaintext/ciphertext update"); + } else if (mode != MODE_AEAD) { + throw new IllegalStateException( + "Cipher is running in non-AEAD mode"); + } else { + try { + aadLen = Math.addExact(aadLen, len); + authUpdate(src, offset, len); + } catch (ArithmeticException ae) { + throw new IllegalStateException("AAD overflow", ae); + } + } + } + + /** + * Update additional authenticated data (AAD). + * + * @param src the ByteBuffer containing the authentication data. + * + * @throws IllegalStateException if the cipher has not been initialized, + * {@code engineUpdate} has been called, or the cipher is running + * in a non-AEAD mode of operation. It will also throw this + * exception if the submitted AAD would overflow a 64-bit length + * counter. + */ + @Override + protected void engineUpdateAAD(ByteBuffer src) { + if (!initialized) { + // We know that the cipher has not been initialized if the key + // is still null. + throw new IllegalStateException( + "Attempted to update AAD on uninitialized Cipher"); + } else if (aadDone) { + // No AAD updates allowed after the PT/CT update method is called + throw new IllegalStateException("Attempted to update AAD on " + + "Cipher after plaintext/ciphertext update"); + } else if (mode != MODE_AEAD) { + throw new IllegalStateException( + "Cipher is running in non-AEAD mode"); + } else { + try { + aadLen = Math.addExact(aadLen, (src.limit() - src.position())); + authenticator.engineUpdate(src); + } catch (ArithmeticException ae) { + throw new IllegalStateException("AAD overflow", ae); + } + } + } + + /** + * Create a random 12-byte nonce. + * + * @param random a {@code SecureRandom} object. If {@code null} is + * provided a new {@code SecureRandom} object will be instantiated. + * + * @return a 12-byte array containing the random nonce. + */ + private byte[] createRandomNonce(SecureRandom random) { + byte[] newNonce = new byte[12]; + SecureRandom rand = (random != null) ? random : new SecureRandom(); + rand.nextBytes(newNonce); + return newNonce; + } + + /** + * Perform additional initialization actions based on the key and operation + * type. + * + * @param opmode the type of operation to do. This value must be either + * {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE} + * @param key a 256-bit key suitable for ChaCha20 + * @param newNonce the new nonce value for this initialization. + * + * @throws UnsupportedOperationException if the {@code opmode} parameter + * is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE} + * (currently unsupported). + * @throws InvalidKeyException if the {@code opmode} parameter is not + * {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE}, or + * if the key format is not {@code RAW}. + */ + private void init(int opmode, Key key, byte[] newNonce) + throws InvalidKeyException { + if ((opmode == Cipher.WRAP_MODE) || (opmode == Cipher.UNWRAP_MODE)) { + throw new UnsupportedOperationException( + "WRAP_MODE and UNWRAP_MODE are not currently supported"); + } else if ((opmode != Cipher.ENCRYPT_MODE) && + (opmode != Cipher.DECRYPT_MODE)) { + throw new InvalidKeyException("Unknown opmode: " + opmode); + } + + // Make sure that the provided key and nonce are unique before + // assigning them to the object. + byte[] newKeyBytes = getEncodedKey(key); + checkKeyAndNonce(newKeyBytes, newNonce); + this.keyBytes = newKeyBytes; + nonce = newNonce; + + // Now that we have the key and nonce, we can build the initial state + setInitialState(); + + if (mode == MODE_NONE) { + engine = new EngineStreamOnly(); + } else if (mode == MODE_AEAD) { + if (opmode == Cipher.ENCRYPT_MODE) { + engine = new EngineAEADEnc(); + } else if (opmode == Cipher.DECRYPT_MODE) { + engine = new EngineAEADDec(); + } else { + throw new InvalidKeyException("Not encrypt or decrypt mode"); + } + } + + // We can also get one block's worth of keystream created + finalCounterValue = counter + MAX_UINT32; + generateKeystream(); + direction = opmode; + aadDone = false; + this.keyStrOffset = 0; + initialized = true; + } + + /** + * Check the key and nonce bytes to make sure that they do not repeat + * across reinitialization. + * + * @param newKeyBytes the byte encoding for the newly provided key + * @param newNonce the new nonce to be used with this initialization + * + * @throws InvalidKeyException if both the key and nonce match the + * previous initialization. + * + */ + private void checkKeyAndNonce(byte[] newKeyBytes, byte[] newNonce) + throws InvalidKeyException { + // A new initialization must have either a different key or nonce + // so the starting state for each block is not the same as the + // previous initialization. + if (MessageDigest.isEqual(newKeyBytes, keyBytes) && + MessageDigest.isEqual(newNonce, nonce)) { + throw new InvalidKeyException( + "Matching key and nonce from previous initialization"); + } + } + + /** + * Return the encoded key as a byte array + * + * @param key the {@code Key} object used for this {@code Cipher} + * + * @return the key bytes + * + * @throws InvalidKeyException if the key is of the wrong type or length, + * or if the key encoding format is not {@code RAW}. + */ + private static byte[] getEncodedKey(Key key) throws InvalidKeyException { + if ("RAW".equals(key.getFormat()) == false) { + throw new InvalidKeyException("Key encoding format must be RAW"); + } + byte[] encodedKey = key.getEncoded(); + if (encodedKey == null || encodedKey.length != 32) { + throw new InvalidKeyException("Key length must be 256 bits"); + } + return encodedKey; + } + + /** + * Update the currently running operation with additional data + * + * @param in the plaintext or ciphertext input bytes (depending on the + * operation type). + * @param inOfs the offset into the input array + * @param inLen the length of the data to use for the update operation. + * + * @return the resulting plaintext or ciphertext bytes (depending on + * the operation type) + */ + @Override + protected byte[] engineUpdate(byte[] in, int inOfs, int inLen) { + byte[] out = new byte[inLen]; + try { + engine.doUpdate(in, inOfs, inLen, out, 0); + } catch (ShortBufferException | KeyException exc) { + throw new RuntimeException(exc); + } + + return out; + } + + /** + * Update the currently running operation with additional data + * + * @param in the plaintext or ciphertext input bytes (depending on the + * operation type). + * @param inOfs the offset into the input array + * @param inLen the length of the data to use for the update operation. + * @param out the byte array that will hold the resulting data. The array + * must be large enough to hold the resulting data. + * @param outOfs the offset for the {@code out} buffer to begin writing + * the resulting data. + * + * @return the length in bytes of the data written into the {@code out} + * buffer. + * + * @throws ShortBufferException if the buffer {@code out} does not have + * enough space to hold the resulting data. + */ + @Override + protected int engineUpdate(byte[] in, int inOfs, int inLen, + byte[] out, int outOfs) throws ShortBufferException { + int bytesUpdated = 0; + try { + bytesUpdated = engine.doUpdate(in, inOfs, inLen, out, outOfs); + } catch (KeyException ke) { + throw new RuntimeException(ke); + } + return bytesUpdated; + } + + /** + * Complete the currently running operation using any final + * data provided by the caller. + * + * @param in the plaintext or ciphertext input bytes (depending on the + * operation type). + * @param inOfs the offset into the input array + * @param inLen the length of the data to use for the update operation. + * + * @return the resulting plaintext or ciphertext bytes (depending on + * the operation type) + * + * @throws AEADBadTagException if, during decryption, the provided tag + * does not match the calculated tag. + */ + @Override + protected byte[] engineDoFinal(byte[] in, int inOfs, int inLen) + throws AEADBadTagException { + byte[] output = new byte[engineGetOutputSize(inLen)]; + try { + engine.doFinal(in, inOfs, inLen, output, 0); + } catch (ShortBufferException | KeyException exc) { + throw new RuntimeException(exc); + } finally { + // Regardless of what happens, the cipher cannot be used for + // further processing until it has been freshly initialized. + initialized = false; + } + return output; + } + + /** + * Complete the currently running operation using any final + * data provided by the caller. + * + * @param in the plaintext or ciphertext input bytes (depending on the + * operation type). + * @param inOfs the offset into the input array + * @param inLen the length of the data to use for the update operation. + * @param out the byte array that will hold the resulting data. The array + * must be large enough to hold the resulting data. + * @param outOfs the offset for the {@code out} buffer to begin writing + * the resulting data. + * + * @return the length in bytes of the data written into the {@code out} + * buffer. + * + * @throws ShortBufferException if the buffer {@code out} does not have + * enough space to hold the resulting data. + * @throws AEADBadTagException if, during decryption, the provided tag + * does not match the calculated tag. + */ + @Override + protected int engineDoFinal(byte[] in, int inOfs, int inLen, byte[] out, + int outOfs) throws ShortBufferException, AEADBadTagException { + + int bytesUpdated = 0; + try { + bytesUpdated = engine.doFinal(in, inOfs, inLen, out, outOfs); + } catch (KeyException ke) { + throw new RuntimeException(ke); + } finally { + // Regardless of what happens, the cipher cannot be used for + // further processing until it has been freshly initialized. + initialized = false; + } + return bytesUpdated; + } + + /** + * Wrap a {@code Key} using this Cipher's current encryption parameters. + * + * @param key the key to wrap. The data that will be encrypted will + * be the provided {@code Key} in its encoded form. + * + * @return a byte array consisting of the wrapped key. + * + * @throws UnsupportedOperationException this will (currently) always + * be thrown, as this method is not currently supported. + */ + @Override + protected byte[] engineWrap(Key key) throws IllegalBlockSizeException, + InvalidKeyException { + throw new UnsupportedOperationException( + "Wrap operations are not supported"); + } + + /** + * Unwrap a {@code Key} using this Cipher's current encryption parameters. + * + * @param wrappedKey the key to unwrap. + * @param algorithm the algorithm associated with the wrapped key + * @param type the type of the wrapped key. This is one of + * {@code SECRET_KEY}, {@code PRIVATE_KEY}, or {@code PUBLIC_KEY}. + * + * @return the unwrapped key as a {@code Key} object. + * + * @throws UnsupportedOperationException this will (currently) always + * be thrown, as this method is not currently supported. + */ + @Override + protected Key engineUnwrap(byte[] wrappedKey, String algorithm, + int type) throws InvalidKeyException, NoSuchAlgorithmException { + throw new UnsupportedOperationException( + "Unwrap operations are not supported"); + } + + /** + * Get the length of a provided key in bits. + * + * @param key the key to be evaluated + * + * @return the length of the key in bits + * + * @throws InvalidKeyException if the key is invalid or does not + * have an encoded form. + */ + @Override + protected int engineGetKeySize(Key key) throws InvalidKeyException { + byte[] encodedKey = getEncodedKey(key); + return encodedKey.length << 3; + } + + /** + * Set the initial state. This will populate the state array and put the + * key and nonce into their proper locations. The counter field is not + * set here. + * + * @throws IllegalArgumentException if the key or nonce are not in + * their proper lengths (32 bytes for the key, 12 bytes for the + * nonce). + * @throws InvalidKeyException if the key does not support an encoded form. + */ + private void setInitialState() throws InvalidKeyException { + // Apply constants to first 4 words + startState[0] = STATE_CONST_0; + startState[1] = STATE_CONST_1; + startState[2] = STATE_CONST_2; + startState[3] = STATE_CONST_3; + + // Apply the key bytes as 8 32-bit little endian ints (4 through 11) + for (int i = 0; i < 32; i += 4) { + startState[(i / 4) + 4] = (keyBytes[i] & 0x000000FF) | + ((keyBytes[i + 1] << 8) & 0x0000FF00) | + ((keyBytes[i + 2] << 16) & 0x00FF0000) | + ((keyBytes[i + 3] << 24) & 0xFF000000); + } + + startState[12] = 0; + + // The final integers for the state are from the nonce + // interpreted as 3 little endian integers + for (int i = 0; i < 12; i += 4) { + startState[(i / 4) + 13] = (nonce[i] & 0x000000FF) | + ((nonce[i + 1] << 8) & 0x0000FF00) | + ((nonce[i + 2] << 16) & 0x00FF0000) | + ((nonce[i + 3] << 24) & 0xFF000000); + } + } + + /** + * Using the current state and counter create the next set of keystream + * bytes. This method will generate the next 512 bits of keystream and + * return it in the {@code keyStream} parameter. Following the + * block function the counter will be incremented. + */ + private void generateKeystream() { + chaCha20Block(startState, counter, keyStream); + counter++; + } + + /** + * Perform a full 20-round ChaCha20 transform on the initial state. + * + * @param initState the starting state, not including the counter + * value. + * @param counter the counter value to apply + * @param result the array that will hold the result of the ChaCha20 + * block function. + * + * @note it is the caller's responsibility to ensure that the workState + * is sized the same as the initState, no checking is performed internally. + */ + private static void chaCha20Block(int[] initState, long counter, + byte[] result) { + // Create an initial state and clone a working copy + int ws00 = STATE_CONST_0; + int ws01 = STATE_CONST_1; + int ws02 = STATE_CONST_2; + int ws03 = STATE_CONST_3; + int ws04 = initState[4]; + int ws05 = initState[5]; + int ws06 = initState[6]; + int ws07 = initState[7]; + int ws08 = initState[8]; + int ws09 = initState[9]; + int ws10 = initState[10]; + int ws11 = initState[11]; + int ws12 = (int)counter; + int ws13 = initState[13]; + int ws14 = initState[14]; + int ws15 = initState[15]; + + // Peform 10 iterations of the 8 quarter round set + for (int round = 0; round < 10; round++) { + ws00 += ws04; + ws12 = Integer.rotateLeft(ws12 ^ ws00, 16); + + ws08 += ws12; + ws04 = Integer.rotateLeft(ws04 ^ ws08, 12); + + ws00 += ws04; + ws12 = Integer.rotateLeft(ws12 ^ ws00, 8); + + ws08 += ws12; + ws04 = Integer.rotateLeft(ws04 ^ ws08, 7); + + ws01 += ws05; + ws13 = Integer.rotateLeft(ws13 ^ ws01, 16); + + ws09 += ws13; + ws05 = Integer.rotateLeft(ws05 ^ ws09, 12); + + ws01 += ws05; + ws13 = Integer.rotateLeft(ws13 ^ ws01, 8); + + ws09 += ws13; + ws05 = Integer.rotateLeft(ws05 ^ ws09, 7); + + ws02 += ws06; + ws14 = Integer.rotateLeft(ws14 ^ ws02, 16); + + ws10 += ws14; + ws06 = Integer.rotateLeft(ws06 ^ ws10, 12); + + ws02 += ws06; + ws14 = Integer.rotateLeft(ws14 ^ ws02, 8); + + ws10 += ws14; + ws06 = Integer.rotateLeft(ws06 ^ ws10, 7); + + ws03 += ws07; + ws15 = Integer.rotateLeft(ws15 ^ ws03, 16); + + ws11 += ws15; + ws07 = Integer.rotateLeft(ws07 ^ ws11, 12); + + ws03 += ws07; + ws15 = Integer.rotateLeft(ws15 ^ ws03, 8); + + ws11 += ws15; + ws07 = Integer.rotateLeft(ws07 ^ ws11, 7); + + ws00 += ws05; + ws15 = Integer.rotateLeft(ws15 ^ ws00, 16); + + ws10 += ws15; + ws05 = Integer.rotateLeft(ws05 ^ ws10, 12); + + ws00 += ws05; + ws15 = Integer.rotateLeft(ws15 ^ ws00, 8); + + ws10 += ws15; + ws05 = Integer.rotateLeft(ws05 ^ ws10, 7); + + ws01 += ws06; + ws12 = Integer.rotateLeft(ws12 ^ ws01, 16); + + ws11 += ws12; + ws06 = Integer.rotateLeft(ws06 ^ ws11, 12); + + ws01 += ws06; + ws12 = Integer.rotateLeft(ws12 ^ ws01, 8); + + ws11 += ws12; + ws06 = Integer.rotateLeft(ws06 ^ ws11, 7); + + ws02 += ws07; + ws13 = Integer.rotateLeft(ws13 ^ ws02, 16); + + ws08 += ws13; + ws07 = Integer.rotateLeft(ws07 ^ ws08, 12); + + ws02 += ws07; + ws13 = Integer.rotateLeft(ws13 ^ ws02, 8); + + ws08 += ws13; + ws07 = Integer.rotateLeft(ws07 ^ ws08, 7); + + ws03 += ws04; + ws14 = Integer.rotateLeft(ws14 ^ ws03, 16); + + ws09 += ws14; + ws04 = Integer.rotateLeft(ws04 ^ ws09, 12); + + ws03 += ws04; + ws14 = Integer.rotateLeft(ws14 ^ ws03, 8); + + ws09 += ws14; + ws04 = Integer.rotateLeft(ws04 ^ ws09, 7); + } + + // Add the end working state back into the original state + asIntLittleEndian.set(result, 0, ws00 + STATE_CONST_0); + asIntLittleEndian.set(result, 4, ws01 + STATE_CONST_1); + asIntLittleEndian.set(result, 8, ws02 + STATE_CONST_2); + asIntLittleEndian.set(result, 12, ws03 + STATE_CONST_3); + asIntLittleEndian.set(result, 16, ws04 + initState[4]); + asIntLittleEndian.set(result, 20, ws05 + initState[5]); + asIntLittleEndian.set(result, 24, ws06 + initState[6]); + asIntLittleEndian.set(result, 28, ws07 + initState[7]); + asIntLittleEndian.set(result, 32, ws08 + initState[8]); + asIntLittleEndian.set(result, 36, ws09 + initState[9]); + asIntLittleEndian.set(result, 40, ws10 + initState[10]); + asIntLittleEndian.set(result, 44, ws11 + initState[11]); + // Add the counter back into workState[12] + asIntLittleEndian.set(result, 48, ws12 + (int)counter); + asIntLittleEndian.set(result, 52, ws13 + initState[13]); + asIntLittleEndian.set(result, 56, ws14 + initState[14]); + asIntLittleEndian.set(result, 60, ws15 + initState[15]); + } + + /** + * Perform the ChaCha20 transform. + * + * @param in the array of bytes for the input + * @param inOff the offset into the input array to start the transform + * @param inLen the length of the data to perform the transform on. + * @param out the output array. It must be large enough to hold the + * resulting data + * @param outOff the offset into the output array to place the resulting + * data. + */ + private void chaCha20Transform(byte[] in, int inOff, int inLen, + byte[] out, int outOff) throws KeyException { + int remainingData = inLen; + + while (remainingData > 0) { + int ksRemain = keyStream.length - keyStrOffset; + if (ksRemain <= 0) { + if (counter <= finalCounterValue) { + generateKeystream(); + keyStrOffset = 0; + ksRemain = keyStream.length; + } else { + throw new KeyException("Counter exhausted. " + + "Reinitialize with new key and/or nonce"); + } + } + + // XOR each byte in the keystream against the input + int xformLen = Math.min(remainingData, ksRemain); + xor(keyStream, keyStrOffset, in, inOff, out, outOff, xformLen); + outOff += xformLen; + inOff += xformLen; + keyStrOffset += xformLen; + remainingData -= xformLen; + } + } + + private static void xor(byte[] in1, int off1, byte[] in2, int off2, + byte[] out, int outOff, int len) { + while (len >= 8) { + long v1 = (long) asLongView.get(in1, off1); + long v2 = (long) asLongView.get(in2, off2); + asLongView.set(out, outOff, v1 ^ v2); + off1 += 8; + off2 += 8; + outOff += 8; + len -= 8; + } + while (len > 0) { + out[outOff] = (byte) (in1[off1] ^ in2[off2]); + off1++; + off2++; + outOff++; + len--; + } + } + + /** + * Perform initialization steps for the authenticator + * + * @throws InvalidKeyException if the key is unusable for some reason + * (invalid length, etc.) + */ + private void initAuthenticator() throws InvalidKeyException { + authenticator = new Poly1305(); + + // Derive the Poly1305 key from the starting state + byte[] serializedKey = new byte[KEYSTREAM_SIZE]; + chaCha20Block(startState, 0, serializedKey); + + authenticator.engineInit(new SecretKeySpec(serializedKey, 0, 32, + authAlgName), null); + aadLen = 0; + dataLen = 0; + } + + /** + * Update the authenticator state with data. This routine can be used + * to add data to the authenticator, whether AAD or application data. + * + * @param data the data to stir into the authenticator. + * @param offset the offset into the data. + * @param length the length of data to add to the authenticator. + * + * @return the number of bytes processed by this method. + */ + private int authUpdate(byte[] data, int offset, int length) { + Objects.checkFromIndexSize(offset, length, data.length); + authenticator.engineUpdate(data, offset, length); + return length; + } + + /** + * Finalize the data and return the tag. + * + * @param data an array containing any remaining data to process. + * @param dataOff the offset into the data. + * @param length the length of the data to process. + * @param out the array to write the resulting tag into + * @param outOff the offset to begin writing the data. + * + * @throws ShortBufferException if there is insufficient room to + * write the tag. + */ + private void authFinalizeData(byte[] data, int dataOff, int length, + byte[] out, int outOff) throws ShortBufferException { + // Update with the final chunk of ciphertext, then pad to a + // multiple of 16. + if (data != null) { + dataLen += authUpdate(data, dataOff, length); + } + authPad16(dataLen); + + // Also write the AAD and ciphertext data lengths as little-endian + // 64-bit values. + authWriteLengths(aadLen, dataLen, lenBuf); + authenticator.engineUpdate(lenBuf, 0, lenBuf.length); + byte[] tag = authenticator.engineDoFinal(); + Objects.checkFromIndexSize(outOff, tag.length, out.length); + System.arraycopy(tag, 0, out, outOff, tag.length); + aadLen = 0; + dataLen = 0; + } + + /** + * Based on a given length of data, make the authenticator process + * zero bytes that will pad the length out to a multiple of 16. + * + * @param dataLen the starting length to be padded. + */ + private void authPad16(long dataLen) { + // Pad out the AAD or data to a multiple of 16 bytes + authenticator.engineUpdate(padBuf, 0, + (TAG_LENGTH - ((int)dataLen & 15)) & 15); + } + + /** + * Write the two 64-bit little-endian length fields into an array + * for processing by the poly1305 authenticator. + * + * @param aLen the length of the AAD. + * @param dLen the length of the application data. + * @param buf the buffer to write the two lengths into. + * + * @note it is the caller's responsibility to provide an array large + * enough to hold the two longs. + */ + private void authWriteLengths(long aLen, long dLen, byte[] buf) { + asLongLittleEndian.set(buf, 0, aLen); + asLongLittleEndian.set(buf, Long.BYTES, dLen); + } + + /** + * Interface for the underlying processing engines for ChaCha20 + */ + interface ChaChaEngine { + /** + * Perform a multi-part update for ChaCha20. + * + * @param in the input data. + * @param inOff the offset into the input. + * @param inLen the length of the data to process. + * @param out the output buffer. + * @param outOff the offset at which to write the output data. + * + * @return the number of output bytes written. + * + * @throws ShortBufferException if the output buffer does not + * provide enough space. + * @throws KeyException if the counter value has been exhausted. + */ + int doUpdate(byte[] in, int inOff, int inLen, byte[] out, int outOff) + throws ShortBufferException, KeyException; + + /** + * Finalize a multi-part or single-part ChaCha20 operation. + * + * @param in the input data. + * @param inOff the offset into the input. + * @param inLen the length of the data to process. + * @param out the output buffer. + * @param outOff the offset at which to write the output data. + * + * @return the number of output bytes written. + * + * @throws ShortBufferException if the output buffer does not + * provide enough space. + * @throws AEADBadTagException if in decryption mode the provided + * tag and calculated tag do not match. + * @throws KeyException if the counter value has been exhausted. + */ + int doFinal(byte[] in, int inOff, int inLen, byte[] out, int outOff) + throws ShortBufferException, AEADBadTagException, KeyException; + } + + private final class EngineStreamOnly implements ChaChaEngine { + + private EngineStreamOnly () { } + + @Override + public int doUpdate(byte[] in, int inOff, int inLen, byte[] out, + int outOff) throws ShortBufferException, KeyException { + if (initialized) { + try { + if (out != null) { + Objects.checkFromIndexSize(outOff, inLen, out.length); + } else { + throw new ShortBufferException( + "Output buffer too small"); + } + } catch (IndexOutOfBoundsException iobe) { + throw new ShortBufferException("Output buffer too small"); + } + if (in != null) { + Objects.checkFromIndexSize(inOff, inLen, in.length); + chaCha20Transform(in, inOff, inLen, out, outOff); + } + return inLen; + } else { + throw new IllegalStateException( + "Must use either a different key or iv."); + } + } + + @Override + public int doFinal(byte[] in, int inOff, int inLen, byte[] out, + int outOff) throws ShortBufferException, KeyException { + return doUpdate(in, inOff, inLen, out, outOff); + } + } + + private final class EngineAEADEnc implements ChaChaEngine { + + private EngineAEADEnc() throws InvalidKeyException { + initAuthenticator(); + counter = 1; + } + + @Override + public int doUpdate(byte[] in, int inOff, int inLen, byte[] out, + int outOff) throws ShortBufferException, KeyException { + if (initialized) { + // If this is the first update since AAD updates, signal that + // we're done processing AAD info and pad the AAD to a multiple + // of 16 bytes. + if (!aadDone) { + authPad16(aadLen); + aadDone = true; + } + try { + if (out != null) { + Objects.checkFromIndexSize(outOff, inLen, out.length); + } else { + throw new ShortBufferException( + "Output buffer too small"); + } + } catch (IndexOutOfBoundsException iobe) { + throw new ShortBufferException("Output buffer too small"); + } + if (in != null) { + Objects.checkFromIndexSize(inOff, inLen, in.length); + chaCha20Transform(in, inOff, inLen, out, outOff); + dataLen += authUpdate(out, outOff, inLen); + } + + return inLen; + } else { + throw new IllegalStateException( + "Must use either a different key or iv."); + } + } + + @Override + public int doFinal(byte[] in, int inOff, int inLen, byte[] out, + int outOff) throws ShortBufferException, KeyException { + // Make sure we have enough room for the remaining data (if any) + // and the tag. + if ((inLen + TAG_LENGTH) > (out.length - outOff)) { + throw new ShortBufferException("Output buffer too small"); + } + + doUpdate(in, inOff, inLen, out, outOff); + authFinalizeData(null, 0, 0, out, outOff + inLen); + aadDone = false; + return inLen + TAG_LENGTH; + } + } + + private final class EngineAEADDec implements ChaChaEngine { + + private final ByteArrayOutputStream cipherBuf; + private final byte[] tag; + + private EngineAEADDec() throws InvalidKeyException { + initAuthenticator(); + counter = 1; + cipherBuf = new ByteArrayOutputStream(CIPHERBUF_BASE); + tag = new byte[TAG_LENGTH]; + } + + @Override + public int doUpdate(byte[] in, int inOff, int inLen, byte[] out, + int outOff) { + if (initialized) { + // If this is the first update since AAD updates, signal that + // we're done processing AAD info and pad the AAD to a multiple + // of 16 bytes. + if (!aadDone) { + authPad16(aadLen); + aadDone = true; + } + + if (in != null) { + Objects.checkFromIndexSize(inOff, inLen, in.length); + cipherBuf.write(in, inOff, inLen); + } + } else { + throw new IllegalStateException( + "Must use either a different key or iv."); + } + + return 0; + } + + @Override + public int doFinal(byte[] in, int inOff, int inLen, byte[] out, + int outOff) throws ShortBufferException, AEADBadTagException, + KeyException { + + byte[] ctPlusTag; + int ctPlusTagLen; + if (cipherBuf.size() == 0 && inOff == 0) { + // No previous data has been seen before doFinal, so we do + // not need to hold any ciphertext in a buffer. We can + // process it directly from the "in" parameter. + doUpdate(null, inOff, inLen, out, outOff); + ctPlusTag = in; + ctPlusTagLen = inLen; + } else { + doUpdate(in, inOff, inLen, out, outOff); + ctPlusTag = cipherBuf.toByteArray(); + ctPlusTagLen = ctPlusTag.length; + } + cipherBuf.reset(); + + // There must at least be a tag length's worth of ciphertext + // data in the buffered input. + if (ctPlusTagLen < TAG_LENGTH) { + throw new AEADBadTagException("Input too short - need tag"); + } + int ctLen = ctPlusTagLen - TAG_LENGTH; + + // Make sure we will have enough room for the output buffer + try { + Objects.checkFromIndexSize(outOff, ctLen, out.length); + } catch (IndexOutOfBoundsException ioobe) { + throw new ShortBufferException("Output buffer too small"); + } + + // Calculate and compare the tag. Only do the decryption + // if and only if the tag matches. + authFinalizeData(ctPlusTag, 0, ctLen, tag, 0); + if (Arrays.compare(ctPlusTag, ctLen, ctPlusTagLen, + tag, 0, tag.length) != 0) { + throw new AEADBadTagException("Tag mismatch"); + } + chaCha20Transform(ctPlusTag, 0, ctLen, out, outOff); + aadDone = false; + + return ctLen; + } + } + + public static final class ChaCha20Only extends ChaCha20Cipher { + public ChaCha20Only() { + mode = MODE_NONE; + } + } + + public static final class ChaCha20Poly1305 extends ChaCha20Cipher { + public ChaCha20Poly1305() { + mode = MODE_AEAD; + authAlgName = "Poly1305"; + } + } +} diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Poly1305Parameters.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Poly1305Parameters.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.crypto.provider; + +import java.io.IOException; +import java.security.AlgorithmParametersSpi; +import java.security.spec.AlgorithmParameterSpec; +import java.security.spec.InvalidParameterSpecException; +import javax.crypto.spec.IvParameterSpec; +import sun.security.util.*; + +/** + * This class implements the parameter set used with the ChaCha20-Poly1305 + * algorithm. The parameter definition comes from + * RFC 8103 + * and is defined according to the following ASN.1: + * + *
+ * id-alg-AEADChaCha20Poly1305 OBJECT IDENTIFIER ::=
+          { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1)
+            pkcs9(9) smime(16) alg(3) 18 }
+
+ * AEADChaCha20Poly1305Nonce ::= OCTET STRING (SIZE(12))
+ * 
+ * + * The AlgorithmParameters may be instantiated either by its name + * ("ChaCha20-Poly1305") or via its OID (1.2.840.113549.1.9.16.3.18) + */ +public final class ChaCha20Poly1305Parameters extends AlgorithmParametersSpi { + + private static final String DEFAULT_FMT = "ASN.1"; + private byte[] nonce; + + public ChaCha20Poly1305Parameters() {} + + /** + * Initialize the ChaCha20Poly1305Parameters using an IvParameterSpec. + * + * @param paramSpec the {@code IvParameterSpec} used to configure + * this object. + * + * @throws InvalidParameterSpecException if an object of a type other + * than {@code IvParameterSpec} is used. + */ + @Override + protected void engineInit(AlgorithmParameterSpec paramSpec) + throws InvalidParameterSpecException { + + if (!(paramSpec instanceof IvParameterSpec)) { + throw new InvalidParameterSpecException + ("Inappropriate parameter specification"); + } + IvParameterSpec ivps = (IvParameterSpec)paramSpec; + + // Obtain the nonce + nonce = ivps.getIV(); + if (nonce.length != 12) { + throw new InvalidParameterSpecException("ChaCha20-Poly1305 nonce" + + " must be 12 bytes in length"); + } + } + + /** + * Initialize the ChaCha20Poly1305Parameters from a DER encoded + * parameter block. + + * @param encoded the DER encoding of the nonce as an OCTET STRING. + * + * @throws IOException if the encoded nonce is not 12 bytes long or a DER + * decoding error occurs. + */ + @Override + protected void engineInit(byte[] encoded) throws IOException { + DerValue val = new DerValue(encoded); + + // Get the nonce value + nonce = val.getOctetString(); + if (nonce.length != 12) { + throw new IOException( + "ChaCha20-Poly1305 nonce must be 12 bytes in length"); + } + } + + /** + * Initialize the ChaCha20Poly1305Parameters from a DER encoded + * parameter block. + * + * @param encoded the DER encoding of the nonce and initial block counter. + * @param decodingMethod the decoding method. The only currently accepted + * value is "ASN.1" + * + * @throws IOException if the encoded nonce is not 12 bytes long, a DER + * decoding error occurs, or an unsupported decoding method is + * provided. + */ + @Override + protected void engineInit(byte[] encoded, String decodingMethod) + throws IOException { + if (decodingMethod == null || + decodingMethod.equalsIgnoreCase(DEFAULT_FMT)) { + engineInit(encoded); + } else { + throw new IOException("Unsupported parameter format: " + + decodingMethod); + } + } + + /** + * Return an IvParameterSpec with the same parameters as those + * held in this object. + * + * @param paramSpec the class name of the spec. In this case it should + * be {@code IvParameterSpec.class}. + * + * @return a {@code IvParameterSpec} object containing the nonce + * value held in this object. + * + * @throws InvalidParameterSpecException if a class other than + * {@code IvParameterSpec.class} was specified in the paramSpec + * parameter. + */ + @Override + protected + T engineGetParameterSpec(Class paramSpec) + throws InvalidParameterSpecException { + + if (IvParameterSpec.class.isAssignableFrom(paramSpec)) { + return paramSpec.cast(new IvParameterSpec(nonce)); + } else { + throw new InvalidParameterSpecException + ("Inappropriate parameter specification"); + } + } + + /** + * Return the encoded parameters in ASN.1 form. + * + * @return a byte array containing the DER-encoding for the + * ChaCha20-Poly1305 parameters. This will be the nonce + * encoded as a DER OCTET STRING. + * + * @throws IOException if any DER encoding error occurs. + */ + @Override + protected byte[] engineGetEncoded() throws IOException { + DerOutputStream out = new DerOutputStream(); + out.write(DerValue.tag_OctetString, nonce); + return out.toByteArray(); + } + + /** + * Return the encoded parameters in ASN.1 form. + * + * @param encodingMethod the encoding method to be used. This parameter + * must be "ASN.1" as it is the only currently supported encoding + * format. If the parameter is {@code null} then the default + * encoding format will be used. + * + * @return a byte array containing the DER-encoding for the + * ChaCha20-Poly1305 parameters. + * + * @throws IOException if any DER encoding error occurs or an unsupported + * encoding method is provided. + */ + @Override + protected byte[] engineGetEncoded(String encodingMethod) + throws IOException { + if (encodingMethod == null || + encodingMethod.equalsIgnoreCase(DEFAULT_FMT)) { + return engineGetEncoded(); + } else { + throw new IOException("Unsupported encoding format: " + + encodingMethod); + } + } + + /** + * Creates a formatted string describing the parameters. + * + * @return a string representation of the ChaCha20 parameters. + */ + @Override + protected String engineToString() { + String LINE_SEP = System.lineSeparator(); + HexDumpEncoder encoder = new HexDumpEncoder(); + StringBuilder sb = new StringBuilder(LINE_SEP + "nonce:" + + LINE_SEP + "[" + encoder.encodeBuffer(nonce) + "]"); + return sb.toString(); + } +} diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/com/sun/crypto/provider/KeyGeneratorCore.java --- a/src/java.base/share/classes/com/sun/crypto/provider/KeyGeneratorCore.java Thu May 31 09:19:54 2018 -0400 +++ b/src/java.base/share/classes/com/sun/crypto/provider/KeyGeneratorCore.java Thu May 31 07:05:10 2018 -0700 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2003, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -111,16 +111,20 @@ protected HmacSHA2KG(String algoName, int len) { core = new KeyGeneratorCore(algoName, len); } + @Override protected void engineInit(SecureRandom random) { core.implInit(random); } + @Override protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException { core.implInit(params, random); } + @Override protected void engineInit(int keySize, SecureRandom random) { core.implInit(keySize, random); } + @Override protected SecretKey engineGenerateKey() { return core.implGenerateKey(); } @@ -153,13 +157,16 @@ public RC2KeyGenerator() { core = new KeyGeneratorCore("RC2", 128); } + @Override protected void engineInit(SecureRandom random) { core.implInit(random); } + @Override protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException { core.implInit(params, random); } + @Override protected void engineInit(int keySize, SecureRandom random) { if ((keySize < 40) || (keySize > 1024)) { throw new InvalidParameterException("Key length for RC2" @@ -167,6 +174,7 @@ } core.implInit(keySize, random); } + @Override protected SecretKey engineGenerateKey() { return core.implGenerateKey(); } @@ -178,13 +186,16 @@ public ARCFOURKeyGenerator() { core = new KeyGeneratorCore("ARCFOUR", 128); } + @Override protected void engineInit(SecureRandom random) { core.implInit(random); } + @Override protected void engineInit(AlgorithmParameterSpec params, SecureRandom random) throws InvalidAlgorithmParameterException { core.implInit(params, random); } + @Override protected void engineInit(int keySize, SecureRandom random) { if ((keySize < 40) || (keySize > 1024)) { throw new InvalidParameterException("Key length for ARCFOUR" @@ -192,9 +203,38 @@ } core.implInit(keySize, random); } + @Override protected SecretKey engineGenerateKey() { return core.implGenerateKey(); } } + // nested static class for the ChaCha20 key generator + public static final class ChaCha20KeyGenerator extends KeyGeneratorSpi { + private final KeyGeneratorCore core; + public ChaCha20KeyGenerator() { + core = new KeyGeneratorCore("ChaCha20", 256); + } + @Override + protected void engineInit(SecureRandom random) { + core.implInit(random); + } + @Override + protected void engineInit(AlgorithmParameterSpec params, + SecureRandom random) throws InvalidAlgorithmParameterException { + core.implInit(params, random); + } + @Override + protected void engineInit(int keySize, SecureRandom random) { + if (keySize != 256) { + throw new InvalidParameterException( + "Key length for ChaCha20 must be 256 bits"); + } + core.implInit(keySize, random); + } + @Override + protected SecretKey engineGenerateKey() { + return core.implGenerateKey(); + } + } } diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/com/sun/crypto/provider/Poly1305.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.base/share/classes/com/sun/crypto/provider/Poly1305.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.sun.crypto.provider; + +import java.nio.ByteBuffer; +import java.security.Key; +import java.security.InvalidKeyException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; +import java.util.Objects; + +import sun.security.util.math.*; +import sun.security.util.math.intpoly.*; + +/** + * This class represents the Poly1305 function defined in RFC 7539. + * + * This function is used in the implementation of ChaCha20/Poly1305 + * AEAD mode. + */ +final class Poly1305 { + + private static final int KEY_LENGTH = 32; + private static final int RS_LENGTH = KEY_LENGTH / 2; + private static final int BLOCK_LENGTH = 16; + private static final int TAG_LENGTH = 16; + + private static final IntegerFieldModuloP ipl1305 = + new IntegerPolynomial1305(); + + private byte[] keyBytes; + private final byte[] block = new byte[BLOCK_LENGTH]; + private int blockOffset; + + private IntegerModuloP r; + private IntegerModuloP s; + private MutableIntegerModuloP a; + private final MutableIntegerModuloP n = ipl1305.get1().mutable(); + + Poly1305() { } + + /** + * Initialize the Poly1305 object + * + * @param newKey the {@code Key} which will be used for the authentication. + * @param params this parameter is unused. + * + * @throws InvalidKeyException if {@code newKey} is {@code null} or is + * not 32 bytes in length. + */ + void engineInit(Key newKey, AlgorithmParameterSpec params) + throws InvalidKeyException { + Objects.requireNonNull(newKey, "Null key provided during init"); + keyBytes = newKey.getEncoded(); + if (keyBytes == null) { + throw new InvalidKeyException("Key does not support encoding"); + } else if (keyBytes.length != KEY_LENGTH) { + throw new InvalidKeyException("Incorrect length for key: " + + keyBytes.length); + } + + engineReset(); + setRSVals(); + } + + /** + * Returns the length of the MAC (authentication tag). + * + * @return the length of the auth tag, which is always 16 bytes. + */ + int engineGetMacLength() { + return TAG_LENGTH; + } + + /** + * Reset the Poly1305 object, discarding any current operation but + * maintaining the same key. + */ + void engineReset() { + // Clear the block and reset the offset + Arrays.fill(block, (byte)0); + blockOffset = 0; + // Discard any previous accumulator and start at zero + a = ipl1305.get0().mutable(); + } + + /** + * Update the MAC with bytes from a {@code ByteBuffer} + * + * @param buf the {@code ByteBuffer} containing the data to be consumed. + * Upon return the buffer's position will be equal to its limit. + */ + void engineUpdate(ByteBuffer buf) { + int remaining = buf.remaining(); + while (remaining > 0) { + int bytesToWrite = Integer.min(remaining, + BLOCK_LENGTH - blockOffset); + + if (bytesToWrite >= BLOCK_LENGTH) { + // If bytes to write == BLOCK_LENGTH, then we have no + // left-over data from previous updates and we can create + // the IntegerModuloP directly from the input buffer. + processBlock(buf, bytesToWrite); + } else { + // We have some left-over data from previous updates, so + // copy that into the holding block until we get a full block. + buf.get(block, blockOffset, bytesToWrite); + blockOffset += bytesToWrite; + + if (blockOffset >= BLOCK_LENGTH) { + processBlock(block, 0, BLOCK_LENGTH); + blockOffset = 0; + } + } + + remaining -= bytesToWrite; + } + } + + /** + * Update the MAC with bytes from an array. + * + * @param input the input bytes. + * @param offset the starting index from which to update the MAC. + * @param len the number of bytes to process. + */ + void engineUpdate(byte[] input, int offset, int len) { + Objects.checkFromIndexSize(offset, len, input.length); + if (blockOffset > 0) { + // We have some left-over data from previous updates + int blockSpaceLeft = BLOCK_LENGTH - blockOffset; + if (len < blockSpaceLeft) { + System.arraycopy(input, offset, block, blockOffset, len); + blockOffset += len; + return; // block wasn't filled + } else { + System.arraycopy(input, offset, block, blockOffset, + blockSpaceLeft); + offset += blockSpaceLeft; + len -= blockSpaceLeft; + processBlock(block, 0, BLOCK_LENGTH); + blockOffset = 0; + } + } + while (len >= BLOCK_LENGTH) { + processBlock(input, offset, BLOCK_LENGTH); + offset += BLOCK_LENGTH; + len -= BLOCK_LENGTH; + } + if (len > 0) { // and len < BLOCK_LENGTH + System.arraycopy(input, offset, block, 0, len); + blockOffset = len; + } + } + + /** + * Update the MAC with a single byte of input + * + * @param input the byte to update the MAC with. + */ + void engineUpdate(byte input) { + assert (blockOffset < BLOCK_LENGTH); + // we can't hold fully filled unprocessed block + block[blockOffset++] = input; + + if (blockOffset == BLOCK_LENGTH) { + processBlock(block, 0, BLOCK_LENGTH); + blockOffset = 0; + } + } + + + /** + * Finish the authentication operation and reset the MAC for a new + * authentication operation. + * + * @return the authentication tag as a byte array. + */ + byte[] engineDoFinal() { + byte[] tag = new byte[BLOCK_LENGTH]; + + // Finish up: process any remaining data < BLOCK_SIZE, then + // create the tag from the resulting little-endian integer. + if (blockOffset > 0) { + processBlock(block, 0, blockOffset); + blockOffset = 0; + } + + // Add in the s-half of the key to the accumulator + a.addModPowerTwo(s, tag); + + // Reset for the next auth + engineReset(); + return tag; + } + + /** + * Process a single block of data. This should only be called + * when the block array is complete. That may not necessarily + * be a full 16 bytes if the last block has less than 16 bytes. + */ + private void processBlock(ByteBuffer buf, int len) { + n.setValue(buf, len, (byte)0x01); + a.setSum(n); // a += (n | 0x01) + a.setProduct(r); // a = (a * r) % p + } + + private void processBlock(byte[] block, int offset, int length) { + Objects.checkFromIndexSize(offset, length, block.length); + n.setValue(block, offset, length, (byte)0x01); + a.setSum(n); // a += (n | 0x01) + a.setProduct(r); // a = (a * r) % p + } + + /** + * Partition the authentication key into the R and S components, clamp + * the R value, and instantiate IntegerModuloP objects to R and S's + * numeric values. + */ + private void setRSVals() { + // Clamp the bytes in the "r" half of the key. + keyBytes[3] &= 15; + keyBytes[7] &= 15; + keyBytes[11] &= 15; + keyBytes[15] &= 15; + keyBytes[4] &= 252; + keyBytes[8] &= 252; + keyBytes[12] &= 252; + + // Create IntegerModuloP elements from the r and s values + r = ipl1305.getElement(keyBytes, 0, RS_LENGTH, (byte)0); + s = ipl1305.getElement(keyBytes, RS_LENGTH, RS_LENGTH, (byte)0); + } +} diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java --- a/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java Thu May 31 09:19:54 2018 -0400 +++ b/src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java Thu May 31 07:05:10 2018 -0700 @@ -57,6 +57,8 @@ * * - ARCFOUR (RC4 compatible) * + * - ChaCha20 (Stream cipher only and in AEAD mode with Poly1305) + * * - Cipher modes ECB, CBC, CFB, OFB, PCBC, CTR, and CTS for all block ciphers * and mode GCM for AES cipher * @@ -77,7 +79,7 @@ private static final String info = "SunJCE Provider " + "(implements RSA, DES, Triple DES, AES, Blowfish, ARCFOUR, RC2, PBE, " - + "Diffie-Hellman, HMAC)"; + + "Diffie-Hellman, HMAC, ChaCha20)"; private static final String OID_PKCS12_RC4_128 = "1.2.840.113549.1.12.1.1"; private static final String OID_PKCS12_RC4_40 = "1.2.840.113549.1.12.1.2"; @@ -336,6 +338,15 @@ put("Cipher.ARCFOUR SupportedPaddings", "NOPADDING"); put("Cipher.ARCFOUR SupportedKeyFormats", "RAW"); + put("Cipher.ChaCha20", + "com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Only"); + put("Cipher.ChaCha20 SupportedKeyFormats", "RAW"); + put("Cipher.ChaCha20-Poly1305", + "com.sun.crypto.provider.ChaCha20Cipher$ChaCha20Poly1305"); + put("Cipher.ChaCha20-Poly1305 SupportedKeyFormats", "RAW"); + put("Alg.Alias.Cipher.1.2.840.113549.1.9.16.3.18", "ChaCha20-Poly1305"); + put("Alg.Alias.Cipher.OID.1.2.840.113549.1.9.16.3.18", "ChaCha20-Poly1305"); + /* * Key(pair) Generator engines */ @@ -361,6 +372,10 @@ "ARCFOURKeyGenerator"); put("Alg.Alias.KeyGenerator.RC4", "ARCFOUR"); + put("KeyGenerator.ChaCha20", + "com.sun.crypto.provider.KeyGeneratorCore$" + + "ChaCha20KeyGenerator"); + put("KeyGenerator.HmacMD5", "com.sun.crypto.provider.HmacMD5KeyGenerator"); @@ -541,6 +556,9 @@ put("AlgorithmParameters.OAEP", "com.sun.crypto.provider.OAEPParameters"); + put("AlgorithmParameters.ChaCha20-Poly1305", + "com.sun.crypto.provider.ChaCha20Poly1305Parameters"); + /* * Key factories */ diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/javax/crypto/Cipher.java --- a/src/java.base/share/classes/javax/crypto/Cipher.java Thu May 31 09:19:54 2018 -0400 +++ b/src/java.base/share/classes/javax/crypto/Cipher.java Thu May 31 07:05:10 2018 -0700 @@ -111,7 +111,7 @@ * encryption with a given key. When IVs are repeated for GCM * encryption, such usages are subject to forgery attacks. Thus, after * each encryption operation using GCM mode, callers should re-initialize - * the cipher objects with GCM parameters which has a different IV value. + * the cipher objects with GCM parameters which have a different IV value. *
  *     GCMParameterSpec s = ...;
  *     cipher.init(..., s);
@@ -131,6 +131,13 @@
  *     ...
  *
  * 
+ * The ChaCha20 and ChaCha20-Poly1305 algorithms have a similar requirement + * for unique nonces with a given key. After each encryption or decryption + * operation, callers should re-initialize their ChaCha20 or ChaCha20-Poly1305 + * ciphers with parameters that specify a different nonce value. Please + * see RFC 7539 for more + * information on the ChaCha20 and ChaCha20-Poly1305 algorithms. + *

* Every implementation of the Java platform is required to support * the following standard {@code Cipher} transformations with the keysizes * in parentheses: diff -r c75f3cdeb48c -r 25d711fca885 src/java.base/share/classes/javax/crypto/spec/ChaCha20ParameterSpec.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.base/share/classes/javax/crypto/spec/ChaCha20ParameterSpec.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package javax.crypto.spec; + +import java.security.spec.AlgorithmParameterSpec; +import java.util.Objects; + +/** + * This class specifies the parameters used with the + * ChaCha20 + * algorithm. + * + *

The parameters consist of a 12-byte nonce and an initial + * counter value expressed as a 32-bit integer. + * + *

This class can be used to initialize a {@code Cipher} object that + * implements the ChaCha20 algorithm. + * + * @since 11 + */ +public final class ChaCha20ParameterSpec implements AlgorithmParameterSpec { + + // The nonce length is defined by the spec as 96 bits (12 bytes) in length. + private static final int NONCE_LENGTH = 12; + + private final byte[] nonce; + private final int counter; + + /** + * Constructs a parameter set for ChaCha20 from the given nonce + * and counter. + * + * @param nonce a 12-byte nonce value + * @param counter the initial counter value + * + * @throws NullPointerException if {@code nonce} is {@code null} + * @throws IllegalArgumentException if {@code nonce} is not 12 bytes + * in length + */ + public ChaCha20ParameterSpec(byte[] nonce, int counter) { + this.counter = counter; + + Objects.requireNonNull(nonce, "Nonce must be non-null"); + this.nonce = nonce.clone(); + if (this.nonce.length != NONCE_LENGTH) { + throw new IllegalArgumentException( + "Nonce must be 12-bytes in length"); + } + } + + /** + * Returns the nonce value. + * + * @return the nonce value. This method returns a new array each time + * this method is called. + */ + public byte[] getNonce() { + return nonce.clone(); + } + + /** + * Returns the configured counter value. + * + * @return the counter value + */ + public int getCounter() { + return counter; + } +} diff -r c75f3cdeb48c -r 25d711fca885 test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20KAT.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20KAT.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8153029 + * @library /test/lib + * @build jdk.test.lib.Convert + * @run main ChaCha20KAT + * @summary ChaCha20 Cipher Implementation (KAT) + */ + +import java.util.*; +import java.security.GeneralSecurityException; +import javax.crypto.Cipher; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.AEADBadTagException; +import java.nio.ByteBuffer; +import jdk.test.lib.Convert; + +public class ChaCha20KAT { + public static class TestData { + public TestData(String name, String keyStr, String nonceStr, int ctr, + int dir, String inputStr, String aadStr, String outStr) { + testName = Objects.requireNonNull(name); + key = Convert.hexStringToByteArray(Objects.requireNonNull(keyStr)); + nonce = Convert.hexStringToByteArray( + Objects.requireNonNull(nonceStr)); + if ((counter = ctr) < 0) { + throw new IllegalArgumentException( + "counter must be 0 or greater"); + } + direction = dir; + if ((direction != Cipher.ENCRYPT_MODE) && + (direction != Cipher.DECRYPT_MODE)) { + throw new IllegalArgumentException( + "Direction must be ENCRYPT_MODE or DECRYPT_MODE"); + } + input = Convert.hexStringToByteArray( + Objects.requireNonNull(inputStr)); + aad = (aadStr != null) ? + Convert.hexStringToByteArray(aadStr) : null; + expOutput = Convert.hexStringToByteArray( + Objects.requireNonNull(outStr)); + } + + public final String testName; + public final byte[] key; + public final byte[] nonce; + public final int counter; + public final int direction; + public final byte[] input; + public final byte[] aad; + public final byte[] expOutput; + } + + public static final List testList = new LinkedList() {{ + add(new TestData("RFC 7539 Sample Test Vector", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "000000000000004a00000000", + 1, Cipher.ENCRYPT_MODE, + "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e", + null, + "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b" + + "f91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d8" + + "07ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab7793736" + + "5af90bbf74a35be6b40b8eedf2785e42874d")); + add(new TestData("RFC 7539 Test Vector 1 (all zeroes)", + "0000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000", + 0, Cipher.ENCRYPT_MODE, + "0000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000000", + null, + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" + + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586")); + add(new TestData("RFC 7539 Test Vector 2", + "0000000000000000000000000000000000000000000000000000000000000001", + "000000000000000000000002", + 1, Cipher.ENCRYPT_MODE, + "416e79207375626d697373696f6e20746f20746865204945544620696e74656e" + + "6465642062792074686520436f6e7472696275746f7220666f72207075626c69" + + "636174696f6e20617320616c6c206f722070617274206f6620616e2049455446" + + "20496e7465726e65742d4472616674206f722052464320616e6420616e792073" + + "746174656d656e74206d6164652077697468696e2074686520636f6e74657874" + + "206f6620616e204945544620616374697669747920697320636f6e7369646572" + + "656420616e20224945544620436f6e747269627574696f6e222e205375636820" + + "73746174656d656e747320696e636c756465206f72616c2073746174656d656e" + + "747320696e20494554462073657373696f6e732c2061732077656c6c20617320" + + "7772697474656e20616e6420656c656374726f6e696320636f6d6d756e696361" + + "74696f6e73206d61646520617420616e792074696d65206f7220706c6163652c" + + "207768696368206172652061646472657373656420746f", + null, + "a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec" + + "2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d" + + "4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e527950" + + "42bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85a" + + "d00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259d" + + "c4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b" + + "0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6c" + + "cc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0b" + + "c39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f" + + "5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e6" + + "98ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab" + + "7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221")); + add(new TestData("RFC 7539 Test Vector 3", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "000000000000000000000002", + 42, Cipher.ENCRYPT_MODE, + "2754776173206272696c6c69672c20616e642074686520736c6974687920746f" + + "7665730a446964206779726520616e642067696d626c6520696e207468652077" + + "6162653a0a416c6c206d696d737920776572652074686520626f726f676f7665" + + "732c0a416e6420746865206d6f6d65207261746873206f757467726162652e", + null, + "62e6347f95ed87a45ffae7426f27a1df5fb69110044c0d73118effa95b01e5cf" + + "166d3df2d721caf9b21e5fb14c616871fd84c54f9d65b283196c7fe4f60553eb" + + "f39c6402c42234e32a356b3e764312a61a5532055716ead6962568f87d3f3f77" + + "04c6a8d1bcd1bf4d50d6154b6da731b187b58dfd728afa36757a797ac188d1")); + }}; + + public static final List aeadTestList = + new LinkedList() {{ + add(new TestData("RFC 7539 Sample AEAD Test Vector", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "070000004041424344454647", + 1, Cipher.ENCRYPT_MODE, + "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e", + "50515253c0c1c2c3c4c5c6c7", + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" + + "0691")); + add(new TestData("RFC 7539 A.5 Sample Decryption", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "000000000102030405060708", + 1, Cipher.DECRYPT_MODE, + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + + "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38", + "f33388860000000000004e91", + "496e7465726e65742d4472616674732061726520647261667420646f63756d65" + + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + + "726573732e2fe2809d")); + }}; + + + public static void main(String args[]) throws Exception { + int testsPassed = 0; + int testNumber = 0; + + System.out.println("----- Single-part (byte[]) Tests -----"); + for (TestData test : testList) { + System.out.println("*** Test " + ++testNumber + ": " + + test.testName); + if (runSinglePartTest(test)) { + testsPassed++; + } + } + System.out.println(); + + System.out.println("----- Single-part (ByteBuffer) Tests -----"); + for (TestData test : testList) { + System.out.println("*** Test " + ++testNumber + ": " + + test.testName); + if (runByteBuffer(test)) { + testsPassed++; + } + } + System.out.println(); + + System.out.println("----- Multi-part Tests -----"); + for (TestData test : testList) { + System.out.println("*** Test " + ++testNumber + ": " + + test.testName); + if (runMultiPartTest(test)) { + testsPassed++; + } + } + System.out.println(); + + System.out.println("----- AEAD Tests -----"); + for (TestData test : aeadTestList) { + System.out.println("*** Test " + ++testNumber + ": " + + test.testName); + if (runAEADTest(test)) { + testsPassed++; + } + } + System.out.println(); + + System.out.println("Total tests: " + testNumber + + ", Passed: " + testsPassed + ", Failed: " + + (testNumber - testsPassed)); + if (testsPassed != testNumber) { + throw new RuntimeException("One or more tests failed. " + + "Check output for details"); + } + } + + private static boolean runSinglePartTest(TestData testData) + throws GeneralSecurityException { + boolean encRes = false; + boolean decRes = false; + byte[] encryptedResult; + byte[] decryptedResult; + + // Get a Cipher instance and set up the parameters + Cipher mambo = Cipher.getInstance("ChaCha20"); + SecretKeySpec mamboKey = new SecretKeySpec(testData.key, "ChaCha20"); + ChaCha20ParameterSpec mamboSpec = new ChaCha20ParameterSpec( + testData.nonce, testData.counter); + + // Encrypt our input + mambo.init(Cipher.ENCRYPT_MODE, mamboKey, mamboSpec); + encryptedResult = mambo.doFinal(testData.input); + + if (!Arrays.equals(encryptedResult, testData.expOutput)) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(testData.expOutput, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(encryptedResult, 16, "\n", " ")); + System.out.println(); + } else { + encRes = true; + } + + // Decrypt the result of the encryption operation + mambo = Cipher.getInstance("ChaCha20"); + mambo.init(Cipher.DECRYPT_MODE, mamboKey, mamboSpec); + decryptedResult = mambo.doFinal(encryptedResult); + + if (!Arrays.equals(decryptedResult, testData.input)) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(testData.input, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(decryptedResult, 16, "\n", " ")); + System.out.println(); + } else { + decRes = true; + } + + return (encRes && decRes); + } + + private static boolean runMultiPartTest(TestData testData) + throws GeneralSecurityException { + boolean encRes = false; + boolean decRes = false; + + // Get a cipher instance and initialize it + Cipher mambo = Cipher.getInstance("ChaCha20"); + SecretKeySpec mamboKey = new SecretKeySpec(testData.key, "ChaCha20"); + ChaCha20ParameterSpec mamboSpec = new ChaCha20ParameterSpec( + testData.nonce, testData.counter); + + byte[] encryptedResult = new byte[testData.input.length]; + mambo.init(Cipher.ENCRYPT_MODE, mamboKey, mamboSpec); + System.out.print("Encrypt - "); + doMulti(mambo, testData.input, encryptedResult); + + if (!Arrays.equals(encryptedResult, testData.expOutput)) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(testData.expOutput, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(encryptedResult, 16, "\n", " ")); + System.out.println(); + } else { + encRes = true; + } + + // Decrypt the result of the encryption operation + mambo = Cipher.getInstance("ChaCha20"); + byte[] decryptedResult = new byte[encryptedResult.length]; + mambo.init(Cipher.DECRYPT_MODE, mamboKey, mamboSpec); + System.out.print("Decrypt - "); + doMulti(mambo, encryptedResult, decryptedResult); + + if (!Arrays.equals(decryptedResult, testData.input)) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(testData.input, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(decryptedResult, 16, "\n", " ")); + System.out.println(); + } else { + decRes = true; + } + + return (encRes && decRes); + } + + private static void doMulti(Cipher c, byte[] input, byte[] output) + throws GeneralSecurityException { + int offset = 0; + boolean done = false; + Random randIn = new Random(System.currentTimeMillis()); + + // Send small updates between 1 - 8 bytes in length until we get + // 8 or less bytes from the end of the input, then finalize. + System.out.println("Input length: " + input.length); + System.out.print("Multipart (bytes in/out): "); + while (!done) { + int mPartLen = randIn.nextInt(8) + 1; + int bytesLeft = input.length - offset; + int processed; + if (mPartLen < bytesLeft) { + System.out.print(mPartLen + "/"); + processed = c.update(input, offset, mPartLen, + output, offset); + offset += processed; + System.out.print(processed + " "); + } else { + processed = c.doFinal(input, offset, bytesLeft, + output, offset); + System.out.print(bytesLeft + "/" + processed + " "); + done = true; + } + } + System.out.println(); + } + + private static boolean runByteBuffer(TestData testData) + throws GeneralSecurityException { + boolean encRes = false; + boolean decRes = false; + + // Get a cipher instance and initialize it + Cipher mambo = Cipher.getInstance("ChaCha20"); + SecretKeySpec mamboKey = new SecretKeySpec(testData.key, "ChaCha20"); + ChaCha20ParameterSpec mamboSpec = new ChaCha20ParameterSpec( + testData.nonce, testData.counter); + mambo.init(Cipher.ENCRYPT_MODE, mamboKey, mamboSpec); + + ByteBuffer bbIn = ByteBuffer.wrap(testData.input); + ByteBuffer bbEncOut = ByteBuffer.allocate( + mambo.getOutputSize(testData.input.length)); + ByteBuffer bbExpOut = ByteBuffer.wrap(testData.expOutput); + + mambo.doFinal(bbIn, bbEncOut); + bbIn.rewind(); + bbEncOut.rewind(); + + if (bbEncOut.compareTo(bbExpOut) != 0) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(bbExpOut, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(bbEncOut, 16, "\n", " ")); + System.out.println(); + } else { + encRes = true; + } + + // Decrypt the result of the encryption operation + mambo = Cipher.getInstance("ChaCha20"); + mambo.init(Cipher.DECRYPT_MODE, mamboKey, mamboSpec); + System.out.print("Decrypt - "); + ByteBuffer bbDecOut = ByteBuffer.allocate( + mambo.getOutputSize(bbEncOut.remaining())); + + mambo.doFinal(bbEncOut, bbDecOut); + bbEncOut.rewind(); + bbDecOut.rewind(); + + if (bbDecOut.compareTo(bbIn) != 0) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(bbIn, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(bbDecOut, 16, "\n", " ")); + System.out.println(); + } else { + decRes = true; + } + + return (encRes && decRes); + } + + private static boolean runAEADTest(TestData testData) + throws GeneralSecurityException { + boolean result = false; + + Cipher mambo = Cipher.getInstance("ChaCha20-Poly1305"); + SecretKeySpec mamboKey = new SecretKeySpec(testData.key, "ChaCha20"); + IvParameterSpec mamboSpec = new IvParameterSpec(testData.nonce); + + mambo.init(testData.direction, mamboKey, mamboSpec); + + byte[] out = new byte[mambo.getOutputSize(testData.input.length)]; + int outOff = 0; + try { + mambo.updateAAD(testData.aad); + outOff += mambo.update(testData.input, 0, testData.input.length, + out, outOff); + outOff += mambo.doFinal(out, outOff); + } catch (AEADBadTagException abte) { + // If we get a bad tag or derive a tag mismatch, log it + // and register it as a failure + System.out.println("FAIL: " + abte); + return false; + } + + if (!Arrays.equals(out, testData.expOutput)) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(testData.expOutput, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(out, 16, "\n", " ")); + System.out.println(); + } else { + result = true; + } + + return result; + } + + /** + * Dump the hex bytes of a buffer into string form. + * + * @param data The array of bytes to dump to stdout. + * @param itemsPerLine The number of bytes to display per line + * if the {@code lineDelim} character is blank then all bytes + * will be printed on a single line. + * @param lineDelim The delimiter between lines + * @param itemDelim The delimiter between bytes + * + * @return The hexdump of the byte array + */ + private static String dumpHexBytes(byte[] data, int itemsPerLine, + String lineDelim, String itemDelim) { + return dumpHexBytes(ByteBuffer.wrap(data), itemsPerLine, lineDelim, + itemDelim); + } + + private static String dumpHexBytes(ByteBuffer data, int itemsPerLine, + String lineDelim, String itemDelim) { + StringBuilder sb = new StringBuilder(); + if (data != null) { + data.mark(); + int i = 0; + while (data.remaining() > 0) { + if (i % itemsPerLine == 0 && i != 0) { + sb.append(lineDelim); + } + sb.append(String.format("%02X", data.get())).append(itemDelim); + i++; + } + data.reset(); + } + + return sb.toString(); + } +} + diff -r c75f3cdeb48c -r 25d711fca885 test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20NoReuse.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20NoReuse.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,625 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8153029 + * @library /test/lib + * @build jdk.test.lib.Convert + * @run main ChaCha20NoReuse + * @summary ChaCha20 Cipher Implementation (key/nonce reuse protection) + */ + +import java.util.*; +import javax.crypto.Cipher; +import java.security.spec.AlgorithmParameterSpec; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.AEADBadTagException; +import javax.crypto.SecretKey; +import java.security.InvalidKeyException; +import jdk.test.lib.Convert; + +public class ChaCha20NoReuse { + + private static final String ALG_CC20 = "ChaCha20"; + private static final String ALG_CC20_P1305 = "ChaCha20-Poly1305"; + + /** + * Basic TestMethod interface definition. + */ + public interface TestMethod { + /** + * Runs the actual test case + * + * @param algorithm the algorithm to use (e.g. ChaCha20, etc.) + * + * @return true if the test passes, false otherwise. + */ + boolean run(String algorithm); + + /** + * Check if this TestMethod can be run for this algorithm. Some tests + * are specific to ChaCha20 or ChaCha20-Poly1305, so this method + * can be used to determine if a given Cipher type is appropriate. + * + * @param algorithm the algorithm to use. + * + * @return true if this test can be run on this algorithm, + * false otherwise. + */ + boolean isValid(String algorithm); + } + + public static class TestData { + public TestData(String name, String keyStr, String nonceStr, int ctr, + int dir, String inputStr, String aadStr, String outStr) { + testName = Objects.requireNonNull(name); + key = Convert.hexStringToByteArray(Objects.requireNonNull(keyStr)); + nonce = Convert.hexStringToByteArray( + Objects.requireNonNull(nonceStr)); + if ((counter = ctr) < 0) { + throw new IllegalArgumentException( + "counter must be 0 or greater"); + } + direction = dir; + if ((direction != Cipher.ENCRYPT_MODE) && + (direction != Cipher.DECRYPT_MODE)) { + throw new IllegalArgumentException( + "Direction must be ENCRYPT_MODE or DECRYPT_MODE"); + } + input = Convert.hexStringToByteArray( + Objects.requireNonNull(inputStr)); + aad = (aadStr != null) ? + Convert.hexStringToByteArray(aadStr) : null; + expOutput = Convert.hexStringToByteArray( + Objects.requireNonNull(outStr)); + } + + public final String testName; + public final byte[] key; + public final byte[] nonce; + public final int counter; + public final int direction; + public final byte[] input; + public final byte[] aad; + public final byte[] expOutput; + } + + public static final List testList = new LinkedList() {{ + add(new TestData("RFC 7539 Sample Test Vector [ENCRYPT]", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "000000000000004a00000000", + 1, Cipher.ENCRYPT_MODE, + "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e", + null, + "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b" + + "f91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d8" + + "07ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab7793736" + + "5af90bbf74a35be6b40b8eedf2785e42874d")); + add(new TestData("RFC 7539 Sample Test Vector [DECRYPT]", + "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", + "000000000000004a00000000", + 1, Cipher.DECRYPT_MODE, + "6e2e359a2568f98041ba0728dd0d6981e97e7aec1d4360c20a27afccfd9fae0b" + + "f91b65c5524733ab8f593dabcd62b3571639d624e65152ab8f530c359f0861d8" + + "07ca0dbf500d6a6156a38e088a22b65e52bc514d16ccf806818ce91ab7793736" + + "5af90bbf74a35be6b40b8eedf2785e42874d", + null, + "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e")); + }}; + + public static final List aeadTestList = + new LinkedList() {{ + add(new TestData("RFC 7539 Sample AEAD Test Vector", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "070000004041424344454647", + 1, Cipher.ENCRYPT_MODE, + "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e", + "50515253c0c1c2c3c4c5c6c7", + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" + + "0691")); + add(new TestData("RFC 7539 A.5 Sample Decryption", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "000000000102030405060708", + 1, Cipher.DECRYPT_MODE, + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + + "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38", + "f33388860000000000004e91", + "496e7465726e65742d4472616674732061726520647261667420646f63756d65" + + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + + "726573732e2fe2809d")); + }}; + + /** + * Make sure we do not use this Cipher object without initializing it + * at all + */ + public static final TestMethod noInitTest = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return true; // Valid for all algs + } + + @Override + public boolean run(String algorithm) { + System.out.println("----- No Init Test [" + algorithm + + "] -----"); + try { + Cipher cipher = Cipher.getInstance(algorithm); + TestData testData; + switch (algorithm) { + case ALG_CC20: + testData = testList.get(0); + break; + case ALG_CC20_P1305: + testData = aeadTestList.get(0); + break; + default: + throw new IllegalArgumentException( + "Unsupported cipher type: " + algorithm); + } + + // Attempting to use the cipher without initializing it + // should throw an IllegalStateException + try { + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + throw new RuntimeException( + "Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + /** + * Make sure we don't allow a double init using the same parameters + */ + public static final TestMethod doubleInitTest = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return true; // Valid for all algs + } + + @Override + public boolean run(String algorithm) { + System.out.println("----- Double Init Test [" + algorithm + + "] -----"); + try { + AlgorithmParameterSpec spec; + Cipher cipher = Cipher.getInstance(algorithm); + TestData testData; + switch (algorithm) { + case ALG_CC20: + testData = testList.get(0); + spec = new ChaCha20ParameterSpec(testData.nonce, + testData.counter); + break; + case ALG_CC20_P1305: + testData = aeadTestList.get(0); + spec = new IvParameterSpec(testData.nonce); + break; + default: + throw new IllegalArgumentException( + "Unsupported cipher type: " + algorithm); + } + SecretKey key = new SecretKeySpec(testData.key, ALG_CC20); + + // Initialize the first time, this should work. + cipher.init(testData.direction, key, spec); + + // Immediately initializing a second time with the same + // parameters should fail + try { + cipher.init(testData.direction, key, spec); + throw new RuntimeException( + "Expected InvalidKeyException not thrown"); + } catch (InvalidKeyException ike) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + /** + * Attempt to run two full encryption operations without an init in + * between. + */ + public static final TestMethod encTwiceNoInit = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return true; // Valid for all algs + } + + @Override + public boolean run(String algorithm) { + System.out.println("----- Encrypt second time without init [" + + algorithm + "] -----"); + try { + AlgorithmParameterSpec spec; + Cipher cipher = Cipher.getInstance(algorithm); + TestData testData; + switch (algorithm) { + case ALG_CC20: + testData = testList.get(0); + spec = new ChaCha20ParameterSpec(testData.nonce, + testData.counter); + break; + case ALG_CC20_P1305: + testData = aeadTestList.get(0); + spec = new IvParameterSpec(testData.nonce); + break; + default: + throw new IllegalArgumentException( + "Unsupported cipher type: " + algorithm); + } + SecretKey key = new SecretKeySpec(testData.key, ALG_CC20); + + // Initialize and encrypt + cipher.init(testData.direction, key, spec); + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + System.out.println("First encryption complete"); + + // Now attempt to encrypt again without changing the key/IV + // This should fail. + try { + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + throw new RuntimeException( + "Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + /** + * Attempt to run two full decryption operations without an init in + * between. + */ + public static final TestMethod decTwiceNoInit = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return true; // Valid for all algs + } + + @Override + public boolean run(String algorithm) { + System.out.println("----- Decrypt second time without init [" + + algorithm + "] -----"); + try { + AlgorithmParameterSpec spec; + Cipher cipher = Cipher.getInstance(algorithm); + TestData testData; + switch (algorithm) { + case ALG_CC20: + testData = testList.get(1); + spec = new ChaCha20ParameterSpec(testData.nonce, + testData.counter); + break; + case ALG_CC20_P1305: + testData = aeadTestList.get(1); + spec = new IvParameterSpec(testData.nonce); + break; + default: + throw new IllegalArgumentException( + "Unsupported cipher type: " + algorithm); + } + SecretKey key = new SecretKeySpec(testData.key, ALG_CC20); + + // Initialize and encrypt + cipher.init(testData.direction, key, spec); + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + System.out.println("First decryption complete"); + + // Now attempt to encrypt again without changing the key/IV + // This should fail. + try { + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + throw new RuntimeException( + "Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + /** + * Perform an AEAD decryption with corrupted data so the tag does not + * match. Then attempt to reuse the cipher without initialization. + */ + public static final TestMethod decFailNoInit = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return algorithm.equals(ALG_CC20_P1305); + } + + @Override + public boolean run(String algorithm) { + System.out.println( + "----- Fail decryption, try again with no init [" + + algorithm + "] -----"); + try { + TestData testData = aeadTestList.get(1); + AlgorithmParameterSpec spec = + new IvParameterSpec(testData.nonce); + byte[] corruptInput = testData.input.clone(); + corruptInput[0]++; // Corrupt the ciphertext + SecretKey key = new SecretKeySpec(testData.key, ALG_CC20); + Cipher cipher = Cipher.getInstance(algorithm); + + try { + // Initialize and encrypt + cipher.init(testData.direction, key, spec); + cipher.updateAAD(testData.aad); + cipher.doFinal(corruptInput); + throw new RuntimeException( + "Expected AEADBadTagException not thrown"); + } catch (AEADBadTagException abte) { + System.out.println("Expected decryption failure occurred"); + } + + // Make sure that despite the exception, the Cipher object is + // not in a state that would leave it initialized and able + // to process future decryption operations without init. + try { + cipher.updateAAD(testData.aad); + cipher.doFinal(testData.input); + throw new RuntimeException( + "Expected IllegalStateException not thrown"); + } catch (IllegalStateException ise) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + /** + * Encrypt once successfully, then attempt to init with the same + * key and nonce. + */ + public static final TestMethod encTwiceInitSameParams = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return true; // Valid for all algs + } + + @Override + public boolean run(String algorithm) { + System.out.println("----- Encrypt, then init with same params [" + + algorithm + "] -----"); + try { + AlgorithmParameterSpec spec; + Cipher cipher = Cipher.getInstance(algorithm); + TestData testData; + switch (algorithm) { + case ALG_CC20: + testData = testList.get(0); + spec = new ChaCha20ParameterSpec(testData.nonce, + testData.counter); + break; + case ALG_CC20_P1305: + testData = aeadTestList.get(0); + spec = new IvParameterSpec(testData.nonce); + break; + default: + throw new IllegalArgumentException( + "Unsupported cipher type: " + algorithm); + } + SecretKey key = new SecretKeySpec(testData.key, ALG_CC20); + + // Initialize then encrypt + cipher.init(testData.direction, key, spec); + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + System.out.println("First encryption complete"); + + // Initializing after the completed encryption with + // the same key and nonce should fail. + try { + cipher.init(testData.direction, key, spec); + throw new RuntimeException( + "Expected InvalidKeyException not thrown"); + } catch (InvalidKeyException ike) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + /** + * Decrypt once successfully, then attempt to init with the same + * key and nonce. + */ + public static final TestMethod decTwiceInitSameParams = new TestMethod() { + @Override + public boolean isValid(String algorithm) { + return true; // Valid for all algs + } + + @Override + public boolean run(String algorithm) { + System.out.println("----- Decrypt, then init with same params [" + + algorithm + "] -----"); + try { + AlgorithmParameterSpec spec; + Cipher cipher = Cipher.getInstance(algorithm); + TestData testData; + switch (algorithm) { + case ALG_CC20: + testData = testList.get(1); + spec = new ChaCha20ParameterSpec(testData.nonce, + testData.counter); + break; + case ALG_CC20_P1305: + testData = aeadTestList.get(1); + spec = new IvParameterSpec(testData.nonce); + break; + default: + throw new IllegalArgumentException( + "Unsupported cipher type: " + algorithm); + } + SecretKey key = new SecretKeySpec(testData.key, ALG_CC20); + + // Initialize then decrypt + cipher.init(testData.direction, key, spec); + if (algorithm.equals(ALG_CC20_P1305)) { + cipher.updateAAD(testData.aad); + } + cipher.doFinal(testData.input); + System.out.println("First decryption complete"); + + // Initializing after the completed decryption with + // the same key and nonce should fail. + try { + cipher.init(testData.direction, key, spec); + throw new RuntimeException( + "Expected InvalidKeyException not thrown"); + } catch (InvalidKeyException ike) { + // Do nothing, this is what we expected to happen + } + } catch (Exception exc) { + System.out.println("Unexpected exception: " + exc); + exc.printStackTrace(); + return false; + } + + return true; + } + }; + + public static final List algList = + Arrays.asList(ALG_CC20, ALG_CC20_P1305); + + public static final List testMethodList = + Arrays.asList(noInitTest, doubleInitTest, encTwiceNoInit, + decTwiceNoInit, decFailNoInit, encTwiceInitSameParams, + decTwiceInitSameParams); + + public static void main(String args[]) throws Exception { + int testsPassed = 0; + int testNumber = 0; + + for (TestMethod tm : testMethodList) { + for (String alg : algList) { + if (tm.isValid(alg)) { + testNumber++; + boolean result = tm.run(alg); + System.out.println("Result: " + (result ? "PASS" : "FAIL")); + if (result) { + testsPassed++; + } + } + } + } + + System.out.println("Total Tests: " + testNumber + + ", Tests passed: " + testsPassed); + if (testsPassed < testNumber) { + throw new RuntimeException( + "Not all tests passed. See output for failure info"); + } + } +} + diff -r c75f3cdeb48c -r 25d711fca885 test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20Poly1305ParamTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20Poly1305ParamTest.java Thu May 31 07:05:10 2018 -0700 @@ -0,0 +1,414 @@ +/* + * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/** + * @test + * @bug 8153029 + * @library /test/lib + * @build jdk.test.lib.Convert + * @run main ChaCha20Poly1305ParamTest + * @summary ChaCha20 Cipher Implementation (parameters) + */ + +import java.util.*; +import java.io.IOException; +import java.security.GeneralSecurityException; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.ChaCha20ParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.crypto.AEADBadTagException; +import java.security.spec.AlgorithmParameterSpec; +import java.security.AlgorithmParameters; +import java.security.NoSuchAlgorithmException; +import java.nio.ByteBuffer; +import jdk.test.lib.Convert; + +public class ChaCha20Poly1305ParamTest { + public static class TestData { + public TestData(String name, String keyStr, String nonceStr, int ctr, + int dir, String inputStr, String aadStr, String outStr) { + testName = Objects.requireNonNull(name); + key = Convert.hexStringToByteArray(Objects.requireNonNull(keyStr)); + nonce = Convert.hexStringToByteArray( + Objects.requireNonNull(nonceStr)); + if ((counter = ctr) < 0) { + throw new IllegalArgumentException( + "counter must be 0 or greater"); + } + direction = dir; + if ((direction != Cipher.ENCRYPT_MODE) && + (direction != Cipher.DECRYPT_MODE)) { + throw new IllegalArgumentException( + "Direction must be ENCRYPT_MODE or DECRYPT_MODE"); + } + input = Convert.hexStringToByteArray( + Objects.requireNonNull(inputStr)); + aad = (aadStr != null) ? + Convert.hexStringToByteArray(aadStr) : null; + expOutput = Convert.hexStringToByteArray( + Objects.requireNonNull(outStr)); + } + + public final String testName; + public final byte[] key; + public final byte[] nonce; + public final int counter; + public final int direction; + public final byte[] input; + public final byte[] aad; + public final byte[] expOutput; + } + + public static final List aeadTestList = + new LinkedList() {{ + add(new TestData("RFC 7539 Sample AEAD Test Vector", + "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", + "070000004041424344454647", + 1, Cipher.ENCRYPT_MODE, + "4c616469657320616e642047656e746c656d656e206f662074686520636c6173" + + "73206f66202739393a204966204920636f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069742e", + "50515253c0c1c2c3c4c5c6c7", + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b61161ae10b594f09e26a7e902ecbd060" + + "0691")); + add(new TestData("RFC 7539 A.5 Sample Decryption", + "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0", + "000000000102030405060708", + 1, Cipher.DECRYPT_MODE, + "64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb2" + + "4c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf" + + "332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c855" + + "9797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4" + + "b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523e" + + "af4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a" + + "0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a10" + + "49e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29" + + "a6ad5cb4022b02709beead9d67890cbb22392336fea1851f38", + "f33388860000000000004e91", + "496e7465726e65742d4472616674732061726520647261667420646f63756d65" + + "6e74732076616c696420666f722061206d6178696d756d206f6620736978206d" + + "6f6e74687320616e64206d617920626520757064617465642c207265706c6163" + + "65642c206f72206f62736f6c65746564206279206f7468657220646f63756d65" + + "6e747320617420616e792074696d652e20497420697320696e617070726f7072" + + "6961746520746f2075736520496e7465726e65742d4472616674732061732072" + + "65666572656e6365206d6174657269616c206f7220746f206369746520746865" + + "6d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67" + + "726573732e2fe2809d")); + }}; + + // 12-byte nonce DER-encoded as an OCTET_STRING + public static final byte[] NONCE_OCTET_STR_12 = { + 4, 12, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8 + }; + + // Invalid 16-byte nonce DER-encoded as an OCTET_STRING + public static final byte[] NONCE_OCTET_STR_16 = { + 4, 16, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 + }; + + // Throwaway key for default init tests + public static final SecretKey DEF_KEY = new SecretKeySpec(new byte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 + }, "ChaCha20"); + + public static void main(String args[]) throws Exception { + int testsPassed = 0; + int testNumber = 0; + + // Try some default initializations + testDefaultAlgParams("ChaCha20", Cipher.ENCRYPT_MODE, true); + testDefaultAlgParams("ChaCha20-Poly1305", Cipher.ENCRYPT_MODE, true); + testDefaultAlgParamSpec("ChaCha20", Cipher.ENCRYPT_MODE, true); + testDefaultAlgParamSpec("ChaCha20-Poly1305", Cipher.ENCRYPT_MODE, true); + testDefaultAlgParams("ChaCha20", Cipher.DECRYPT_MODE, false); + testDefaultAlgParams("ChaCha20-Poly1305", Cipher.DECRYPT_MODE, false); + testDefaultAlgParamSpec("ChaCha20", Cipher.DECRYPT_MODE, false); + testDefaultAlgParamSpec("ChaCha20-Poly1305", Cipher.DECRYPT_MODE, + false); + + // Try (and hopefully fail) to create a ChaCha20 AlgorithmParameterSpec + System.out.println( + "*** Test: Try to make ChaCha20 AlgorithmParameterSpec"); + try { + ChaCha20ParameterSpec badChaCha20Spec = + new ChaCha20ParameterSpec(NONCE_OCTET_STR_16, 1); + throw new RuntimeException("ChaCha20 AlgorithmParameterSpec " + + "with 16 byte nonce should fail"); + } catch (IllegalArgumentException iae) { + System.out.println("Caught expected exception: " + iae); + } + + // Try (and hopefully fail) to create a ChaCha20 AlgorithmParameters + System.out.println( + "*** Test: Try to make ChaCha20 AlgorithmParameters"); + try { + AlgorithmParameters apsNoChaCha20 = + AlgorithmParameters.getInstance("ChaCha20"); + throw new RuntimeException( + "ChaCha20 AlgorithmParameters should fail"); + } catch (NoSuchAlgorithmException nsae) { + System.out.println("Caught expected exception: " + nsae); + } + + // Create the AlgorithmParameters object from a valid encoding + System.out.println("*** Test: Create and init ChaCha20-Poly1305 APS"); + AlgorithmParameters apsGood = + AlgorithmParameters.getInstance("ChaCha20-Poly1305"); + apsGood.init(NONCE_OCTET_STR_12); + System.out.println("Test Passed"); + + // Pull an AlgorithmParameters object out of the initialized cipher + // and compare its value against the original. + System.out.println("*** Test: Init ChaCha20-Poly1305 Cipher using " + + "AP, retrieve AP and compare"); + Cipher cc20p1305 = Cipher.getInstance("ChaCha20-Poly1305"); + cc20p1305.init(Cipher.ENCRYPT_MODE, DEF_KEY, apsGood); + AlgorithmParameters pulledParams = cc20p1305.getParameters(); + byte[] apsGoodData = apsGood.getEncoded(); + byte[] pulledParamsData = pulledParams.getEncoded(); + if (!Arrays.equals(apsGoodData, pulledParamsData)) { + throw new RuntimeException( + "Retrieved parameters do not match those used to init cipher"); + } + System.out.println("Test Passed"); + + // Try the same test with ChaCha20. It should always be null. + System.out.println("*** Test: Init ChaCha20 Cipher using " + + "AP, retrieve AP and compare"); + Cipher cc20 = Cipher.getInstance("ChaCha20"); + cc20.init(Cipher.ENCRYPT_MODE, DEF_KEY); + pulledParams = cc20.getParameters(); + if (pulledParams != null) { + throw new RuntimeException("Unexpected non-null " + + "AlgorithmParameters from ChaCha20 cipiher"); + } + System.out.println("Test Passed"); + + // Create and try to init using invalid encoding + AlgorithmParameters apsBad = + AlgorithmParameters.getInstance("ChaCha20-Poly1305"); + System.out.println("*** Test: Use invalid encoding scheme"); + try { + apsBad.init(NONCE_OCTET_STR_12, "OraclePrivate"); + throw new RuntimeException("Allowed unsupported encoding scheme: " + + apsBad.getAlgorithm()); + } catch (IOException iae) { + System.out.println("Caught expected exception: " + iae); + } + + // Try to init using supported scheme but invalid length + System.out.println("*** Test: Use supported scheme, nonce too large"); + try { + apsBad.init(NONCE_OCTET_STR_16, "ASN.1"); + throw new RuntimeException("Allowed invalid encoded length"); + } catch (IOException ioe) { + System.out.println("Caught expected exception: " + ioe); + } + + System.out.println("----- AEAD Tests -----"); + for (TestData test : aeadTestList) { + System.out.println("*** Test " + ++testNumber + ": " + + test.testName); + if (runAEADTest(test)) { + testsPassed++; + } + } + System.out.println(); + + System.out.println("Total tests: " + testNumber + + ", Passed: " + testsPassed + ", Failed: " + + (testNumber - testsPassed)); + if (testsPassed != testNumber) { + throw new RuntimeException("One or more tests failed. " + + "Check output for details"); + } + } + + /** + * Attempt default inits with null AlgorithmParameters + * + * @param alg the algorithm (ChaCha20, ChaCha20-Poly1305) + * @param mode the Cipher mode (ENCRYPT_MODE, etc.) + */ + private static void testDefaultAlgParams(String alg, int mode, + boolean shouldPass) { + byte[] ivOne = null, ivTwo = null; + System.out.println("Test default AlgorithmParameters: Cipher = " + + alg + ", mode = " + mode); + try { + AlgorithmParameters params = null; + Cipher cipher = Cipher.getInstance(alg); + cipher.init(mode, DEF_KEY, params, null); + ivOne = cipher.getIV(); + cipher.init(mode, DEF_KEY, params, null); + ivTwo = cipher.getIV(); + if (!shouldPass) { + throw new RuntimeException( + "Did not receive expected exception"); + } + } catch (GeneralSecurityException gse) { + if (shouldPass) { + throw new RuntimeException(gse); + } + System.out.println("Caught expected exception: " + gse); + return; + } + if (Arrays.equals(ivOne, ivTwo)) { + throw new RuntimeException( + "FAIL! Two inits generated same nonces"); + } else { + System.out.println("IV 1:\n" + dumpHexBytes(ivOne, 16, "\n", " ")); + System.out.println("IV 1:\n" + dumpHexBytes(ivTwo, 16, "\n", " ")); + } + } + + /** + * Attempt default inits with null AlgorithmParameters + * + * @param alg the algorithm (ChaCha20, ChaCha20-Poly1305) + * @param mode the Cipher mode (ENCRYPT_MODE, etc.) + */ + private static void testDefaultAlgParamSpec(String alg, int mode, + boolean shouldPass) { + byte[] ivOne = null, ivTwo = null; + System.out.println("Test default AlgorithmParameterSpec: Cipher = " + + alg + ", mode = " + mode); + try { + AlgorithmParameterSpec params = null; + Cipher cipher = Cipher.getInstance(alg); + cipher.init(mode, DEF_KEY, params, null); + ivOne = cipher.getIV(); + cipher.init(mode, DEF_KEY, params, null); + ivTwo = cipher.getIV(); + if (!shouldPass) { + throw new RuntimeException( + "Did not receive expected exception"); + } + } catch (GeneralSecurityException gse) { + if (shouldPass) { + throw new RuntimeException(gse); + } + System.out.println("Caught expected exception: " + gse); + return; + } + if (Arrays.equals(ivOne, ivTwo)) { + throw new RuntimeException( + "FAIL! Two inits generated same nonces"); + } else { + System.out.println("IV 1:\n" + dumpHexBytes(ivOne, 16, "\n", " ")); + System.out.println("IV 2:\n" + dumpHexBytes(ivTwo, 16, "\n", " ")); + } + } + + private static boolean runAEADTest(TestData testData) + throws GeneralSecurityException, IOException { + boolean result = false; + + Cipher mambo = Cipher.getInstance("ChaCha20-Poly1305"); + SecretKeySpec mamboKey = new SecretKeySpec(testData.key, "ChaCha20"); + AlgorithmParameters mamboParams = + AlgorithmParameters.getInstance("ChaCha20-Poly1305"); + + // Put the nonce into ASN.1 ChaCha20-Poly1305 parameter format + byte[] derNonce = new byte[testData.nonce.length + 2]; + derNonce[0] = 0x04; + derNonce[1] = (byte)testData.nonce.length; + System.arraycopy(testData.nonce, 0, derNonce, 2, + testData.nonce.length); + mamboParams.init(derNonce); + + mambo.init(testData.direction, mamboKey, mamboParams); + + byte[] out = new byte[mambo.getOutputSize(testData.input.length)]; + int outOff = 0; + try { + mambo.updateAAD(testData.aad); + outOff += mambo.update(testData.input, 0, testData.input.length, + out, outOff); + outOff += mambo.doFinal(out, outOff); + } catch (AEADBadTagException abte) { + // If we get a bad tag or derive a tag mismatch, log it + // and register it as a failure + System.out.println("FAIL: " + abte); + return false; + } + + if (!Arrays.equals(out, testData.expOutput)) { + System.out.println("ERROR - Output Mismatch!"); + System.out.println("Expected:\n" + + dumpHexBytes(testData.expOutput, 16, "\n", " ")); + System.out.println("Actual:\n" + + dumpHexBytes(out, 16, "\n", " ")); + System.out.println(); + } else { + result = true; + } + + return result; + } + + /** + * Dump the hex bytes of a buffer into string form. + * + * @param data The array of bytes to dump to stdout. + * @param itemsPerLine The number of bytes to display per line + * if the {@code lineDelim} character is blank then all bytes + * will be printed on a single line. + * @param lineDelim The delimiter between lines + * @param itemDelim The delimiter between bytes + * + * @return The hexdump of the byte array + */ + private static String dumpHexBytes(byte[] data, int itemsPerLine, + String lineDelim, String itemDelim) { + return dumpHexBytes(ByteBuffer.wrap(data), itemsPerLine, lineDelim, + itemDelim); + } + + private static String dumpHexBytes(ByteBuffer data, int itemsPerLine, + String lineDelim, String itemDelim) { + StringBuilder sb = new StringBuilder(); + if (data != null) { + data.mark(); + int i = 0; + while (data.remaining() > 0) { + if (i % itemsPerLine == 0 && i != 0) { + sb.append(lineDelim); + } + sb.append(String.format("%02X", data.get())).append(itemDelim); + i++; + } + data.reset(); + } + + return sb.toString(); + } +} +