8153029: ChaCha20 Cipher Implementation
Summary: Add the ChaCha20 and ChaCha20-Poly1305 Cipher implementations
Reviewed-by: mullan
--- /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";
+ }
+ }
+}
--- /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
+ * <a href="https://tools.ietf.org/html/rfc8103"><i>RFC 8103</i></a>
+ * and is defined according to the following ASN.1:
+ *
+ * <pre>
+ * 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))
+ * </pre>
+ *
+ * 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 extends AlgorithmParameterSpec>
+ T engineGetParameterSpec(Class<T> 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();
+ }
+}
--- 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();
+ }
+ }
}
--- /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);
+ }
+}
--- 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
*/
--- 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.
* <pre>
* GCMParameterSpec s = ...;
* cipher.init(..., s);
@@ -131,6 +131,13 @@
* ...
*
* </pre>
+ * 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 <a href="https://tools.ietf.org/html/rfc7539">RFC 7539</a> for more
+ * information on the ChaCha20 and ChaCha20-Poly1305 algorithms.
+ * <p>
* Every implementation of the Java platform is required to support
* the following standard {@code Cipher} transformations with the keysizes
* in parentheses:
--- /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
+ * <a href="https://tools.ietf.org/html/rfc7539"><i>ChaCha20</i></a>
+ * algorithm.
+ *
+ * <p> The parameters consist of a 12-byte nonce and an initial
+ * counter value expressed as a 32-bit integer.
+ *
+ * <p> This class can be used to initialize a {@code Cipher} object that
+ * implements the <i>ChaCha20</i> 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;
+ }
+}
--- /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<TestData> testList = new LinkedList<TestData>() {{
+ 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<TestData> aeadTestList =
+ new LinkedList<TestData>() {{
+ 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();
+ }
+}
+
--- /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<TestData> testList = new LinkedList<TestData>() {{
+ 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<TestData> aeadTestList =
+ new LinkedList<TestData>() {{
+ 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<String> algList =
+ Arrays.asList(ALG_CC20, ALG_CC20_P1305);
+
+ public static final List<TestMethod> 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");
+ }
+ }
+}
+
--- /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<TestData> aeadTestList =
+ new LinkedList<TestData>() {{
+ 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();
+ }
+}
+