--- /dev/null 2018-04-11 21:51:09.965351392 -0700 +++ new/src/java.base/share/classes/com/sun/crypto/provider/ChaCha20Cipher.java 2018-04-14 00:28:42.679525353 -0700 @@ -0,0 +1,1343 @@ +/* + * 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. Since this cipher + * does not support initialization with {@code AlgorithmParameters} it + * will always return null. + * + * @return a {@code null} value. + */ + @Override + protected AlgorithmParameters engineGetParameters() { + return null; + } + + /** + * 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 must be either + * {@code Cipher.ENCRYPT_MODE} or {@code Cipher.DECRYPT_MODE} + * @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 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}. + */ + @Override + protected void engineInit(int opmode, Key key, SecureRandom random) + throws InvalidKeyException { + byte[] newNonce = new byte[12]; + SecureRandom rand = (random != null) ? random : new SecureRandom(); + rand.nextBytes(newNonce); + + counter = 1; + init(opmode, key, newNonce); + } + + /** + * 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 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}. + * @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 { + + // 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(); + if (newNonce.length != 12) { + throw new InvalidAlgorithmParameterException( + "ChaCha20 nonce must be 96 bits in length"); + } + 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 96 bits 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 + * either a random nonce if the {@code random} parameter is non-null, or + * a nonce of all zeroes otherwise. + * + * @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 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}. + * @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 { + byte[] newNonce = null; + switch (mode) { + case MODE_NONE: + if (params != null) { + throw new InvalidAlgorithmParameterException( + "AlgorithmParameters not supported"); + } + break; + case MODE_AEAD: + if (params != null) { + String paramAlg = params.getAlgorithm(); + if (!paramAlg.equalsIgnoreCase("ChaCha20-Poly1305")) { + throw new InvalidAlgorithmParameterException( + "Invalid parameter type: " + paramAlg); + } + try { + DerValue dv = new DerValue(params.getEncoded()); + if (dv.tag == DerValue.tag_OctetString) { + // Get the nonce value + newNonce = dv.getOctetString(); + if (newNonce.length != 12) { + throw new InvalidAlgorithmParameterException( + "ChaCha20-Poly1305 nonce must be " + + "96 bits in length"); + } + } else { + throw new InvalidAlgorithmParameterException( + "ChaCha20-Poly1305 Parameter " + + "ASN.1 encoding error"); + } + } catch (IOException ioe) { + throw new InvalidAlgorithmParameterException(ioe); + } + + } + break; + default: + throw new UnsupportedOperationException("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 = new byte[12]; + if (random != null) { + random.nextBytes(newNonce); + } + } + + // 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); + } + } + } + + /** + * 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 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.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; + + aadDone = false; + direction = opmode; + + // 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 (direction == Cipher.ENCRYPT_MODE) { + engine = new EngineAEADEnc(); + } else if (direction == 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(); + 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 (Arrays.equals(newKeyBytes, keyBytes) && + Arrays.equals(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 IllegalBlockSizeException + * @throws InvalidKeyException if the provided {@code Key} does not + * have an encoded form. This exception will also be thrown + * if an AEAD cipher is used to perform the wrap and it fails + * during tag calculation. + */ + @Override + protected byte[] engineWrap(Key key) throws IllegalBlockSizeException, + InvalidKeyException { + byte[] encoded = key.getEncoded(); + if ((encoded == null) || (encoded.length == 0)) { + throw new InvalidKeyException("Could not obtain encoded key"); + } + + try { + return engineDoFinal(encoded, 0, encoded.length); + } catch (AEADBadTagException abte) { + throw new InvalidKeyException(abte); + } + } + + /** + * 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 NoSuchAlgorithmException if no installed providers can create + * keys of type {@code type} for the algorithm specified in + * {@code algorithm}. + * @throws InvalidKeyException if {@code wrappedKey} does not represent a + * wrapped key of type {@code type} for the algorithm specified in + * {@code algorithm}. This exception will also be thrown if the + * provided tag does not match the calculated tag during unwrap. + */ + @Override + protected Key engineUnwrap(byte[] wrappedKey, String algorithm, + int type) throws InvalidKeyException, NoSuchAlgorithmException { + try { + byte[] encoded = engineDoFinal(wrappedKey, 0, wrappedKey.length); + return ConstructKeys.constructKey(encoded, algorithm, type); + } catch (AEADBadTagException abte) { + throw new InvalidKeyException(abte); + } + } + + /** + * 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) { + if (in != null) { + Objects.checkFromIndexSize(inOff, inLen, in.length); + Objects.checkFromIndexSize(outOff, inLen, out.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; + } + + if (in != null) { + Objects.checkFromIndexSize(inOff, inLen, in.length); + Objects.checkFromIndexSize(outOff, inLen, out.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"; + } + } +}