8153029: ChaCha20 Cipher Implementation
authorjnimeh
Thu, 31 May 2018 07:05:10 -0700
changeset 50323 25d711fca885
parent 50322 c75f3cdeb48c
child 50324 a29b954314d2
child 50325 462453f3c7f6
8153029: ChaCha20 Cipher Implementation Summary: Add the ChaCha20 and ChaCha20-Poly1305 Cipher implementations Reviewed-by: mullan
src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Cipher.java
src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Poly1305Parameters.java
src/java.base/share/classes/com/sun/crypto/provider/KeyGeneratorCore.java
src/java.base/share/classes/com/sun/crypto/provider/Poly1305.java
src/java.base/share/classes/com/sun/crypto/provider/SunJCE.java
src/java.base/share/classes/javax/crypto/Cipher.java
src/java.base/share/classes/javax/crypto/spec/ChaCha20ParameterSpec.java
test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20KAT.java
test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20NoReuse.java
test/jdk/com/sun/crypto/provider/Cipher/ChaCha20/ChaCha20Poly1305ParamTest.java
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Cipher.java	Thu May 31 07:05:10 2018 -0700
@@ -0,0 +1,1389 @@
+/*
+ * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package com.sun.crypto.provider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.security.*;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Arrays;
+import java.util.Objects;
+import javax.crypto.spec.ChaCha20ParameterSpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.crypto.*;
+import sun.security.util.DerValue;
+
+/**
+ * Implementation of the ChaCha20 cipher, as described in RFC 7539.
+ *
+ * @since 11
+ */
+abstract class ChaCha20Cipher extends CipherSpi {
+    // Mode constants
+    private static final int MODE_NONE = 0;
+    private static final int MODE_AEAD = 1;
+
+    // Constants used in setting up the initial state
+    private static final int STATE_CONST_0 = 0x61707865;
+    private static final int STATE_CONST_1 = 0x3320646e;
+    private static final int STATE_CONST_2 = 0x79622d32;
+    private static final int STATE_CONST_3 = 0x6b206574;
+
+    // The keystream block size in bytes and as integers
+    private static final int KEYSTREAM_SIZE = 64;
+    private static final int KS_SIZE_INTS = KEYSTREAM_SIZE / Integer.BYTES;
+    private static final int CIPHERBUF_BASE = 1024;
+
+    // The initialization state of the cipher
+    private boolean initialized;
+
+    // The mode of operation for this object
+    protected int mode;
+
+    // The direction (encrypt vs. decrypt) for the data flow
+    private int direction;
+
+    // Has all AAD data been provided (i.e. have we called our first update)
+    private boolean aadDone = false;
+
+    // The key's encoding in bytes for this object
+    private byte[] keyBytes;
+
+    // The nonce used for this object
+    private byte[] nonce;
+
+    // The counter
+    private static final long MAX_UINT32 = 0x00000000FFFFFFFFL;
+    private long finalCounterValue;
+    private long counter;
+
+    // Two arrays, both implemented as 16-element integer arrays:
+    // The base state, created at initialization time, and a working
+    // state which is a clone of the start state, and is then modified
+    // with the counter and the ChaCha20 block function.
+    private final int[] startState = new int[KS_SIZE_INTS];
+    private final byte[] keyStream = new byte[KEYSTREAM_SIZE];
+
+    // The offset into the current keystream
+    private int keyStrOffset;
+
+    // AEAD-related fields and constants
+    private static final int TAG_LENGTH = 16;
+    private long aadLen;
+    private long dataLen;
+
+    // Have a buffer of zero padding that can be read all or in part
+    // by the authenticator.
+    private static final byte[] padBuf = new byte[TAG_LENGTH];
+
+    // Create a buffer for holding the AAD and Ciphertext lengths
+    private final byte[] lenBuf = new byte[TAG_LENGTH];
+
+    // The authenticator (Poly1305) when running in AEAD mode
+    protected String authAlgName;
+    private Poly1305 authenticator;
+
+    // The underlying engine for doing the ChaCha20/Poly1305 work
+    private ChaChaEngine engine;
+
+    // Use this VarHandle for converting the state elements into little-endian
+    // integer values for the ChaCha20 block function.
+    private static final VarHandle asIntLittleEndian =
+            MethodHandles.byteArrayViewVarHandle(int[].class,
+                    ByteOrder.LITTLE_ENDIAN);
+
+    // Use this VarHandle for converting the AAD and data lengths into
+    // little-endian long values for AEAD tag computations.
+    private static final VarHandle asLongLittleEndian =
+            MethodHandles.byteArrayViewVarHandle(long[].class,
+                    ByteOrder.LITTLE_ENDIAN);
+
+    // Use this for pulling in 8 bytes at a time as longs for XOR operations
+    private static final VarHandle asLongView =
+            MethodHandles.byteArrayViewVarHandle(long[].class,
+                    ByteOrder.nativeOrder());
+
+    /**
+     * Default constructor.
+     */
+    protected ChaCha20Cipher() {
+    }
+
+    /**
+     * Set the mode of operation.  Since this is a stream cipher, there
+     * is no mode of operation in the block-cipher sense of things.  The
+     * protected {@code mode} field will only accept a value of {@code None}
+     * (case-insensitive).
+     *
+     * @param mode The mode value
+     *
+     * @throws NoSuchAlgorithmException if a mode of operation besides
+     *      {@code None} is provided.
+     */
+    @Override
+    protected void engineSetMode(String mode) throws NoSuchAlgorithmException {
+        if (mode.equalsIgnoreCase("None") == false) {
+            throw new NoSuchAlgorithmException("Mode must be None");
+        }
+    }
+
+    /**
+     * Set the padding scheme.  Padding schemes do not make sense with stream
+     * ciphers, but allow {@code NoPadding}.  See JCE spec.
+     *
+     * @param padding The padding type.  The only allowed value is
+     *      {@code NoPadding} case insensitive).
+     *
+     * @throws NoSuchPaddingException if a padding scheme besides
+     *      {@code NoPadding} is provided.
+     */
+    @Override
+    protected void engineSetPadding(String padding)
+            throws NoSuchPaddingException {
+        if (padding.equalsIgnoreCase("NoPadding") == false) {
+            throw new NoSuchPaddingException("Padding must be NoPadding");
+        }
+    }
+
+    /**
+     * Returns the block size.  For a stream cipher like ChaCha20, this
+     * value will always be zero.
+     *
+     * @return This method always returns 0.  See the JCE Specification.
+     */
+    @Override
+    protected int engineGetBlockSize() {
+        return 0;
+    }
+
+    /**
+     * Get the output size based on an input length.  In simple stream-cipher
+     * mode, the output size will equal the input size.  For ChaCha20-Poly1305
+     * for encryption the output size will be the sum of the input length
+     * and tag length.  For decryption, the output size will be the input
+     * length less the tag length or zero, whichever is larger.
+     *
+     * @param inputLen the length in bytes of the input
+     *
+     * @return the output length in bytes.
+     */
+    @Override
+    protected int engineGetOutputSize(int inputLen) {
+        int outLen = 0;
+
+        if (mode == MODE_NONE) {
+            outLen = inputLen;
+        } else if (mode == MODE_AEAD) {
+            outLen = (direction == Cipher.ENCRYPT_MODE) ?
+                    Math.addExact(inputLen, TAG_LENGTH) :
+                    Integer.max(inputLen - TAG_LENGTH, 0);
+        }
+
+        return outLen;
+    }
+
+    /**
+     * Get the nonce value used.
+     *
+     * @return the nonce bytes.  For ChaCha20 this will be a 12-byte value.
+     */
+    @Override
+    protected byte[] engineGetIV() {
+        return nonce.clone();
+    }
+
+    /**
+     * Get the algorithm parameters for this cipher.  For the ChaCha20
+     * cipher, this will always return {@code null} as there currently is
+     * no {@code AlgorithmParameters} implementation for ChaCha20.  For
+     * ChaCha20-Poly1305, a {@code ChaCha20Poly1305Parameters} object will be
+     * created and initialized with the configured nonce value and returned
+     * to the caller.
+     *
+     * @return a {@code null} value if the ChaCha20 cipher is used (mode is
+     * MODE_NONE), or a {@code ChaCha20Poly1305Parameters} object containing
+     * the nonce if the mode is MODE_AEAD.
+     */
+    @Override
+    protected AlgorithmParameters engineGetParameters() {
+        AlgorithmParameters params = null;
+        if (mode == MODE_AEAD) {
+            try {
+                // Force the 12-byte nonce into a DER-encoded OCTET_STRING
+                byte[] derNonce = new byte[nonce.length + 2];
+                derNonce[0] = 0x04;                 // OCTET_STRING tag
+                derNonce[1] = (byte)nonce.length;   // 12-byte length;
+                System.arraycopy(nonce, 0, derNonce, 2, nonce.length);
+                params = AlgorithmParameters.getInstance("ChaCha20-Poly1305");
+                params.init(derNonce);
+            } catch (NoSuchAlgorithmException | IOException exc) {
+                throw new RuntimeException(exc);
+            }
+        }
+
+        return params;
+    }
+
+    /**
+     * Initialize the engine using a key and secure random implementation.  If
+     * a SecureRandom object is provided it will be used to create a random
+     * nonce value.  If the {@code random} parameter is null an internal
+     * secure random source will be used to create the random nonce.
+     * The counter value will be set to 1.
+     *
+     * @param opmode the type of operation to do.  This value may not be
+     *      {@code Cipher.DECRYPT_MODE} or {@code Cipher.UNWRAP_MODE} mode
+     *      because it must generate random parameters like the nonce.
+     * @param key a 256-bit key suitable for ChaCha20
+     * @param random a {@code SecureRandom} implementation used to create the
+     *      random nonce.  If {@code null} is used for the random object,
+     *      then an internal secure random source will be used to create the
+     *      nonce.
+     *
+     * @throws UnsupportedOperationException if the mode of operation
+     *      is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE}
+     *      (currently unsupported).
+     * @throws InvalidKeyException if the key is of the wrong type or is
+     *      not 256-bits in length.  This will also be thrown if the opmode
+     *      parameter is {@code Cipher.DECRYPT_MODE}.
+     *      {@code Cipher.UNWRAP_MODE} would normally be disallowed in this
+     *      context but it is preempted by the UOE case above.
+     */
+    @Override
+    protected void engineInit(int opmode, Key key, SecureRandom random)
+            throws InvalidKeyException {
+        if (opmode != Cipher.DECRYPT_MODE) {
+            byte[] newNonce = createRandomNonce(random);
+            counter = 1;
+            init(opmode, key, newNonce);
+        } else {
+            throw new InvalidKeyException("Default parameter generation " +
+                "disallowed in DECRYPT and UNWRAP modes");
+        }
+    }
+
+    /**
+     * Initialize the engine using a key and secure random implementation.
+     *
+     * @param opmode the type of operation to do.  This value must be either
+     *      {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE}
+     * @param key a 256-bit key suitable for ChaCha20
+     * @param params a {@code ChaCha20ParameterSpec} that will provide
+     *      the nonce and initial block counter value.
+     * @param random a {@code SecureRandom} implementation, this parameter
+     *      is not used in this form of the initializer.
+     *
+     * @throws UnsupportedOperationException if the mode of operation
+     *      is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE}
+     *      (currently unsupported).
+     * @throws InvalidKeyException if the key is of the wrong type or is
+     *      not 256-bits in length.  This will also be thrown if the opmode
+     *      parameter is not {@code Cipher.ENCRYPT_MODE} or
+     *      {@code Cipher.DECRYPT_MODE} (excepting the UOE case above).
+     * @throws InvalidAlgorithmParameterException if {@code params} is
+     *      not a {@code ChaCha20ParameterSpec}
+     * @throws NullPointerException if {@code params} is {@code null}
+     */
+    @Override
+    protected void engineInit(int opmode, Key key,
+            AlgorithmParameterSpec params, SecureRandom random)
+            throws InvalidKeyException, InvalidAlgorithmParameterException {
+
+        // If AlgorithmParameterSpec is null, then treat this like an init
+        // of the form (int, Key, SecureRandom)
+        if (params == null) {
+            engineInit(opmode, key, random);
+            return;
+        }
+
+        // We will ignore the secure random implementation and use the nonce
+        // from the AlgorithmParameterSpec instead.
+        byte[] newNonce = null;
+        switch (mode) {
+            case MODE_NONE:
+                if (!(params instanceof ChaCha20ParameterSpec)) {
+                    throw new InvalidAlgorithmParameterException(
+                        "ChaCha20 algorithm requires ChaCha20ParameterSpec");
+                }
+                ChaCha20ParameterSpec chaParams = (ChaCha20ParameterSpec)params;
+                newNonce = chaParams.getNonce();
+                counter = ((long)chaParams.getCounter()) & 0x00000000FFFFFFFFL;
+                break;
+            case MODE_AEAD:
+                if (!(params instanceof IvParameterSpec)) {
+                    throw new InvalidAlgorithmParameterException(
+                        "ChaCha20-Poly1305 requires IvParameterSpec");
+                }
+                IvParameterSpec ivParams = (IvParameterSpec)params;
+                newNonce = ivParams.getIV();
+                if (newNonce.length != 12) {
+                    throw new InvalidAlgorithmParameterException(
+                        "ChaCha20-Poly1305 nonce must be 12 bytes in length");
+                }
+                break;
+            default:
+                // Should never happen
+                throw new RuntimeException("ChaCha20 in unsupported mode");
+        }
+        init(opmode, key, newNonce);
+    }
+
+    /**
+     * Initialize the engine using the {@code AlgorithmParameter} initialization
+     * format.  This cipher does supports initialization with
+     * {@code AlgorithmParameter} objects for ChaCha20-Poly1305 but not for
+     * ChaCha20 as a simple stream cipher.  In the latter case, it will throw
+     * an {@code InvalidAlgorithmParameterException} if the value is non-null.
+     * If a null value is supplied for the {@code params} field
+     * the cipher will be initialized with the counter value set to 1 and
+     * a random nonce.  If {@code null} is used for the random object,
+     * then an internal secure random source will be used to create the
+     * nonce.
+     *
+     * @param opmode the type of operation to do.  This value must be either
+     *      {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE}
+     * @param key a 256-bit key suitable for ChaCha20
+     * @param params a {@code null} value if the algorithm is ChaCha20, or
+     *      the appropriate {@code AlgorithmParameters} object containing the
+     *      nonce information if the algorithm is ChaCha20-Poly1305.
+     * @param random a {@code SecureRandom} implementation, may be {@code null}.
+     *
+     * @throws UnsupportedOperationException if the mode of operation
+     *      is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE}
+     *      (currently unsupported).
+     * @throws InvalidKeyException if the key is of the wrong type or is
+     *      not 256-bits in length.  This will also be thrown if the opmode
+     *      parameter is not {@code Cipher.ENCRYPT_MODE} or
+     *      {@code Cipher.DECRYPT_MODE} (excepting the UOE case above).
+     * @throws InvalidAlgorithmParameterException if {@code params} is
+     *      non-null and the algorithm is ChaCha20.  This exception will be
+     *      also thrown if the algorithm is ChaCha20-Poly1305 and an incorrect
+     *      {@code AlgorithmParameters} object is supplied.
+     */
+    @Override
+    protected void engineInit(int opmode, Key key,
+            AlgorithmParameters params, SecureRandom random)
+            throws InvalidKeyException, InvalidAlgorithmParameterException {
+
+        // If AlgorithmParameters is null, then treat this like an init
+        // of the form (int, Key, SecureRandom)
+        if (params == null) {
+            engineInit(opmode, key, random);
+            return;
+        }
+
+        byte[] newNonce = null;
+        switch (mode) {
+            case MODE_NONE:
+                throw new InvalidAlgorithmParameterException(
+                        "AlgorithmParameters not supported");
+            case MODE_AEAD:
+                String paramAlg = params.getAlgorithm();
+                if (!paramAlg.equalsIgnoreCase("ChaCha20-Poly1305")) {
+                    throw new InvalidAlgorithmParameterException(
+                            "Invalid parameter type: " + paramAlg);
+                }
+                try {
+                    DerValue dv = new DerValue(params.getEncoded());
+                    newNonce = dv.getOctetString();
+                    if (newNonce.length != 12) {
+                        throw new InvalidAlgorithmParameterException(
+                                "ChaCha20-Poly1305 nonce must be " +
+                                "12 bytes in length");
+                    }
+                } catch (IOException ioe) {
+                    throw new InvalidAlgorithmParameterException(ioe);
+                }
+                break;
+            default:
+                throw new RuntimeException("Invalid mode: " + mode);
+        }
+
+        // If after all the above processing we still don't have a nonce value
+        // then supply a random one provided a random source has been given.
+        if (newNonce == null) {
+            newNonce = createRandomNonce(random);
+        }
+
+        // Continue with initialization
+        init(opmode, key, newNonce);
+    }
+
+    /**
+     * Update additional authenticated data (AAD).
+     *
+     * @param src the byte array containing the authentication data.
+     * @param offset the starting offset in the buffer to update.
+     * @param len the amount of authentication data to update.
+     *
+     * @throws IllegalStateException if the cipher has not been initialized,
+     *      {@code engineUpdate} has been called, or the cipher is running
+     *      in a non-AEAD mode of operation.  It will also throw this
+     *      exception if the submitted AAD would overflow a 64-bit length
+     *      counter.
+     */
+    @Override
+    protected void engineUpdateAAD(byte[] src, int offset, int len) {
+        if (!initialized) {
+            // We know that the cipher has not been initialized if the key
+            // is still null.
+            throw new IllegalStateException(
+                    "Attempted to update AAD on uninitialized Cipher");
+        } else if (aadDone) {
+            // No AAD updates allowed after the PT/CT update method is called
+            throw new IllegalStateException("Attempted to update AAD on " +
+                    "Cipher after plaintext/ciphertext update");
+        } else if (mode != MODE_AEAD) {
+            throw new IllegalStateException(
+                    "Cipher is running in non-AEAD mode");
+        } else {
+            try {
+                aadLen = Math.addExact(aadLen, len);
+                authUpdate(src, offset, len);
+            } catch (ArithmeticException ae) {
+                throw new IllegalStateException("AAD overflow", ae);
+            }
+        }
+    }
+
+    /**
+     * Update additional authenticated data (AAD).
+     *
+     * @param src the ByteBuffer containing the authentication data.
+     *
+     * @throws IllegalStateException if the cipher has not been initialized,
+     *      {@code engineUpdate} has been called, or the cipher is running
+     *      in a non-AEAD mode of operation.  It will also throw this
+     *      exception if the submitted AAD would overflow a 64-bit length
+     *      counter.
+     */
+    @Override
+    protected void engineUpdateAAD(ByteBuffer src) {
+        if (!initialized) {
+            // We know that the cipher has not been initialized if the key
+            // is still null.
+            throw new IllegalStateException(
+                    "Attempted to update AAD on uninitialized Cipher");
+        } else if (aadDone) {
+            // No AAD updates allowed after the PT/CT update method  is called
+            throw new IllegalStateException("Attempted to update AAD on " +
+                    "Cipher after plaintext/ciphertext update");
+        } else if (mode != MODE_AEAD) {
+            throw new IllegalStateException(
+                    "Cipher is running in non-AEAD mode");
+        } else {
+            try {
+                aadLen = Math.addExact(aadLen, (src.limit() - src.position()));
+                authenticator.engineUpdate(src);
+            } catch (ArithmeticException ae) {
+                throw new IllegalStateException("AAD overflow", ae);
+            }
+        }
+    }
+
+    /**
+     * Create a random 12-byte nonce.
+     *
+     * @param random a {@code SecureRandom} object.  If {@code null} is
+     * provided a new {@code SecureRandom} object will be instantiated.
+     *
+     * @return a 12-byte array containing the random nonce.
+     */
+    private byte[] createRandomNonce(SecureRandom random) {
+        byte[] newNonce = new byte[12];
+        SecureRandom rand = (random != null) ? random : new SecureRandom();
+        rand.nextBytes(newNonce);
+        return newNonce;
+    }
+
+    /**
+     * Perform additional initialization actions based on the key and operation
+     * type.
+     *
+     * @param opmode the type of operation to do.  This value must be either
+     *      {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE}
+     * @param key a 256-bit key suitable for ChaCha20
+     * @param newNonce the new nonce value for this initialization.
+     *
+     * @throws UnsupportedOperationException if the {@code opmode} parameter
+     *      is {@code Cipher.WRAP_MODE} or {@code Cipher.UNWRAP_MODE}
+     *      (currently unsupported).
+     * @throws InvalidKeyException if the {@code opmode} parameter is not
+     *      {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE}, or
+     *      if the key format is not {@code RAW}.
+     */
+    private void init(int opmode, Key key, byte[] newNonce)
+            throws InvalidKeyException {
+        if ((opmode == Cipher.WRAP_MODE) || (opmode == Cipher.UNWRAP_MODE)) {
+            throw new UnsupportedOperationException(
+                    "WRAP_MODE and UNWRAP_MODE are not currently supported");
+        } else if ((opmode != Cipher.ENCRYPT_MODE) &&
+                (opmode != Cipher.DECRYPT_MODE)) {
+            throw new InvalidKeyException("Unknown opmode: " + opmode);
+        }
+
+        // Make sure that the provided key and nonce are unique before
+        // assigning them to the object.
+        byte[] newKeyBytes = getEncodedKey(key);
+        checkKeyAndNonce(newKeyBytes, newNonce);
+        this.keyBytes = newKeyBytes;
+        nonce = newNonce;
+
+        // Now that we have the key and nonce, we can build the initial state
+        setInitialState();
+
+        if (mode == MODE_NONE) {
+            engine = new EngineStreamOnly();
+        } else if (mode == MODE_AEAD) {
+            if (opmode == Cipher.ENCRYPT_MODE) {
+                engine = new EngineAEADEnc();
+            } else if (opmode == Cipher.DECRYPT_MODE) {
+                engine = new EngineAEADDec();
+            } else {
+                throw new InvalidKeyException("Not encrypt or decrypt mode");
+            }
+        }
+
+        // We can also get one block's worth of keystream created
+        finalCounterValue = counter + MAX_UINT32;
+        generateKeystream();
+        direction = opmode;
+        aadDone = false;
+        this.keyStrOffset = 0;
+        initialized = true;
+    }
+
+    /**
+     * Check the key and nonce bytes to make sure that they do not repeat
+     * across reinitialization.
+     *
+     * @param newKeyBytes the byte encoding for the newly provided key
+     * @param newNonce the new nonce to be used with this initialization
+     *
+     * @throws InvalidKeyException if both the key and nonce match the
+     *      previous initialization.
+     *
+     */
+    private void checkKeyAndNonce(byte[] newKeyBytes, byte[] newNonce)
+            throws InvalidKeyException {
+        // A new initialization must have either a different key or nonce
+        // so the starting state for each block is not the same as the
+        // previous initialization.
+        if (MessageDigest.isEqual(newKeyBytes, keyBytes) &&
+                MessageDigest.isEqual(newNonce, nonce)) {
+            throw new InvalidKeyException(
+                    "Matching key and nonce from previous initialization");
+        }
+    }
+
+    /**
+     * Return the encoded key as a byte array
+     *
+     * @param key the {@code Key} object used for this {@code Cipher}
+     *
+     * @return the key bytes
+     *
+     * @throws InvalidKeyException if the key is of the wrong type or length,
+     *      or if the key encoding format is not {@code RAW}.
+     */
+    private static byte[] getEncodedKey(Key key) throws InvalidKeyException {
+        if ("RAW".equals(key.getFormat()) == false) {
+            throw new InvalidKeyException("Key encoding format must be RAW");
+        }
+        byte[] encodedKey = key.getEncoded();
+        if (encodedKey == null || encodedKey.length != 32) {
+            throw new InvalidKeyException("Key length must be 256 bits");
+        }
+        return encodedKey;
+    }
+
+    /**
+     * Update the currently running operation with additional data
+     *
+     * @param in the plaintext or ciphertext input bytes (depending on the
+     *      operation type).
+     * @param inOfs the offset into the input array
+     * @param inLen the length of the data to use for the update operation.
+     *
+     * @return the resulting plaintext or ciphertext bytes (depending on
+     *      the operation type)
+     */
+    @Override
+    protected byte[] engineUpdate(byte[] in, int inOfs, int inLen) {
+        byte[] out = new byte[inLen];
+        try {
+            engine.doUpdate(in, inOfs, inLen, out, 0);
+        } catch (ShortBufferException | KeyException exc) {
+            throw new RuntimeException(exc);
+        }
+
+        return out;
+    }
+
+    /**
+     * Update the currently running operation with additional data
+     *
+     * @param in the plaintext or ciphertext input bytes (depending on the
+     *      operation type).
+     * @param inOfs the offset into the input array
+     * @param inLen the length of the data to use for the update operation.
+     * @param out the byte array that will hold the resulting data.  The array
+     *      must be large enough to hold the resulting data.
+     * @param outOfs the offset for the {@code out} buffer to begin writing
+     *      the resulting data.
+     *
+     * @return the length in bytes of the data written into the {@code out}
+     *      buffer.
+     *
+     * @throws ShortBufferException if the buffer {@code out} does not have
+     *      enough space to hold the resulting data.
+     */
+    @Override
+    protected int engineUpdate(byte[] in, int inOfs, int inLen,
+            byte[] out, int outOfs) throws ShortBufferException {
+        int bytesUpdated = 0;
+        try {
+            bytesUpdated = engine.doUpdate(in, inOfs, inLen, out, outOfs);
+        } catch (KeyException ke) {
+            throw new RuntimeException(ke);
+        }
+        return bytesUpdated;
+    }
+
+    /**
+     * Complete the currently running operation using any final
+     * data provided by the caller.
+     *
+     * @param in the plaintext or ciphertext input bytes (depending on the
+     *      operation type).
+     * @param inOfs the offset into the input array
+     * @param inLen the length of the data to use for the update operation.
+     *
+     * @return the resulting plaintext or ciphertext bytes (depending on
+     *      the operation type)
+     *
+     * @throws AEADBadTagException if, during decryption, the provided tag
+     *      does not match the calculated tag.
+     */
+    @Override
+    protected byte[] engineDoFinal(byte[] in, int inOfs, int inLen)
+            throws AEADBadTagException {
+        byte[] output = new byte[engineGetOutputSize(inLen)];
+        try {
+            engine.doFinal(in, inOfs, inLen, output, 0);
+        } catch (ShortBufferException | KeyException exc) {
+            throw new RuntimeException(exc);
+        } finally {
+            // Regardless of what happens, the cipher cannot be used for
+            // further processing until it has been freshly initialized.
+            initialized = false;
+        }
+        return output;
+    }
+
+    /**
+     * Complete the currently running operation using any final
+     * data provided by the caller.
+     *
+     * @param in the plaintext or ciphertext input bytes (depending on the
+     *      operation type).
+     * @param inOfs the offset into the input array
+     * @param inLen the length of the data to use for the update operation.
+     * @param out the byte array that will hold the resulting data.  The array
+     *      must be large enough to hold the resulting data.
+     * @param outOfs the offset for the {@code out} buffer to begin writing
+     *      the resulting data.
+     *
+     * @return the length in bytes of the data written into the {@code out}
+     *      buffer.
+     *
+     * @throws ShortBufferException if the buffer {@code out} does not have
+     *      enough space to hold the resulting data.
+     * @throws AEADBadTagException if, during decryption, the provided tag
+     *      does not match the calculated tag.
+     */
+    @Override
+    protected int engineDoFinal(byte[] in, int inOfs, int inLen, byte[] out,
+            int outOfs) throws ShortBufferException, AEADBadTagException {
+
+        int bytesUpdated = 0;
+        try {
+            bytesUpdated = engine.doFinal(in, inOfs, inLen, out, outOfs);
+        } catch (KeyException ke) {
+            throw new RuntimeException(ke);
+        } finally {
+            // Regardless of what happens, the cipher cannot be used for
+            // further processing until it has been freshly initialized.
+            initialized = false;
+        }
+        return bytesUpdated;
+    }
+
+    /**
+     * Wrap a {@code Key} using this Cipher's current encryption parameters.
+     *
+     * @param key the key to wrap.  The data that will be encrypted will
+     *      be the provided {@code Key} in its encoded form.
+     *
+     * @return a byte array consisting of the wrapped key.
+     *
+     * @throws UnsupportedOperationException this will (currently) always
+     *      be thrown, as this method is not currently supported.
+     */
+    @Override
+    protected byte[] engineWrap(Key key) throws IllegalBlockSizeException,
+            InvalidKeyException {
+        throw new UnsupportedOperationException(
+                "Wrap operations are not supported");
+    }
+
+    /**
+     * Unwrap a {@code Key} using this Cipher's current encryption parameters.
+     *
+     * @param wrappedKey the key to unwrap.
+     * @param algorithm the algorithm associated with the wrapped key
+     * @param type the type of the wrapped key. This is one of
+     *      {@code SECRET_KEY}, {@code PRIVATE_KEY}, or {@code PUBLIC_KEY}.
+     *
+     * @return the unwrapped key as a {@code Key} object.
+     *
+     * @throws UnsupportedOperationException this will (currently) always
+     *      be thrown, as this method is not currently supported.
+     */
+    @Override
+    protected Key engineUnwrap(byte[] wrappedKey, String algorithm,
+            int type) throws InvalidKeyException, NoSuchAlgorithmException {
+        throw new UnsupportedOperationException(
+                "Unwrap operations are not supported");
+    }
+
+    /**
+     * Get the length of a provided key in bits.
+     *
+     * @param key the key to be evaluated
+     *
+     * @return the length of the key in bits
+     *
+     * @throws InvalidKeyException if the key is invalid or does not
+     *      have an encoded form.
+     */
+    @Override
+    protected int engineGetKeySize(Key key) throws InvalidKeyException {
+        byte[] encodedKey = getEncodedKey(key);
+        return encodedKey.length << 3;
+    }
+
+    /**
+     * Set the initial state.  This will populate the state array and put the
+     * key and nonce into their proper locations.  The counter field is not
+     * set here.
+     *
+     * @throws IllegalArgumentException if the key or nonce are not in
+     *      their proper lengths (32 bytes for the key, 12 bytes for the
+     *      nonce).
+     * @throws InvalidKeyException if the key does not support an encoded form.
+     */
+    private void setInitialState() throws InvalidKeyException {
+        // Apply constants to first 4 words
+        startState[0] = STATE_CONST_0;
+        startState[1] = STATE_CONST_1;
+        startState[2] = STATE_CONST_2;
+        startState[3] = STATE_CONST_3;
+
+        // Apply the key bytes as 8 32-bit little endian ints (4 through 11)
+        for (int i = 0; i < 32; i += 4) {
+            startState[(i / 4) + 4] = (keyBytes[i] & 0x000000FF) |
+                ((keyBytes[i + 1] << 8) & 0x0000FF00) |
+                ((keyBytes[i + 2] << 16) & 0x00FF0000) |
+                ((keyBytes[i + 3] << 24) & 0xFF000000);
+        }
+
+        startState[12] = 0;
+
+        // The final integers for the state are from the nonce
+        // interpreted as 3 little endian integers
+        for (int i = 0; i < 12; i += 4) {
+            startState[(i / 4) + 13] = (nonce[i] & 0x000000FF) |
+                ((nonce[i + 1] << 8) & 0x0000FF00) |
+                ((nonce[i + 2] << 16) & 0x00FF0000) |
+                ((nonce[i + 3] << 24) & 0xFF000000);
+        }
+    }
+
+    /**
+     * Using the current state and counter create the next set of keystream
+     * bytes.  This method will generate the next 512 bits of keystream and
+     * return it in the {@code keyStream} parameter.  Following the
+     * block function the counter will be incremented.
+     */
+    private void generateKeystream() {
+        chaCha20Block(startState, counter, keyStream);
+        counter++;
+    }
+
+    /**
+     * Perform a full 20-round ChaCha20 transform on the initial state.
+     *
+     * @param initState the starting state, not including the counter
+     *      value.
+     * @param counter the counter value to apply
+     * @param result  the array that will hold the result of the ChaCha20
+     *      block function.
+     *
+     * @note it is the caller's responsibility to ensure that the workState
+     * is sized the same as the initState, no checking is performed internally.
+     */
+    private static void chaCha20Block(int[] initState, long counter,
+                                      byte[] result) {
+        // Create an initial state and clone a working copy
+        int ws00 = STATE_CONST_0;
+        int ws01 = STATE_CONST_1;
+        int ws02 = STATE_CONST_2;
+        int ws03 = STATE_CONST_3;
+        int ws04 = initState[4];
+        int ws05 = initState[5];
+        int ws06 = initState[6];
+        int ws07 = initState[7];
+        int ws08 = initState[8];
+        int ws09 = initState[9];
+        int ws10 = initState[10];
+        int ws11 = initState[11];
+        int ws12 = (int)counter;
+        int ws13 = initState[13];
+        int ws14 = initState[14];
+        int ws15 = initState[15];
+
+        // Peform 10 iterations of the 8 quarter round set
+        for (int round = 0; round < 10; round++) {
+            ws00 += ws04;
+            ws12 = Integer.rotateLeft(ws12 ^ ws00, 16);
+
+            ws08 += ws12;
+            ws04 = Integer.rotateLeft(ws04 ^ ws08, 12);
+
+            ws00 += ws04;
+            ws12 = Integer.rotateLeft(ws12 ^ ws00, 8);
+
+            ws08 += ws12;
+            ws04 = Integer.rotateLeft(ws04 ^ ws08, 7);
+
+            ws01 += ws05;
+            ws13 = Integer.rotateLeft(ws13 ^ ws01, 16);
+
+            ws09 += ws13;
+            ws05 = Integer.rotateLeft(ws05 ^ ws09, 12);
+
+            ws01 += ws05;
+            ws13 = Integer.rotateLeft(ws13 ^ ws01, 8);
+
+            ws09 += ws13;
+            ws05 = Integer.rotateLeft(ws05 ^ ws09, 7);
+
+            ws02 += ws06;
+            ws14 = Integer.rotateLeft(ws14 ^ ws02, 16);
+
+            ws10 += ws14;
+            ws06 = Integer.rotateLeft(ws06 ^ ws10, 12);
+
+            ws02 += ws06;
+            ws14 = Integer.rotateLeft(ws14 ^ ws02, 8);
+
+            ws10 += ws14;
+            ws06 = Integer.rotateLeft(ws06 ^ ws10, 7);
+
+            ws03 += ws07;
+            ws15 = Integer.rotateLeft(ws15 ^ ws03, 16);
+
+            ws11 += ws15;
+            ws07 = Integer.rotateLeft(ws07 ^ ws11, 12);
+
+            ws03 += ws07;
+            ws15 = Integer.rotateLeft(ws15 ^ ws03, 8);
+
+            ws11 += ws15;
+            ws07 = Integer.rotateLeft(ws07 ^ ws11, 7);
+
+            ws00 += ws05;
+            ws15 = Integer.rotateLeft(ws15 ^ ws00, 16);
+
+            ws10 += ws15;
+            ws05 = Integer.rotateLeft(ws05 ^ ws10, 12);
+
+            ws00 += ws05;
+            ws15 = Integer.rotateLeft(ws15 ^ ws00, 8);
+
+            ws10 += ws15;
+            ws05 = Integer.rotateLeft(ws05 ^ ws10, 7);
+
+            ws01 += ws06;
+            ws12 = Integer.rotateLeft(ws12 ^ ws01, 16);
+
+            ws11 += ws12;
+            ws06 = Integer.rotateLeft(ws06 ^ ws11, 12);
+
+            ws01 += ws06;
+            ws12 = Integer.rotateLeft(ws12 ^ ws01, 8);
+
+            ws11 += ws12;
+            ws06 = Integer.rotateLeft(ws06 ^ ws11, 7);
+
+            ws02 += ws07;
+            ws13 = Integer.rotateLeft(ws13 ^ ws02, 16);
+
+            ws08 += ws13;
+            ws07 = Integer.rotateLeft(ws07 ^ ws08, 12);
+
+            ws02 += ws07;
+            ws13 = Integer.rotateLeft(ws13 ^ ws02, 8);
+
+            ws08 += ws13;
+            ws07 = Integer.rotateLeft(ws07 ^ ws08, 7);
+
+            ws03 += ws04;
+            ws14 = Integer.rotateLeft(ws14 ^ ws03, 16);
+
+            ws09 += ws14;
+            ws04 = Integer.rotateLeft(ws04 ^ ws09, 12);
+
+            ws03 += ws04;
+            ws14 = Integer.rotateLeft(ws14 ^ ws03, 8);
+
+            ws09 += ws14;
+            ws04 = Integer.rotateLeft(ws04 ^ ws09, 7);
+        }
+
+        // Add the end working state back into the original state
+        asIntLittleEndian.set(result, 0, ws00 + STATE_CONST_0);
+        asIntLittleEndian.set(result, 4, ws01 + STATE_CONST_1);
+        asIntLittleEndian.set(result, 8, ws02 + STATE_CONST_2);
+        asIntLittleEndian.set(result, 12, ws03 + STATE_CONST_3);
+        asIntLittleEndian.set(result, 16, ws04 + initState[4]);
+        asIntLittleEndian.set(result, 20, ws05 + initState[5]);
+        asIntLittleEndian.set(result, 24, ws06 + initState[6]);
+        asIntLittleEndian.set(result, 28, ws07 + initState[7]);
+        asIntLittleEndian.set(result, 32, ws08 + initState[8]);
+        asIntLittleEndian.set(result, 36, ws09 + initState[9]);
+        asIntLittleEndian.set(result, 40, ws10 + initState[10]);
+        asIntLittleEndian.set(result, 44, ws11 + initState[11]);
+        // Add the counter back into workState[12]
+        asIntLittleEndian.set(result, 48, ws12 + (int)counter);
+        asIntLittleEndian.set(result, 52, ws13 + initState[13]);
+        asIntLittleEndian.set(result, 56, ws14 + initState[14]);
+        asIntLittleEndian.set(result, 60, ws15 + initState[15]);
+    }
+
+    /**
+     * Perform the ChaCha20 transform.
+     *
+     * @param in the array of bytes for the input
+     * @param inOff the offset into the input array to start the transform
+     * @param inLen the length of the data to perform the transform on.
+     * @param out the output array.  It must be large enough to hold the
+     *      resulting data
+     * @param outOff the offset into the output array to place the resulting
+     *      data.
+     */
+    private void chaCha20Transform(byte[] in, int inOff, int inLen,
+            byte[] out, int outOff) throws KeyException {
+        int remainingData = inLen;
+
+        while (remainingData > 0) {
+            int ksRemain = keyStream.length - keyStrOffset;
+            if (ksRemain <= 0) {
+                if (counter <= finalCounterValue) {
+                    generateKeystream();
+                    keyStrOffset = 0;
+                    ksRemain = keyStream.length;
+                } else {
+                    throw new KeyException("Counter exhausted.  " +
+                            "Reinitialize with new key and/or nonce");
+                }
+            }
+
+            // XOR each byte in the keystream against the input
+            int xformLen = Math.min(remainingData, ksRemain);
+            xor(keyStream, keyStrOffset, in, inOff, out, outOff, xformLen);
+            outOff += xformLen;
+            inOff += xformLen;
+            keyStrOffset += xformLen;
+            remainingData -= xformLen;
+        }
+    }
+
+    private static void xor(byte[] in1, int off1, byte[] in2, int off2,
+            byte[] out, int outOff, int len) {
+        while (len >= 8) {
+            long v1 = (long) asLongView.get(in1, off1);
+            long v2 = (long) asLongView.get(in2, off2);
+            asLongView.set(out, outOff, v1 ^ v2);
+            off1 += 8;
+            off2 += 8;
+            outOff += 8;
+            len -= 8;
+        }
+        while (len > 0) {
+            out[outOff] = (byte) (in1[off1] ^ in2[off2]);
+            off1++;
+            off2++;
+            outOff++;
+            len--;
+        }
+    }
+
+    /**
+     * Perform initialization steps for the authenticator
+     *
+     * @throws InvalidKeyException if the key is unusable for some reason
+     *      (invalid length, etc.)
+     */
+    private void initAuthenticator() throws InvalidKeyException {
+        authenticator = new Poly1305();
+
+        // Derive the Poly1305 key from the starting state
+        byte[] serializedKey = new byte[KEYSTREAM_SIZE];
+        chaCha20Block(startState, 0, serializedKey);
+
+        authenticator.engineInit(new SecretKeySpec(serializedKey, 0, 32,
+                authAlgName), null);
+        aadLen = 0;
+        dataLen = 0;
+    }
+
+    /**
+     * Update the authenticator state with data.  This routine can be used
+     * to add data to the authenticator, whether AAD or application data.
+     *
+     * @param data the data to stir into the authenticator.
+     * @param offset the offset into the data.
+     * @param length the length of data to add to the authenticator.
+     *
+     * @return the number of bytes processed by this method.
+     */
+    private int authUpdate(byte[] data, int offset, int length) {
+        Objects.checkFromIndexSize(offset, length, data.length);
+        authenticator.engineUpdate(data, offset, length);
+        return length;
+    }
+
+    /**
+     * Finalize the data and return the tag.
+     *
+     * @param data an array containing any remaining data to process.
+     * @param dataOff the offset into the data.
+     * @param length the length of the data to process.
+     * @param out the array to write the resulting tag into
+     * @param outOff the offset to begin writing the data.
+     *
+     * @throws ShortBufferException if there is insufficient room to
+     *      write the tag.
+     */
+    private void authFinalizeData(byte[] data, int dataOff, int length,
+            byte[] out, int outOff) throws ShortBufferException {
+        // Update with the final chunk of ciphertext, then pad to a
+        // multiple of 16.
+        if (data != null) {
+            dataLen += authUpdate(data, dataOff, length);
+        }
+        authPad16(dataLen);
+
+        // Also write the AAD and ciphertext data lengths as little-endian
+        // 64-bit values.
+        authWriteLengths(aadLen, dataLen, lenBuf);
+        authenticator.engineUpdate(lenBuf, 0, lenBuf.length);
+        byte[] tag = authenticator.engineDoFinal();
+        Objects.checkFromIndexSize(outOff, tag.length, out.length);
+        System.arraycopy(tag, 0, out, outOff, tag.length);
+        aadLen = 0;
+        dataLen = 0;
+    }
+
+    /**
+     * Based on a given length of data, make the authenticator process
+     * zero bytes that will pad the length out to a multiple of 16.
+     *
+     * @param dataLen the starting length to be padded.
+     */
+    private void authPad16(long dataLen) {
+        // Pad out the AAD or data to a multiple of 16 bytes
+        authenticator.engineUpdate(padBuf, 0,
+                (TAG_LENGTH - ((int)dataLen & 15)) & 15);
+    }
+
+    /**
+     * Write the two 64-bit little-endian length fields into an array
+     * for processing by the poly1305 authenticator.
+     *
+     * @param aLen the length of the AAD.
+     * @param dLen the length of the application data.
+     * @param buf the buffer to write the two lengths into.
+     *
+     * @note it is the caller's responsibility to provide an array large
+     *      enough to hold the two longs.
+     */
+    private void authWriteLengths(long aLen, long dLen, byte[] buf) {
+        asLongLittleEndian.set(buf, 0, aLen);
+        asLongLittleEndian.set(buf, Long.BYTES, dLen);
+    }
+
+    /**
+     * Interface for the underlying processing engines for ChaCha20
+     */
+    interface ChaChaEngine {
+        /**
+         * Perform a multi-part update for ChaCha20.
+         *
+         * @param in the input data.
+         * @param inOff the offset into the input.
+         * @param inLen the length of the data to process.
+         * @param out the output buffer.
+         * @param outOff the offset at which to write the output data.
+         *
+         * @return the number of output bytes written.
+         *
+         * @throws ShortBufferException if the output buffer does not
+         *      provide enough space.
+         * @throws KeyException if the counter value has been exhausted.
+         */
+        int doUpdate(byte[] in, int inOff, int inLen, byte[] out, int outOff)
+                throws ShortBufferException, KeyException;
+
+        /**
+         * Finalize a multi-part or single-part ChaCha20 operation.
+         *
+         * @param in the input data.
+         * @param inOff the offset into the input.
+         * @param inLen the length of the data to process.
+         * @param out the output buffer.
+         * @param outOff the offset at which to write the output data.
+         *
+         * @return the number of output bytes written.
+         *
+         * @throws ShortBufferException if the output buffer does not
+         *      provide enough space.
+         * @throws AEADBadTagException if in decryption mode the provided
+         *      tag and calculated tag do not match.
+         * @throws KeyException if the counter value has been exhausted.
+         */
+        int doFinal(byte[] in, int inOff, int inLen, byte[] out, int outOff)
+                throws ShortBufferException, AEADBadTagException, KeyException;
+    }
+
+    private final class EngineStreamOnly implements ChaChaEngine {
+
+        private EngineStreamOnly () { }
+
+        @Override
+        public int doUpdate(byte[] in, int inOff, int inLen, byte[] out,
+                int outOff) throws ShortBufferException, KeyException {
+            if (initialized) {
+               try {
+                    if (out != null) {
+                        Objects.checkFromIndexSize(outOff, inLen, out.length);
+                    } else {
+                        throw new ShortBufferException(
+                                "Output buffer too small");
+                    }
+                } catch (IndexOutOfBoundsException iobe) {
+                    throw new ShortBufferException("Output buffer too small");
+                }
+                if (in != null) {
+                    Objects.checkFromIndexSize(inOff, inLen, in.length);
+                    chaCha20Transform(in, inOff, inLen, out, outOff);
+                }
+                return inLen;
+            } else {
+                throw new IllegalStateException(
+                        "Must use either a different key or iv.");
+            }
+        }
+
+        @Override
+        public int doFinal(byte[] in, int inOff, int inLen, byte[] out,
+                int outOff) throws ShortBufferException, KeyException {
+            return doUpdate(in, inOff, inLen, out, outOff);
+        }
+    }
+
+    private final class EngineAEADEnc implements ChaChaEngine {
+
+        private EngineAEADEnc() throws InvalidKeyException {
+            initAuthenticator();
+            counter = 1;
+        }
+
+        @Override
+        public int doUpdate(byte[] in, int inOff, int inLen, byte[] out,
+                int outOff) throws ShortBufferException, KeyException {
+            if (initialized) {
+                // If this is the first update since AAD updates, signal that
+                // we're done processing AAD info and pad the AAD to a multiple
+                // of 16 bytes.
+                if (!aadDone) {
+                    authPad16(aadLen);
+                    aadDone = true;
+                }
+                try {
+                    if (out != null) {
+                        Objects.checkFromIndexSize(outOff, inLen, out.length);
+                    } else {
+                        throw new ShortBufferException(
+                                "Output buffer too small");
+                    }
+                } catch (IndexOutOfBoundsException iobe) {
+                    throw new ShortBufferException("Output buffer too small");
+                }
+                if (in != null) {
+                    Objects.checkFromIndexSize(inOff, inLen, in.length);
+                    chaCha20Transform(in, inOff, inLen, out, outOff);
+                    dataLen += authUpdate(out, outOff, inLen);
+                }
+
+                return inLen;
+            } else {
+                throw new IllegalStateException(
+                        "Must use either a different key or iv.");
+            }
+        }
+
+        @Override
+        public int doFinal(byte[] in, int inOff, int inLen, byte[] out,
+                int outOff) throws ShortBufferException, KeyException {
+            // Make sure we have enough room for the remaining data (if any)
+            // and the tag.
+            if ((inLen + TAG_LENGTH) > (out.length - outOff)) {
+                throw new ShortBufferException("Output buffer too small");
+            }
+
+            doUpdate(in, inOff, inLen, out, outOff);
+            authFinalizeData(null, 0, 0, out, outOff + inLen);
+            aadDone = false;
+            return inLen + TAG_LENGTH;
+        }
+    }
+
+    private final class EngineAEADDec implements ChaChaEngine {
+
+        private final ByteArrayOutputStream cipherBuf;
+        private final byte[] tag;
+
+        private EngineAEADDec() throws InvalidKeyException {
+            initAuthenticator();
+            counter = 1;
+            cipherBuf = new ByteArrayOutputStream(CIPHERBUF_BASE);
+            tag = new byte[TAG_LENGTH];
+        }
+
+        @Override
+        public int doUpdate(byte[] in, int inOff, int inLen, byte[] out,
+                int outOff) {
+            if (initialized) {
+                // If this is the first update since AAD updates, signal that
+                // we're done processing AAD info and pad the AAD to a multiple
+                // of 16 bytes.
+                if (!aadDone) {
+                    authPad16(aadLen);
+                    aadDone = true;
+                }
+
+                if (in != null) {
+                    Objects.checkFromIndexSize(inOff, inLen, in.length);
+                    cipherBuf.write(in, inOff, inLen);
+                }
+            } else {
+                throw new IllegalStateException(
+                        "Must use either a different key or iv.");
+            }
+
+            return 0;
+        }
+
+        @Override
+        public int doFinal(byte[] in, int inOff, int inLen, byte[] out,
+                int outOff) throws ShortBufferException, AEADBadTagException,
+                KeyException {
+
+            byte[] ctPlusTag;
+            int ctPlusTagLen;
+            if (cipherBuf.size() == 0 && inOff == 0) {
+                // No previous data has been seen before doFinal, so we do
+                // not need to hold any ciphertext in a buffer.  We can
+                // process it directly from the "in" parameter.
+                doUpdate(null, inOff, inLen, out, outOff);
+                ctPlusTag = in;
+                ctPlusTagLen = inLen;
+            } else {
+                doUpdate(in, inOff, inLen, out, outOff);
+                ctPlusTag = cipherBuf.toByteArray();
+                ctPlusTagLen = ctPlusTag.length;
+            }
+            cipherBuf.reset();
+
+            // There must at least be a tag length's worth of ciphertext
+            // data in the buffered input.
+            if (ctPlusTagLen < TAG_LENGTH) {
+                throw new AEADBadTagException("Input too short - need tag");
+            }
+            int ctLen = ctPlusTagLen - TAG_LENGTH;
+
+            // Make sure we will have enough room for the output buffer
+            try {
+                Objects.checkFromIndexSize(outOff, ctLen, out.length);
+            } catch (IndexOutOfBoundsException ioobe) {
+                throw new ShortBufferException("Output buffer too small");
+            }
+
+            // Calculate and compare the tag.  Only do the decryption
+            // if and only if the tag matches.
+            authFinalizeData(ctPlusTag, 0, ctLen, tag, 0);
+            if (Arrays.compare(ctPlusTag, ctLen, ctPlusTagLen,
+                    tag, 0, tag.length) != 0) {
+                throw new AEADBadTagException("Tag mismatch");
+            }
+            chaCha20Transform(ctPlusTag, 0, ctLen, out, outOff);
+            aadDone = false;
+
+            return ctLen;
+        }
+    }
+
+    public static final class ChaCha20Only extends ChaCha20Cipher {
+        public ChaCha20Only() {
+            mode = MODE_NONE;
+        }
+    }
+
+    public static final class ChaCha20Poly1305 extends ChaCha20Cipher {
+        public ChaCha20Poly1305() {
+            mode = MODE_AEAD;
+            authAlgName = "Poly1305";
+        }
+    }
+}
--- /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();
+    }
+}
+