src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Cipher.java
changeset 50323 25d711fca885
child 55710 18130ed28231
child 58678 9cf78a70fa4f
--- /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";
+        }
+    }
+}