com.sun.crypto.provider.GaloisCounterMode Maven / Gradle / Ivy
/*
* Copyright (c) 2013, 2021, 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 sun.nio.ch.DirectBuffer;
import sun.security.jca.JCAUtil;
import sun.security.util.ArrayUtil;
import javax.crypto.AEADBadTagException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.GCMParameterSpec;
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.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.ProviderException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.InvalidParameterSpecException;
import java.util.Arrays;
/**
* This class represents ciphers in GaloisCounter (GCM) mode.
*
* This mode currently should only be used w/ AES cipher.
* Although no checking is done, caller should only pass AES
* Cipher to the constructor.
*
*
NOTE: Unlike other modes, when used for decryption, this class
* will buffer all processed outputs internally and won't return them
* until the tag has been successfully verified.
*
* @since 1.8
*/
abstract class GaloisCounterMode extends CipherSpi {
static int DEFAULT_IV_LEN = 12; // in bytes
static int DEFAULT_TAG_LEN = 16; // in bytes
// In NIST SP 800-38D, GCM input size is limited to be no longer
// than (2^36 - 32) bytes. Otherwise, the counter will wrap
// around and lead to a leak of plaintext.
// However, given the current GCM spec requirement that recovered
// text can only be returned after successful tag verification,
// we are bound by limiting the data size to the size limit of
// java byte array, e.g. Integer.MAX_VALUE, since all data
// can only be returned by the doFinal(...) call.
private static final int MAX_BUF_SIZE = Integer.MAX_VALUE;
// data size when buffer is divided up to aid in intrinsics
private static final int TRIGGERLEN = 65536; // 64k
static final byte[] EMPTY_BUF = new byte[0];
private boolean initialized = false;
SymmetricCipher blockCipher;
// Engine instance for encryption or decryption
private GCMEngine engine;
private boolean encryption = true;
// Default value is 128bits, this is in bytes.
int tagLenBytes = DEFAULT_TAG_LEN;
// Key size if the value is passed, in bytes.
int keySize;
// Prevent reuse of iv or key
boolean reInit = false;
byte[] lastKey = EMPTY_BUF;
byte[] lastIv = EMPTY_BUF;
byte[] iv = null;
SecureRandom random = null;
/**
*
* @param keySize length of key.
* @param embeddedCipher Cipher object, such as AESCrypt.
*/
GaloisCounterMode(int keySize, SymmetricCipher embeddedCipher) {
blockCipher = embeddedCipher;
this.keySize = keySize;
}
/**
* Initializes the cipher in the specified mode with the given key
* and iv.
*/
void init(int opmode, Key key, GCMParameterSpec spec)
throws InvalidKeyException, InvalidAlgorithmParameterException {
encryption = (opmode == Cipher.ENCRYPT_MODE) ||
(opmode == Cipher.WRAP_MODE);
int tagLen = spec.getTLen();
if (tagLen < 96 || tagLen > 128 || ((tagLen & 0x07) != 0)) {
throw new InvalidAlgorithmParameterException
("Unsupported TLen value. Must be one of " +
"{128, 120, 112, 104, 96}");
}
tagLenBytes = tagLen >> 3;
// Check the Key object is valid and the right size
if (key == null) {
throw new InvalidKeyException("The key must not be null");
}
byte[] keyValue = key.getEncoded();
if (keyValue == null) {
throw new InvalidKeyException("Key encoding must not be null");
} else if (keySize != -1 && keyValue.length != keySize) {
Arrays.fill(keyValue, (byte) 0);
throw new InvalidKeyException("The key must be " +
keySize + " bytes");
}
// Check for reuse
if (encryption) {
if (MessageDigest.isEqual(keyValue, lastKey) &&
MessageDigest.isEqual(iv, lastIv)) {
Arrays.fill(keyValue, (byte) 0);
throw new InvalidAlgorithmParameterException(
"Cannot reuse iv for GCM encryption");
}
// Both values are already clones
if (lastKey != null) {
Arrays.fill(lastKey, (byte) 0);
}
lastKey = keyValue;
lastIv = iv;
}
reInit = false;
// always encrypt mode for embedded cipher
try {
blockCipher.init(false, key.getAlgorithm(), keyValue);
} finally {
if (!encryption) {
Arrays.fill(keyValue, (byte) 0);
}
}
}
@Override
protected void engineSetMode(String mode) throws NoSuchAlgorithmException {
if (!mode.equalsIgnoreCase("GCM")) {
throw new NoSuchAlgorithmException("Mode must be GCM");
}
}
@Override
protected void engineSetPadding(String padding)
throws NoSuchPaddingException {
if (!padding.equalsIgnoreCase("NoPadding")) {
throw new NoSuchPaddingException("Padding must be NoPadding");
}
}
@Override
protected int engineGetBlockSize() {
return blockCipher.getBlockSize();
}
@Override
protected int engineGetOutputSize(int inputLen) {
checkInit();
return engine.getOutputSize(inputLen, true);
}
@Override
protected int engineGetKeySize(Key key) throws InvalidKeyException {
byte[] encoded = key.getEncoded();
Arrays.fill(encoded, (byte)0);
if (!AESCrypt.isKeySizeValid(encoded.length)) {
throw new InvalidKeyException("Invalid key length: " +
encoded.length + " bytes");
}
return Math.multiplyExact(encoded.length, 8);
}
@Override
protected byte[] engineGetIV() {
if (iv == null) {
return null;
}
return iv.clone();
}
/**
* Create a random 16-byte iv.
*
* @param rand a {@code SecureRandom} object. If {@code null} is
* provided a new {@code SecureRandom} object will be instantiated.
*
* @return a 16-byte array containing the random nonce.
*/
private static byte[] createIv(SecureRandom rand) {
byte[] iv = new byte[DEFAULT_IV_LEN];
if (rand == null) {
rand = JCAUtil.getDefSecureRandom();
}
rand.nextBytes(iv);
return iv;
}
@Override
protected AlgorithmParameters engineGetParameters() {
GCMParameterSpec spec;
spec = new GCMParameterSpec(tagLenBytes * 8,
iv == null ? createIv(random) : iv.clone());
try {
AlgorithmParameters params =
AlgorithmParameters.getInstance("GCM",
SunJCE.getInstance());
params.init(spec);
return params;
} catch (NoSuchAlgorithmException | InvalidParameterSpecException e) {
throw new RuntimeException(e);
}
}
@Override
protected void engineInit(int opmode, Key key, SecureRandom random)
throws InvalidKeyException {
engine = null;
if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE) {
throw new InvalidKeyException("No GCMParameterSpec specified");
}
try {
engineInit(opmode, key, (AlgorithmParameterSpec) null, random);
} catch (InvalidAlgorithmParameterException e) {
// never happen
}
}
@Override
protected void engineInit(int opmode, Key key,
AlgorithmParameterSpec params, SecureRandom random)
throws InvalidKeyException, InvalidAlgorithmParameterException {
GCMParameterSpec spec;
this.random = random;
engine = null;
if (params == null) {
iv = createIv(random);
spec = new GCMParameterSpec(DEFAULT_TAG_LEN * 8, iv);
} else {
if (!(params instanceof GCMParameterSpec)) {
throw new InvalidAlgorithmParameterException(
"AlgorithmParameterSpec not of GCMParameterSpec");
}
spec = (GCMParameterSpec)params;
iv = spec.getIV();
if (iv == null) {
throw new InvalidAlgorithmParameterException("IV is null");
}
if (iv.length == 0) {
throw new InvalidAlgorithmParameterException("IV is empty");
}
}
init(opmode, key, spec);
initialized = true;
}
@Override
protected void engineInit(int opmode, Key key, AlgorithmParameters params,
SecureRandom random) throws InvalidKeyException,
InvalidAlgorithmParameterException {
GCMParameterSpec spec = null;
engine = null;
if (params != null) {
try {
spec = params.getParameterSpec(GCMParameterSpec.class);
} catch (InvalidParameterSpecException e) {
throw new InvalidAlgorithmParameterException(e);
}
}
engineInit(opmode, key, spec, random);
}
void checkInit() {
if (!initialized) {
throw new IllegalStateException("Operation not initialized.");
}
if (engine == null) {
if (encryption) {
engine = new GCMEncrypt(blockCipher);
} else {
engine = new GCMDecrypt(blockCipher);
}
}
}
void checkReInit() {
if (reInit) {
throw new IllegalStateException(
"Must use either different key or " + " iv for GCM encryption");
}
}
@Override
protected byte[] engineUpdate(byte[] input, int inputOffset, int inputLen) {
checkInit();
ArrayUtil.nullAndBoundsCheck(input, inputOffset, inputLen);
return engine.doUpdate(input, inputOffset, inputLen);
}
@Override
protected int engineUpdate(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException {
checkInit();
ArrayUtil.nullAndBoundsCheck(input, inputOffset, inputLen);
ArrayUtil.nullAndBoundsCheck(output, outputOffset,
output.length - outputOffset);
int len = engine.getOutputSize(inputLen, false);
if (len > output.length - outputOffset) {
throw new ShortBufferException("Output buffer too small, must be " +
"at least " + len + " bytes long");
}
return engine.doUpdate(input, inputOffset, inputLen, output,
outputOffset);
}
@Override
protected int engineUpdate(ByteBuffer src, ByteBuffer dst)
throws ShortBufferException {
checkInit();
int len = engine.getOutputSize(src.remaining(), false);
if (len > dst.remaining()) {
throw new ShortBufferException(
"Output buffer must be at least " + len + " bytes long");
}
return engine.doUpdate(src, dst);
}
@Override
protected void engineUpdateAAD(byte[] src, int offset, int len) {
checkInit();
engine.updateAAD(src, offset, len);
}
@Override
protected void engineUpdateAAD(ByteBuffer src) {
checkInit();
if (src.hasArray()) {
int pos = src.position();
int len = src.remaining();
engine.updateAAD(src.array(), src.arrayOffset() + pos, len);
src.position(pos + len);
} else {
byte[] aad = new byte[src.remaining()];
src.get(aad);
engine.updateAAD(aad, 0, aad.length);
}
}
@Override
protected byte[] engineDoFinal(byte[] input, int inputOffset,
int inputLen) throws IllegalBlockSizeException, BadPaddingException {
if (input == null) {
input = EMPTY_BUF;
}
try {
ArrayUtil.nullAndBoundsCheck(input, inputOffset, inputLen);
} catch (ArrayIndexOutOfBoundsException e) {
throw new IllegalBlockSizeException("input array invalid");
}
checkInit();
byte[] output = new byte[engine.getOutputSize(inputLen, true)];
try {
engine.doFinal(input, inputOffset, inputLen, output, 0);
} catch (ShortBufferException e) {
throw new ProviderException(e);
} finally {
// Release crypto engine
engine = null;
}
return output;
}
@Override
protected int engineDoFinal(byte[] input, int inputOffset, int inputLen,
byte[] output, int outputOffset) throws ShortBufferException,
IllegalBlockSizeException, BadPaddingException {
if (input == null) {
input = EMPTY_BUF;
}
try {
ArrayUtil.nullAndBoundsCheck(input, inputOffset, inputLen);
} catch (ArrayIndexOutOfBoundsException e) {
// Release crypto engine
engine = null;
throw new IllegalBlockSizeException("input array invalid");
}
checkInit();
int len = engine.doFinal(input, inputOffset, inputLen, output,
outputOffset);
// Release crypto engine
engine = null;
return len;
}
@Override
protected int engineDoFinal(ByteBuffer src, ByteBuffer dst)
throws ShortBufferException, IllegalBlockSizeException,
BadPaddingException {
checkInit();
int len = engine.doFinal(src, dst);
// Release crypto engine
engine = null;
return len;
}
@Override
protected byte[] engineWrap(Key key) throws IllegalBlockSizeException,
InvalidKeyException {
byte[] encodedKey = null;
checkInit();
try {
encodedKey = key.getEncoded();
if ((encodedKey == null) || (encodedKey.length == 0)) {
throw new InvalidKeyException(
"Cannot get an encoding of the key to be wrapped");
}
return engineDoFinal(encodedKey, 0, encodedKey.length);
} catch (BadPaddingException e) {
// should never happen
} finally {
// Release crypto engine
engine = null;
if (encodedKey != null) {
Arrays.fill(encodedKey, (byte) 0);
}
}
return null;
}
@Override
protected Key engineUnwrap(byte[] wrappedKey, String wrappedKeyAlgorithm,
int wrappedKeyType) throws InvalidKeyException,
NoSuchAlgorithmException {
checkInit();
byte[] encodedKey;
try {
encodedKey = engineDoFinal(wrappedKey, 0,
wrappedKey.length);
} catch (BadPaddingException ePadding) {
throw new InvalidKeyException(
"The wrapped key is not padded correctly");
} catch (IllegalBlockSizeException eBlockSize) {
throw new InvalidKeyException(
"The wrapped key does not have the correct length");
}
try {
return ConstructKeys.constructKey(encodedKey, wrappedKeyAlgorithm,
wrappedKeyType);
} finally {
Arrays.fill(encodedKey, (byte)0);
}
}
// value must be 16-byte long; used by GCTR and GHASH as well
static void increment32(byte[] value) {
// start from last byte and only go over 4 bytes, i.e. total 32 bits
int n = value.length - 1;
while ((n >= value.length - 4) && (++value[n] == 0)) {
n--;
}
}
private static final VarHandle wrapToByteArray =
MethodHandles.byteArrayViewVarHandle(long[].class,
ByteOrder.BIG_ENDIAN);
private static byte[] getLengthBlock(int ivLenInBytes) {
byte[] out = new byte[16];
wrapToByteArray.set(out, 8, ((long)ivLenInBytes & 0xFFFFFFFFL) << 3);
return out;
}
private static byte[] getLengthBlock(int aLenInBytes, int cLenInBytes) {
byte[] out = new byte[16];
wrapToByteArray.set(out, 0, ((long)aLenInBytes & 0xFFFFFFFFL) << 3);
wrapToByteArray.set(out, 8, ((long)cLenInBytes & 0xFFFFFFFFL) << 3);
return out;
}
private static byte[] expandToOneBlock(byte[] in, int inOfs, int len,
int blockSize) {
if (len > blockSize) {
throw new ProviderException("input " + len + " too long");
}
if (len == blockSize && inOfs == 0) {
return in;
} else {
byte[] paddedIn = new byte[blockSize];
System.arraycopy(in, inOfs, paddedIn, 0, len);
return paddedIn;
}
}
private static byte[] getJ0(byte[] iv, byte[] subkeyH, int blockSize) {
byte[] j0;
if (iv.length == 12) { // 96 bits
j0 = expandToOneBlock(iv, 0, iv.length, blockSize);
j0[blockSize - 1] = 1;
} else {
GHASH g = new GHASH(subkeyH);
int lastLen = iv.length % blockSize;
if (lastLen != 0) {
g.update(iv, 0, iv.length - lastLen);
byte[] padded =
expandToOneBlock(iv, iv.length - lastLen, lastLen,
blockSize);
g.update(padded);
} else {
g.update(iv);
}
g.update(getLengthBlock(iv.length));
j0 = g.digest();
}
return j0;
}
/**
* Calculate if the given data lengths and the already processed data
* exceeds the maximum allowed processed data by GCM.
* @param lengths lengths of unprocessed data.
*/
private void checkDataLength(int ... lengths) {
int max = MAX_BUF_SIZE;
for (int len : lengths) {
max = Math.subtractExact(max, len);
}
if (engine.processed > max) {
throw new ProviderException("SunJCE provider only supports " +
"input size up to " + MAX_BUF_SIZE + " bytes");
}
}
/**
* Abstract class for GCMEncrypt and GCMDecrypt internal context objects
*/
abstract class GCMEngine {
byte[] preCounterBlock;
GCTR gctrPAndC;
GHASH ghashAllToS;
// Block size of the algorithm
final int blockSize;
// length of total data, i.e. len(C)
int processed = 0;
// buffer for AAD data; if null, meaning update has been called
ByteArrayOutputStream aadBuffer = null;
int sizeOfAAD = 0;
boolean aadProcessed = false;
// buffer data for crypto operation
ByteArrayOutputStream ibuffer = null;
// Original dst buffer if there was an overlap situation
ByteBuffer originalDst = null;
byte[] originalOut = null;
int originalOutOfs = 0;
GCMEngine(SymmetricCipher blockCipher) {
blockSize = blockCipher.getBlockSize();
byte[] subkeyH = new byte[blockSize];
blockCipher.encryptBlock(subkeyH, 0, subkeyH,0);
preCounterBlock = getJ0(iv, subkeyH, blockSize);
byte[] j0Plus1 = preCounterBlock.clone();
increment32(j0Plus1);
gctrPAndC = new GCTR(blockCipher, j0Plus1);
ghashAllToS = new GHASH(subkeyH);
}
/**
* Get output buffer size
* @param inLen Contains the length of the input data and buffered data.
* @param isFinal true if this is a doFinal operation
* @return If it's an update operation, inLen must blockSize
* divisible. If it's a final operation, output will
* include the tag.
*/
abstract int getOutputSize(int inLen, boolean isFinal);
// Update operations
abstract byte[] doUpdate(byte[] in, int inOff, int inLen);
abstract int doUpdate(byte[] in, int inOff, int inLen, byte[] out,
int outOff) throws ShortBufferException;
abstract int doUpdate(ByteBuffer src, ByteBuffer dst)
throws ShortBufferException;
// Final operations
abstract int doFinal(byte[] in, int inOff, int inLen, byte[] out,
int outOff) throws IllegalBlockSizeException, AEADBadTagException,
ShortBufferException;
abstract int doFinal(ByteBuffer src, ByteBuffer dst)
throws IllegalBlockSizeException, AEADBadTagException,
ShortBufferException;
// Initialize internal data buffer, if not already.
void initBuffer(int len) {
if (ibuffer == null) {
ibuffer = new ByteArrayOutputStream(len);
}
}
// Helper method for getting ibuffer size
int getBufferedLength() {
return (ibuffer == null ? 0 : ibuffer.size());
}
/**
* The method takes two buffers to create one block of data. The
* difference with the other mergeBlock is this will calculate
* the bufLen from the existing 'buffer' length & offset
*
* This is only called when buffer length is less than a blockSize
* @return number of bytes used from 'in'
*/
int mergeBlock(byte[] buffer, int bufOfs, byte[] in, int inOfs,
int inLen, byte[] block) {
return mergeBlock(buffer, bufOfs, buffer.length - bufOfs, in,
inOfs, inLen, block);
}
/**
* The method takes two buffers to create one block of data
*
* This is only called when buffer length is less than a blockSize
* @return number of bytes used from 'in'
*/
int mergeBlock(byte[] buffer, int bufOfs, int bufLen, byte[] in,
int inOfs, int inLen, byte[] block) {
if (bufLen > blockSize) {
throw new RuntimeException("mergeBlock called on an ibuffer " +
"too big: " + bufLen + " bytes");
}
System.arraycopy(buffer, bufOfs, block, 0, bufLen);
int inUsed = Math.min(block.length - bufLen, inLen);
System.arraycopy(in, inOfs, block, bufLen, inUsed);
return inUsed;
}
/**
* Continues a multi-part update of the Additional Authentication
* Data (AAD), using a subset of the provided buffer. All AAD must be
* supplied before beginning operations on the ciphertext (via the
* {@code update} and {@code doFinal} methods).
*
* @param src the buffer containing the AAD
* @param offset the offset in {@code src} where the AAD input starts
* @param len the number of AAD bytes
*
* @throws IllegalStateException if this cipher is in a wrong state
* (e.g., has not been initialized) or does not accept AAD, and one of
* the {@code update} methods has already been called for the active
* encryption/decryption operation
* @throws UnsupportedOperationException if this method
* has not been overridden by an implementation
*/
void updateAAD(byte[] src, int offset, int len) {
if (encryption) {
checkReInit();
}
if (aadBuffer == null) {
if (sizeOfAAD == 0 && !aadProcessed) {
aadBuffer = new ByteArrayOutputStream(len);
} else {
// update has already been called
throw new IllegalStateException
("Update has been called; no more AAD data");
}
}
aadBuffer.write(src, offset, len);
}
// Feed the AAD data to GHASH, pad if necessary
void processAAD() {
if (aadBuffer != null) {
if (aadBuffer.size() > 0) {
byte[] aad = aadBuffer.toByteArray();
sizeOfAAD = aad.length;
int lastLen = aad.length % blockSize;
if (lastLen != 0) {
ghashAllToS.update(aad, 0, aad.length - lastLen);
byte[] padded = expandToOneBlock(aad,
aad.length - lastLen, lastLen, blockSize);
ghashAllToS.update(padded);
} else {
ghashAllToS.update(aad);
}
}
aadBuffer = null;
}
aadProcessed = true;
}
/**
* Process en/decryption all the way to the last block. It takes both
* For input it takes the ibuffer which is wrapped in 'buffer' and 'src'
* from doFinal.
*/
int doLastBlock(GCM op, ByteBuffer buffer, ByteBuffer src, ByteBuffer dst) {
int resultLen = 0;
int bLen = (buffer != null ? buffer.remaining() : 0);
if (bLen > 0) {
// en/decrypt on how much buffer there is in AES_BLOCK_SIZE
if (bLen >= blockSize) {
resultLen += op.update(buffer, dst);
}
// Process the remainder in the buffer
if (bLen - resultLen > 0) {
// Copy the buffer remainder into an extra block
byte[] block = new byte[blockSize];
int over = buffer.remaining();
buffer.get(block, 0, over);
// If src has data, complete the block;
int slen = Math.min(src.remaining(), blockSize - over);
if (slen > 0) {
src.get(block, over, slen);
}
int len = slen + over;
if (len == blockSize) {
resultLen += op.update(block, 0, blockSize, dst);
} else {
resultLen += op.doFinal(block, 0, len, block,
0);
if (dst != null) {
dst.put(block, 0, len);
}
processed += resultLen;
return resultLen;
}
}
}
// en/decrypt whatever remains in src.
// If src has been consumed, this will be a no-op
if (src.remaining() > TRIGGERLEN) {
resultLen += throttleData(op, src, dst);
}
resultLen += op.doFinal(src, dst);
processed += resultLen;
return resultLen;
}
/**
* This segments large data into smaller chunks so hotspot will start
* using GCTR and GHASH intrinsics sooner. This is a problem for app
* and perf tests that only use large input sizes.
*/
int throttleData(GCM op, byte[] in, int inOfs, int inLen,
byte[] out, int outOfs) {
int segments = (inLen / 6);
segments -= segments % blockSize;
int len = 0;
int i = 0;
do {
len += op.update(in, inOfs + len, segments, out,outOfs + len);
} while (++i < 5);
len += op.update(in, inOfs + len, inLen - len, out, outOfs + len);
return len;
}
/**
* This segments large data into smaller chunks so hotspot will start
* using GCTR and GHASH intrinsics sooner. This is a problem for app
* and perf tests that only use large input sizes.
*/
int throttleData(GCM op, ByteBuffer src, ByteBuffer dst) {
int inLen = src.limit();
int segments = (src.remaining() / 6);
segments -= segments % blockSize;
int i = 0, resultLen = 0;
do {
src.limit(src.position() + segments);
resultLen += op.update(src, dst);
} while (++i < 5);
src.limit(inLen);
// If there is still at least a blockSize left
if (src.remaining() > blockSize) {
resultLen += op.update(src, dst);
}
return resultLen;
}
/**
* Check for overlap. If the src and dst buffers are using shared data
* and if dst will overwrite src data before src can be processed.
* If so, make a copy to put the dst data in.
*/
ByteBuffer overlapDetection(ByteBuffer src, ByteBuffer dst) {
if (src.isDirect() && dst.isDirect()) {
DirectBuffer dsrc = (DirectBuffer) src;
DirectBuffer ddst = (DirectBuffer) dst;
// Get the current memory address for the given ByteBuffers
long srcaddr = dsrc.address();
long dstaddr = ddst.address();
// Find the lowest attachment that is the base memory address
// of the shared memory for the src object
while (dsrc.attachment() != null) {
srcaddr = ((DirectBuffer) dsrc.attachment()).address();
dsrc = (DirectBuffer) dsrc.attachment();
}
// Find the lowest attachment that is the base memory address
// of the shared memory for the dst object
while (ddst.attachment() != null) {
dstaddr = ((DirectBuffer) ddst.attachment()).address();
ddst = (DirectBuffer) ddst.attachment();
}
// If the base addresses are not the same, there is no overlap
if (srcaddr != dstaddr) {
return dst;
}
// At this point we know these objects share the same memory.
// This checks the starting position of the src and dst address
// for overlap.
// It uses the base address minus the passed object's address to
// get the offset from the base address, then add the position()
// from the passed object. That gives up the true offset from
// the base address. As long as the src side is >= the dst
// side, we are not in overlap.
if (((DirectBuffer) src).address() - srcaddr + src.position() >=
((DirectBuffer) dst).address() - dstaddr + dst.position()) {
return dst;
}
} else if (!src.isDirect() && !dst.isDirect()) {
// if src is read only, then we need a copy
if (!src.isReadOnly()) {
// If using the heap, check underlying byte[] address.
if (src.array() != dst.array()) {
return dst;
}
// Position plus arrayOffset() will give us the true offset
// from the underlying byte[] address.
if (src.position() + src.arrayOffset() >=
dst.position() + dst.arrayOffset()) {
return dst;
}
}
} else {
// buffer types are not the same and can be used as-is
return dst;
}
// Create a copy
ByteBuffer tmp = dst.duplicate();
// We can use a heap buffer for internal use, save on alloc cost
ByteBuffer bb = ByteBuffer.allocate(dst.remaining());
tmp.limit(dst.limit());
tmp.position(dst.position());
bb.put(tmp);
bb.flip();
originalDst = dst;
return bb;
}
/**
* Overlap detection for data using byte array.
* If an intermediate array is needed, the original out array length is
* allocated because for code simplicity.
*/
byte[] overlapDetection(byte[] in, int inOfs, byte[] out, int outOfs) {
if (in == out && inOfs < outOfs) {
originalOut = out;
originalOutOfs = outOfs;
return new byte[out.length];
}
return out;
}
/**
* If originalDst is not null, 'dst' is an internal buffer and it's
* data will be copied to the original dst buffer
*/
void restoreDst(ByteBuffer dst) {
if (originalDst == null) {
return;
}
dst.flip();
originalDst.put(dst);
originalDst = null;
}
/**
* If originalOut is not null, the 'out' is an internal buffer and it's
* data will be copied into original out byte[];
*/
void restoreOut(byte[] out, int len) {
if (originalOut == null) {
return;
}
System.arraycopy(out, originalOutOfs, originalOut, originalOutOfs,
len);
originalOut = null;
}
}
/**
* Encryption Engine object
*/
class GCMEncrypt extends GCMEngine {
GCTRGHASH gctrghash;
GCMEncrypt(SymmetricCipher blockCipher) {
super(blockCipher);
gctrghash = new GCTRGHASH(gctrPAndC, ghashAllToS);
}
@Override
public int getOutputSize(int inLen, boolean isFinal) {
int len = getBufferedLength();
if (isFinal) {
return len + inLen + tagLenBytes;
} else {
len += inLen;
return len - (len % blockCipher.getBlockSize());
}
}
@Override
byte[] doUpdate(byte[] in, int inOff, int inLen) {
checkReInit();
byte[] output = new byte[getOutputSize(inLen, false)];
try {
doUpdate(in, inOff, inLen, output, 0);
} catch (ShortBufferException e) {
// This should never happen because we just allocated output
throw new ProviderException("output buffer creation failed", e);
}
return output;
}
/**
* Encrypt update operation. This uses both the ibuffer and 'in' to
* encrypt as many blocksize data as possible. Any remaining data is
* put into the ibuffer.
*/
@Override
public int doUpdate(byte[] in, int inOfs, int inLen, byte[] out,
int outOfs) throws ShortBufferException {
checkReInit();
// 'inLen' stores the length to use with buffer 'in'.
// 'len' stores the length returned by the method.
int len = 0;
int bLen = getBufferedLength();
checkDataLength(inLen, bLen);
processAAD();
out = overlapDetection(in, inOfs, out, outOfs);
// if there is enough data in the ibuffer and 'in', encrypt it.
if (bLen > 0) {
byte[] buffer = ibuffer.toByteArray();
// number of bytes not filling a block
int remainder = blockSize - bLen;
// Construct and encrypt a block if there is enough 'buffer' and
// 'in' to make one
if ((inLen + bLen) >= blockSize) {
byte[] block = new byte[blockSize];
System.arraycopy(buffer, 0, block, 0, bLen);
System.arraycopy(in, inOfs, block, bLen, remainder);
len = gctrghash.update(block, 0, blockSize, out, outOfs);
inOfs += remainder;
inLen -= remainder;
outOfs += blockSize;
ibuffer.reset();
}
}
// Encrypt the remaining blocks inside of 'in'
if (inLen >= blockSize) {
len += gctrghash.update(in, inOfs, inLen, out, outOfs);
}
// Write any remaining bytes less than a blockSize into ibuffer.
int remainder = inLen % blockSize;
if (remainder > 0) {
initBuffer(remainder);
inLen -= remainder;
// remainder offset is based on original buffer length
ibuffer.write(in, inOfs + inLen, remainder);
}
restoreOut(out, len);
processed += len;
return len;
}
/**
* Encrypt update operation. This uses both the ibuffer and 'src' to
* encrypt as many blocksize data as possible. Any remaining data is
* put into the ibuffer.
*/
@Override
public int doUpdate(ByteBuffer src, ByteBuffer dst)
throws ShortBufferException {
checkReInit();
int bLen = getBufferedLength();
checkDataLength(src.remaining(), bLen);
// 'len' stores the length returned by the method.
int len = 0;
processAAD();
dst = overlapDetection(src, dst);
// if there is enough data in the ibuffer and 'in', encrypt it.
if (bLen > 0) {
// number of bytes not filling a block
int remainder = blockSize - bLen;
// Check if there is enough 'src' and 'buffer' to fill a block
if (src.remaining() >= remainder) {
byte[] block = new byte[blockSize];
ByteBuffer buffer = ByteBuffer.wrap(ibuffer.toByteArray());
buffer.get(block, 0, bLen);
src.get(block, bLen, remainder);
len += cryptBlocks(
ByteBuffer.wrap(block, 0, blockSize), dst);
ibuffer.reset();
}
}
// encrypt any blocksized data in 'src'
if (src.remaining() >= blockSize) {
len += cryptBlocks(src, dst);
}
// Write the remaining bytes into the 'ibuffer'
if (src.remaining() > 0) {
initBuffer(src.remaining());
byte[] b = new byte[src.remaining()];
src.get(b);
// remainder offset is based on original buffer length
try {
ibuffer.write(b);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
restoreDst(dst);
return len;
}
/**
* Return final encrypted data with auth tag using byte[]
*/
@Override
public int doFinal(byte[] in, int inOfs, int inLen, byte[] out,
int outOfs) throws IllegalBlockSizeException, ShortBufferException {
checkReInit();
try {
ArrayUtil.nullAndBoundsCheck(out, outOfs, getOutputSize(inLen,
true));
} catch (ArrayIndexOutOfBoundsException aiobe) {
throw new ShortBufferException("Output buffer invalid");
}
int bLen = getBufferedLength();
checkDataLength(inLen, bLen, tagLenBytes);
processAAD();
out = overlapDetection(in, inOfs, out, outOfs);
int resultLen = 0;
byte[] block;
// process what is in the ibuffer
if (bLen > 0) {
byte[] buffer = ibuffer.toByteArray();
// Make a block if the remaining ibuffer and 'in' can make one.
if (bLen + inLen >= blockSize) {
int r, bufOfs = 0;
block = new byte[blockSize];
r = mergeBlock(buffer, bufOfs, in, inOfs, inLen, block);
inOfs += r;
inLen -= r;
r = gctrghash.update(block, 0, blockSize, out,
outOfs);
outOfs += r;
resultLen += r;
processed += r;
} else {
// Need to consume all the ibuffer here to prepare for doFinal()
block = new byte[bLen + inLen];
System.arraycopy(buffer, 0, block, 0, bLen);
System.arraycopy(in, inOfs, block, bLen, inLen);
inLen += bLen;
in = block;
inOfs = 0;
}
}
// process what is left in the input buffer
if (inLen > TRIGGERLEN) {
int r = throttleData(gctrghash, in, inOfs, inLen, out, outOfs);
inOfs += r;
inLen -= r;
outOfs += r;
resultLen += r;
processed += r;
}
processed += gctrghash.doFinal(in, inOfs, inLen, out, outOfs);
outOfs += inLen;
resultLen += inLen;
block = getLengthBlock(sizeOfAAD, processed);
ghashAllToS.update(block);
block = ghashAllToS.digest();
new GCTR(blockCipher, preCounterBlock).doFinal(block, 0,
tagLenBytes, block, 0);
// copy the tag to the end of the buffer
System.arraycopy(block, 0, out, outOfs, tagLenBytes);
int len = resultLen + tagLenBytes;
restoreOut(out, len);
reInit = true;
return len;
}
/**
* Return final encrypted data with auth tag using bytebuffers
*/
@Override
public int doFinal(ByteBuffer src, ByteBuffer dst) throws
IllegalBlockSizeException, ShortBufferException {
checkReInit();
dst = overlapDetection(src, dst);
int len = src.remaining() + getBufferedLength();
// 'len' includes ibuffer data
checkDataLength(len, tagLenBytes);
if (dst.remaining() < len + tagLenBytes) {
throw new ShortBufferException("Output buffer too small, must" +
"be at least " + (len + tagLenBytes) + " bytes long");
}
processAAD();
if (len > 0) {
processed += doLastBlock(gctrghash,
(ibuffer == null || ibuffer.size() == 0) ? null :
ByteBuffer.wrap(ibuffer.toByteArray()), src, dst);
}
// release buffer if needed
if (ibuffer != null) {
ibuffer.reset();
}
byte[] block = getLengthBlock(sizeOfAAD, processed);
ghashAllToS.update(block);
block = ghashAllToS.digest();
new GCTR(blockCipher, preCounterBlock).doFinal(block, 0,
tagLenBytes, block, 0);
dst.put(block, 0, tagLenBytes);
restoreDst(dst);
reInit = true;
return (len + tagLenBytes);
}
// Handler method for encrypting blocks
int cryptBlocks(ByteBuffer src, ByteBuffer dst) {
int len;
if (src.remaining() > TRIGGERLEN) {
len = throttleData(gctrghash, src, dst);
} else {
len = gctrghash.update(src, dst);
}
processed += len;
return len;
}
}
/**
* Decryption Engine object
*/
class GCMDecrypt extends GCMEngine {
// byte array of tag
byte[] tag;
// offset for byte[] operations
int tagOfs = 0;
GCMDecrypt(SymmetricCipher blockCipher) {
super(blockCipher);
}
@Override
public int getOutputSize(int inLen, boolean isFinal) {
if (!isFinal) {
return 0;
}
return Math.max(inLen + getBufferedLength() - tagLenBytes, 0);
}
/**
* Find the tag in a given input buffer
*
* If tagOfs > 0, the tag is inside 'in' along with some encrypted data
* If tagOfs = 0, 'in' contains only the tag
* If tagOfs < 0, that tag is split between ibuffer and 'in'
* If tagOfs = -tagLenBytes, the tag is in the ibuffer, 'in' is empty.
*/
void findTag(byte[] in, int inOfs, int inLen) {
tag = new byte[tagLenBytes];
if (inLen >= tagLenBytes) {
tagOfs = inLen - tagLenBytes;
System.arraycopy(in, inOfs + tagOfs, tag, 0,
tagLenBytes);
} else {
// tagOfs will be negative
byte[] buffer = ibuffer.toByteArray();
tagOfs = mergeBlock(buffer,
buffer.length - (tagLenBytes - inLen), in, inOfs, inLen,
tag) - tagLenBytes;
}
}
// Put the input data into the ibuffer
@Override
byte[] doUpdate(byte[] in, int inOff, int inLen) {
try {
doUpdate(in, inOff, inLen, null, 0);
} catch (ShortBufferException e) {
// update decryption has no output
}
return new byte[0];
}
// Put the input data into the ibuffer
@Override
public int doUpdate(byte[] in, int inOfs, int inLen, byte[] out,
int outOfs) throws ShortBufferException {
processAAD();
if (inLen > 0) {
// store internally until decryptFinal is called because
// spec mentioned that only return recovered data after tag
// is successfully verified
initBuffer(inLen);
ibuffer.write(in, inOfs, inLen);
}
return 0;
}
// Put the src data into the ibuffer
@Override
public int doUpdate(ByteBuffer src, ByteBuffer dst)
throws ShortBufferException {
processAAD();
if (src.remaining() > 0) {
// If there is an array, use that to avoid the extra copy to
// take the src data out of the bytebuffer.
if (src.hasArray()) {
doUpdate(src.array(), src.arrayOffset() + src.position(),
src.remaining(), null, 0);
src.position(src.limit());
} else {
byte[] b = new byte[src.remaining()];
src.get(b);
initBuffer(b.length);
try {
ibuffer.write(b);
} catch (IOException e) {
throw new ProviderException(
"Unable to add remaining input to the buffer", e);
}
}
}
return 0;
}
/**
* Use any data from ibuffer and 'in' to first verify the auth tag. If
* the tag is valid, decrypt the data.
*/
@Override
public int doFinal(byte[] in, int inOfs, int inLen, byte[] out,
int outOfs) throws IllegalBlockSizeException, AEADBadTagException,
ShortBufferException {
GHASH save = null;
int len = inLen + getBufferedLength();
try {
ArrayUtil.nullAndBoundsCheck(out, outOfs, len - tagLenBytes);
} catch (ArrayIndexOutOfBoundsException aiobe) {
throw new ShortBufferException("Output buffer invalid");
}
if (len < tagLenBytes) {
throw new AEADBadTagException("Input too short - need tag");
}
if (len - tagLenBytes > out.length - outOfs) {
save = ghashAllToS.clone();
}
checkDataLength(len - tagLenBytes);
processAAD();
findTag(in, inOfs, inLen);
byte[] block = getLengthBlock(sizeOfAAD,
decryptBlocks(ghashAllToS, in, inOfs, inLen, null, 0));
ghashAllToS.update(block);
block = ghashAllToS.digest();
new GCTR(blockCipher, preCounterBlock).doFinal(block, 0,
tagLenBytes, block, 0);
// check entire authentication tag for time-consistency
int mismatch = 0;
for (int i = 0; i < tagLenBytes; i++) {
mismatch |= tag[i] ^ block[i];
}
if (mismatch != 0) {
throw new AEADBadTagException("Tag mismatch!");
}
if (save != null) {
ghashAllToS = save;
throw new ShortBufferException("Output buffer too small, must" +
"be at least " + (len - tagLenBytes) + " bytes long");
}
out = overlapDetection(in, inOfs, out, outOfs);
len = decryptBlocks(gctrPAndC, in, inOfs, inLen, out, outOfs);
restoreOut(out, len);
return len;
}
/**
* Use any data from ibuffer and 'src' to first verify the auth tag. If
* the tag is valid, decrypt the data.
*/
@Override
public int doFinal(ByteBuffer src, ByteBuffer dst)
throws IllegalBlockSizeException, AEADBadTagException,
ShortBufferException {
GHASH save = null;
ByteBuffer tag;
ByteBuffer ct = src.duplicate();
ByteBuffer buffer = null;
// The 'len' the total amount of ciphertext
int len = ct.remaining() - tagLenBytes;
// Check if ibuffer has data
if (getBufferedLength() != 0) {
buffer = ByteBuffer.wrap(ibuffer.toByteArray());
len += buffer.remaining();
}
checkDataLength(len);
// Save GHASH context to allow the tag to be checked even though
// the dst buffer is too short. Context will be restored so the
// method can be called again with the proper sized dst buffer.
if (len > dst.remaining()) {
save = ghashAllToS.clone();
}
// Create buffer 'tag' that contains only the auth tag
if (ct.remaining() >= tagLenBytes) {
tag = src.duplicate();
tag.position(ct.limit() - tagLenBytes);
ct.limit(ct.limit() - tagLenBytes);
} else if (buffer != null) {
// It's unlikely the tag will be between the buffer and data
tag = ByteBuffer.allocate(tagLenBytes);
int limit = buffer.remaining() - (tagLenBytes - ct.remaining());
buffer.mark();
buffer.position(limit);
// Read from "new" limit to buffer's end
tag.put(buffer);
// reset buffer to data only
buffer.reset();
// Set the limit to where the ciphertext ends
buffer.limit(limit);
tag.put(ct);
tag.flip();
} else {
throw new AEADBadTagException("Input too short - need tag");
}
// Set the mark for a later reset. Either it will be zero, or the
// tag buffer creation above will have consume some or all of it.
ct.mark();
processAAD();
// Perform GHASH check on data
doLastBlock(ghashAllToS, buffer, ct, null);
byte[] block = getLengthBlock(sizeOfAAD, len);
ghashAllToS.update(block);
block = ghashAllToS.digest();
new GCTR(blockCipher, preCounterBlock).doFinal(block, 0,
tagLenBytes, block, 0);
// check entire authentication tag for time-consistency
int mismatch = 0;
for (int i = 0; i < tagLenBytes; i++) {
mismatch |= tag.get() ^ block[i];
}
if (mismatch != 0) {
throw new AEADBadTagException("Tag mismatch!");
}
if (save != null) {
ghashAllToS = save;
throw new ShortBufferException("Output buffer too small, must" +
" be at least " + len + " bytes long");
}
// Prepare for decryption
if (buffer != null) {
buffer.flip();
}
ct.reset();
processed = 0;
// Check for overlap in the bytebuffers
dst = overlapDetection(src, dst);
// Decrypt the all the input data and put it into dst
doLastBlock(gctrPAndC, buffer, ct, dst);
restoreDst(dst);
src.position(src.limit());
if (ibuffer != null) {
ibuffer.reset();
}
return processed;
}
/**
* This method organizes the data from the ibuffer and 'in' to
* blocksize operations for GHASH and GCTR decryption operations.
* When this method is used, all the data is either in the ibuffer
* or in 'in'.
*/
int decryptBlocks(GCM op, byte[] in, int inOfs, int inLen,
byte[] out, int outOfs) {
byte[] buffer;
byte[] block;
int len = 0;
// Calculate the encrypted data length inside the ibuffer
// considering the tag location
int bLen = getBufferedLength();
// Change the inLen based of the tag location.
if (tagOfs < 0) {
inLen = 0;
bLen += tagOfs;
} else {
inLen -= tagLenBytes;
}
if (bLen > 0) {
buffer = ibuffer.toByteArray();
if (bLen >= blockSize) {
len += op.update(buffer, 0, bLen, out, outOfs);
outOfs += len; // noop for ghash
// Use len as it becomes the ibuffer offset, if
// needed, in the next op
}
// merge the remaining ibuffer with the 'in'
int bufRemainder = bLen - len;
if (bufRemainder > 0) {
block = new byte[blockSize];
int inUsed = mergeBlock(buffer, len, bufRemainder, in,
inOfs, inLen, block);
// update the input parameters for what was taken out of 'in'
inOfs += inUsed;
inLen -= inUsed;
// If is more than block between the merged data and 'in',
// update(), otherwise setup for final
if (inLen > 0) {
int resultLen = op.update(block, 0, blockSize,
out, outOfs);
outOfs += resultLen; // noop for ghash
len += resultLen;
} else {
in = block;
inOfs = 0;
inLen = inUsed + bufRemainder;
}
}
}
// Finish off the operation
if (inLen > TRIGGERLEN) {
int l = throttleData(op, in, inOfs, inLen, out, outOfs);
inOfs += l;
inLen -= l;
outOfs += l; // noop for ghash
len += l;
}
return len + op.doFinal(in, inOfs, inLen, out, outOfs);
}
}
public static final class AESGCM extends GaloisCounterMode {
public AESGCM() {
super(-1, new AESCrypt());
}
}
public static final class AES128 extends GaloisCounterMode {
public AES128() {
super(16, new AESCrypt());
}
}
public static final class AES192 extends GaloisCounterMode {
public AES192() {
super(24, new AESCrypt());
}
}
public static final class AES256 extends GaloisCounterMode {
public AES256() {
super(32, new AESCrypt());
}
}
/**
* This class is for encryption when both GCTR and GHASH
* can operation in parallel.
*/
static final class GCTRGHASH implements GCM {
GCTR gctr;
GHASH ghash;
GCTRGHASH(GCTR c, GHASH g) {
gctr = c;
ghash = g;
}
@Override
public int update(byte[] in, int inOfs, int inLen, byte[] out,
int outOfs) {
int len = gctr.update(in, inOfs, inLen, out, outOfs);
ghash.update(out, outOfs, len);
return len;
}
@Override
public int update(byte[] in, int inOfs, int inLen, ByteBuffer dst) {
dst.mark();
int len = gctr.update(in, inOfs, inLen, dst);
dst.reset();
ghash.update(dst, len);
return len;
}
@Override
public int update(ByteBuffer src, ByteBuffer dst) {
dst.mark();
int len = gctr.update(src, dst);
dst.reset();
ghash.update(dst, len);
return len;
}
@Override
public int doFinal(byte[] in, int inOfs, int inLen, byte[] out, int outOfs) {
int len = gctr.doFinal(in, inOfs, inLen, out, outOfs);
ghash.doFinal(out, outOfs, len);
return len;
}
@Override
public int doFinal(ByteBuffer src, ByteBuffer dst) {
dst.mark();
int l = gctr.doFinal(src, dst);
dst.reset();
ghash.doFinal(dst, l);
return l;
}
}
}