org.bouncycastle.crypto.modes.ChaCha20Poly1305 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of bcprov-jdk15to18 Show documentation
Show all versions of bcprov-jdk15to18 Show documentation
The Bouncy Castle Crypto package is a Java implementation of cryptographic algorithms. This jar contains JCE provider and lightweight API for the Bouncy Castle Cryptography APIs for JDK 1.5 to JDK 1.8.
package org.bouncycastle.crypto.modes;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.OutputLengthException;
import org.bouncycastle.crypto.engines.ChaCha7539Engine;
import org.bouncycastle.crypto.macs.Poly1305;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Pack;
public class ChaCha20Poly1305
implements AEADCipher
{
private static final class State
{
static final int UNINITIALIZED = 0;
static final int ENC_INIT = 1;
static final int ENC_AAD = 2;
static final int ENC_DATA = 3;
static final int ENC_FINAL = 4;
static final int DEC_INIT = 5;
static final int DEC_AAD = 6;
static final int DEC_DATA = 7;
static final int DEC_FINAL = 8;
}
private static final int BUF_SIZE = 64;
private static final int KEY_SIZE = 32;
private static final int NONCE_SIZE = 12;
private static final int MAC_SIZE = 16;
private static final byte[] ZEROES = new byte[MAC_SIZE - 1];
private static final long AAD_LIMIT = Long.MAX_VALUE - Long.MIN_VALUE;
private static final long DATA_LIMIT = ((1L << 32) - 1) * 64;
private final ChaCha7539Engine chacha20;
private final Mac poly1305;
private final byte[] key = new byte[KEY_SIZE];
private final byte[] nonce = new byte[NONCE_SIZE];
private final byte[] buf = new byte[BUF_SIZE + MAC_SIZE];
private final byte[] mac = new byte[MAC_SIZE];
private byte[] initialAAD;
private long aadCount;
private long dataCount;
private int state = State.UNINITIALIZED;
private int bufPos;
public ChaCha20Poly1305()
{
this(new Poly1305());
}
public ChaCha20Poly1305(Mac poly1305)
{
if (null == poly1305)
{
throw new NullPointerException("'poly1305' cannot be null");
}
if (MAC_SIZE != poly1305.getMacSize())
{
throw new IllegalArgumentException("'poly1305' must be a 128-bit MAC");
}
this.chacha20 = new ChaCha7539Engine();
this.poly1305 = poly1305;
}
public String getAlgorithmName()
{
return "ChaCha20Poly1305";
}
public void init(boolean forEncryption, CipherParameters params) throws IllegalArgumentException
{
KeyParameter initKeyParam;
byte[] initNonce;
CipherParameters chacha20Params;
if (params instanceof AEADParameters)
{
AEADParameters aeadParams = (AEADParameters)params;
int macSizeBits = aeadParams.getMacSize();
if ((MAC_SIZE * 8) != macSizeBits)
{
throw new IllegalArgumentException("Invalid value for MAC size: " + macSizeBits);
}
initKeyParam = aeadParams.getKey();
initNonce = aeadParams.getNonce();
chacha20Params = new ParametersWithIV(initKeyParam, initNonce);
this.initialAAD = aeadParams.getAssociatedText();
}
else if (params instanceof ParametersWithIV)
{
ParametersWithIV ivParams = (ParametersWithIV)params;
initKeyParam = (KeyParameter)ivParams.getParameters();
initNonce = ivParams.getIV();
chacha20Params = ivParams;
this.initialAAD = null;
}
else
{
throw new IllegalArgumentException("invalid parameters passed to ChaCha20Poly1305");
}
// Validate key
if (null == initKeyParam)
{
if (State.UNINITIALIZED == state)
{
throw new IllegalArgumentException("Key must be specified in initial init");
}
}
else
{
if (KEY_SIZE != initKeyParam.getKeyLength())
{
throw new IllegalArgumentException("Key must be 256 bits");
}
}
// Validate nonce
if (null == initNonce || NONCE_SIZE != initNonce.length)
{
throw new IllegalArgumentException("Nonce must be 96 bits");
}
// Check for encryption with reused nonce
if (State.UNINITIALIZED != state && forEncryption && Arrays.areEqual(nonce, initNonce))
{
if (null == initKeyParam || Arrays.areEqual(key, initKeyParam.getKey()))
{
throw new IllegalArgumentException("cannot reuse nonce for ChaCha20Poly1305 encryption");
}
}
if (null != initKeyParam)
{
initKeyParam.copyTo(key, 0, KEY_SIZE);
}
System.arraycopy(initNonce, 0, nonce, 0, NONCE_SIZE);
chacha20.init(true, chacha20Params);
this.state = forEncryption ? State.ENC_INIT : State.DEC_INIT;
reset(true, false);
}
public int getOutputSize(int len)
{
int total = Math.max(0, len) + bufPos;
switch (state)
{
case State.DEC_INIT:
case State.DEC_AAD:
case State.DEC_DATA:
return Math.max(0, total - MAC_SIZE);
case State.ENC_INIT:
case State.ENC_AAD:
case State.ENC_DATA:
return total + MAC_SIZE;
default:
throw new IllegalStateException();
}
}
public int getUpdateOutputSize(int len)
{
int total = Math.max(0, len) + bufPos;
switch (state)
{
case State.DEC_INIT:
case State.DEC_AAD:
case State.DEC_DATA:
total = Math.max(0, total - MAC_SIZE);
break;
case State.ENC_INIT:
case State.ENC_AAD:
case State.ENC_DATA:
break;
default:
throw new IllegalStateException();
}
return total - (total % BUF_SIZE);
}
public void processAADByte(byte in)
{
checkAAD();
this.aadCount = incrementCount(aadCount, 1, AAD_LIMIT);
poly1305.update(in);
}
public void processAADBytes(byte[] in, int inOff, int len)
{
if (null == in)
{
throw new NullPointerException("'in' cannot be null");
}
if (inOff < 0)
{
throw new IllegalArgumentException("'inOff' cannot be negative");
}
if (len < 0)
{
throw new IllegalArgumentException("'len' cannot be negative");
}
if (inOff > (in.length - len))
{
throw new DataLengthException("Input buffer too short");
}
checkAAD();
if (len > 0)
{
this.aadCount = incrementCount(aadCount, len, AAD_LIMIT);
poly1305.update(in, inOff, len);
}
}
public int processByte(byte in, byte[] out, int outOff) throws DataLengthException
{
checkData();
switch (state)
{
case State.DEC_DATA:
{
buf[bufPos] = in;
if (++bufPos == buf.length)
{
poly1305.update(buf, 0, BUF_SIZE);
processData(buf, 0, BUF_SIZE, out, outOff);
System.arraycopy(buf, BUF_SIZE, buf, 0, MAC_SIZE);
this.bufPos = MAC_SIZE;
return BUF_SIZE;
}
return 0;
}
case State.ENC_DATA:
{
buf[bufPos] = in;
if (++bufPos == BUF_SIZE)
{
processData(buf, 0, BUF_SIZE, out, outOff);
poly1305.update(out, outOff, BUF_SIZE);
this.bufPos = 0;
return BUF_SIZE;
}
return 0;
}
default:
throw new IllegalStateException();
}
}
public int processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) throws DataLengthException
{
if (null == in)
{
throw new NullPointerException("'in' cannot be null");
}
/*
* The BC provider can pass null when it expects no output (e.g. based on a
* getUpdateOutputSize call).
*
* See https://github.com/bcgit/bc-java/issues/674
*/
if (null == out)
{
// throw new NullPointerException("'out' cannot be null");
}
if (inOff < 0)
{
throw new IllegalArgumentException("'inOff' cannot be negative");
}
if (len < 0)
{
throw new IllegalArgumentException("'len' cannot be negative");
}
if (inOff > (in.length - len))
{
throw new DataLengthException("Input buffer too short");
}
if (outOff < 0)
{
throw new IllegalArgumentException("'outOff' cannot be negative");
}
checkData();
int resultLen = 0;
switch (state)
{
case State.DEC_DATA:
{
for (int i = 0; i < len; ++i)
{
buf[bufPos] = in[inOff + i];
if (++bufPos == buf.length)
{
poly1305.update(buf, 0, BUF_SIZE);
processData(buf, 0, BUF_SIZE, out, outOff + resultLen);
System.arraycopy(buf, BUF_SIZE, buf, 0, MAC_SIZE);
this.bufPos = MAC_SIZE;
resultLen += BUF_SIZE;
}
}
break;
}
case State.ENC_DATA:
{
if (bufPos != 0)
{
while (len > 0)
{
--len;
buf[bufPos] = in[inOff++];
if (++bufPos == BUF_SIZE)
{
processData(buf, 0, BUF_SIZE, out, outOff);
poly1305.update(out, outOff, BUF_SIZE);
this.bufPos = 0;
resultLen = BUF_SIZE;
break;
}
}
}
while (len >= BUF_SIZE)
{
processData(in, inOff, BUF_SIZE, out, outOff + resultLen);
poly1305.update(out, outOff + resultLen, BUF_SIZE);
inOff += BUF_SIZE;
len -= BUF_SIZE;
resultLen += BUF_SIZE;
}
if (len > 0)
{
System.arraycopy(in, inOff, buf, 0, len);
this.bufPos = len;
}
break;
}
default:
throw new IllegalStateException();
}
return resultLen;
}
public int doFinal(byte[] out, int outOff) throws IllegalStateException, InvalidCipherTextException
{
if (null == out)
{
throw new NullPointerException("'out' cannot be null");
}
if (outOff < 0)
{
throw new IllegalArgumentException("'outOff' cannot be negative");
}
checkData();
Arrays.clear(mac);
int resultLen = 0;
switch (state)
{
case State.DEC_DATA:
{
if (bufPos < MAC_SIZE)
{
throw new InvalidCipherTextException("data too short");
}
resultLen = bufPos - MAC_SIZE;
if (outOff > (out.length - resultLen))
{
throw new OutputLengthException("Output buffer too short");
}
if (resultLen > 0)
{
poly1305.update(buf, 0, resultLen);
processData(buf, 0, resultLen, out, outOff);
}
finishData(State.DEC_FINAL);
if (!Arrays.constantTimeAreEqual(MAC_SIZE, mac, 0, buf, resultLen))
{
throw new InvalidCipherTextException("mac check in ChaCha20Poly1305 failed");
}
break;
}
case State.ENC_DATA:
{
resultLen = bufPos + MAC_SIZE;
if (outOff > (out.length - resultLen))
{
throw new OutputLengthException("Output buffer too short");
}
if (bufPos > 0)
{
processData(buf, 0, bufPos, out, outOff);
poly1305.update(out, outOff, bufPos);
}
finishData(State.ENC_FINAL);
System.arraycopy(mac, 0, out, outOff + bufPos, MAC_SIZE);
break;
}
default:
throw new IllegalStateException();
}
reset(false, true);
return resultLen;
}
public byte[] getMac()
{
return Arrays.clone(mac);
}
public void reset()
{
reset(true, true);
}
private void checkAAD()
{
switch (state)
{
case State.DEC_INIT:
this.state = State.DEC_AAD;
break;
case State.ENC_INIT:
this.state = State.ENC_AAD;
break;
case State.DEC_AAD:
case State.ENC_AAD:
break;
case State.ENC_FINAL:
throw new IllegalStateException("ChaCha20Poly1305 cannot be reused for encryption");
default:
throw new IllegalStateException();
}
}
private void checkData()
{
switch (state)
{
case State.DEC_INIT:
case State.DEC_AAD:
finishAAD(State.DEC_DATA);
break;
case State.ENC_INIT:
case State.ENC_AAD:
finishAAD(State.ENC_DATA);
break;
case State.DEC_DATA:
case State.ENC_DATA:
break;
case State.ENC_FINAL:
throw new IllegalStateException("ChaCha20Poly1305 cannot be reused for encryption");
default:
throw new IllegalStateException();
}
}
private void finishAAD(int nextState)
{
padMAC(aadCount);
this.state = nextState;
}
private void finishData(int nextState)
{
padMAC(dataCount);
byte[] lengths = new byte[16];
Pack.longToLittleEndian(aadCount, lengths, 0);
Pack.longToLittleEndian(dataCount, lengths, 8);
poly1305.update(lengths, 0, 16);
poly1305.doFinal(mac, 0);
this.state = nextState;
}
private long incrementCount(long count, int increment, long limit)
{
if (count + Long.MIN_VALUE > (limit - increment) + Long.MIN_VALUE)
{
throw new IllegalStateException("Limit exceeded");
}
return count + increment;
}
private void initMAC()
{
byte[] firstBlock = new byte[64];
try
{
chacha20.processBytes(firstBlock, 0, 64, firstBlock, 0);
poly1305.init(new KeyParameter(firstBlock, 0, 32));
}
finally
{
Arrays.clear(firstBlock);
}
}
private void padMAC(long count)
{
int partial = (int)count & (MAC_SIZE - 1);
if (0 != partial)
{
poly1305.update(ZEROES, 0, MAC_SIZE - partial);
}
}
private void processData(byte[] in, int inOff, int inLen, byte[] out, int outOff)
{
if (outOff > (out.length - inLen))
{
throw new OutputLengthException("Output buffer too short");
}
chacha20.processBytes(in, inOff, inLen, out, outOff);
this.dataCount = incrementCount(dataCount, inLen, DATA_LIMIT);
}
private void reset(boolean clearMac, boolean resetCipher)
{
Arrays.clear(buf);
if (clearMac)
{
Arrays.clear(mac);
}
this.aadCount = 0L;
this.dataCount = 0L;
this.bufPos = 0;
switch (state)
{
case State.DEC_INIT:
case State.ENC_INIT:
break;
case State.DEC_AAD:
case State.DEC_DATA:
case State.DEC_FINAL:
this.state = State.DEC_INIT;
break;
case State.ENC_AAD:
case State.ENC_DATA:
case State.ENC_FINAL:
this.state = State.ENC_FINAL;
return;
default:
throw new IllegalStateException();
}
if (resetCipher)
{
chacha20.reset();
}
initMAC();
if (null != initialAAD)
{
processAADBytes(initialAAD, 0, initialAAD.length);
}
}
}