package org.bouncycastle.crypto.modes;
import java.util.Vector;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.OutputLengthException;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.Arrays;
An implementation of RFC 7253 on The OCB
Authenticated-Encryption Algorithm, licensed per:
License for
Open-Source Software Implementations of OCB (Jan 9, 2013) — “License 1”
Under this license, you are authorized to make, use, and distribute open-source software
implementations of OCB. This license terminates for you if you sue someone over their open-source
software implementation of OCB claiming that you have a patent covering their implementation.
This is a non-binding summary of a legal document (the link above). The parameters of the license
are specified in the license document and that document is controlling.
/**
* An implementation of <a href="http://tools.ietf.org/html/rfc7253">RFC 7253 on The OCB
* Authenticated-Encryption Algorithm</a>, licensed per:
* <p>
* <blockquote> <a href="http://www.cs.ucdavis.edu/~rogaway/ocb/license1.pdf">License for
* Open-Source Software Implementations of OCB</a> (Jan 9, 2013) — “License 1” <br>
* Under this license, you are authorized to make, use, and distribute open-source software
* implementations of OCB. This license terminates for you if you sue someone over their open-source
* software implementation of OCB claiming that you have a patent covering their implementation.
* <p>
* This is a non-binding summary of a legal document (the link above). The parameters of the license
* are specified in the license document and that document is controlling. </blockquote>
*/
public class OCBBlockCipher
implements AEADBlockCipher
{
private static final int BLOCK_SIZE = 16;
private BlockCipher hashCipher;
private BlockCipher mainCipher;
/*
* CONFIGURATION
*/
private boolean forEncryption;
private int macSize;
private byte[] initialAssociatedText;
/*
* KEY-DEPENDENT
*/
// NOTE: elements are lazily calculated
private Vector L;
private byte[] L_Asterisk, L_Dollar;
/*
* NONCE-DEPENDENT
*/
private byte[] KtopInput = null;
private byte[] Stretch = new byte[24];
private byte[] OffsetMAIN_0 = new byte[16];
/*
* PER-ENCRYPTION/DECRYPTION
*/
private byte[] hashBlock, mainBlock;
private int hashBlockPos, mainBlockPos;
private long hashBlockCount, mainBlockCount;
private byte[] OffsetHASH;
private byte[] Sum;
private byte[] OffsetMAIN = new byte[16];
private byte[] Checksum;
// NOTE: The MAC value is preserved after doFinal
private byte[] macBlock;
public OCBBlockCipher(BlockCipher hashCipher, BlockCipher mainCipher)
{
if (hashCipher == null)
{
throw new IllegalArgumentException("'hashCipher' cannot be null");
}
if (hashCipher.getBlockSize() != BLOCK_SIZE)
{
throw new IllegalArgumentException("'hashCipher' must have a block size of "
+ BLOCK_SIZE);
}
if (mainCipher == null)
{
throw new IllegalArgumentException("'mainCipher' cannot be null");
}
if (mainCipher.getBlockSize() != BLOCK_SIZE)
{
throw new IllegalArgumentException("'mainCipher' must have a block size of "
+ BLOCK_SIZE);
}
if (!hashCipher.getAlgorithmName().equals(mainCipher.getAlgorithmName()))
{
throw new IllegalArgumentException(
"'hashCipher' and 'mainCipher' must be the same algorithm");
}
this.hashCipher = hashCipher;
this.mainCipher = mainCipher;
}
public BlockCipher getUnderlyingCipher()
{
return mainCipher;
}
public String getAlgorithmName()
{
return mainCipher.getAlgorithmName() + "/OCB";
}
public void init(boolean forEncryption, CipherParameters parameters)
throws IllegalArgumentException
{
boolean oldForEncryption = this.forEncryption;
this.forEncryption = forEncryption;
this.macBlock = null;
KeyParameter keyParameter;
byte[] N;
if (parameters instanceof AEADParameters)
{
AEADParameters aeadParameters = (AEADParameters)parameters;
N = aeadParameters.getNonce();
initialAssociatedText = aeadParameters.getAssociatedText();
int macSizeBits = aeadParameters.getMacSize();
if (macSizeBits < 64 || macSizeBits > 128 || macSizeBits % 8 != 0)
{
throw new IllegalArgumentException("Invalid value for MAC size: " + macSizeBits);
}
macSize = macSizeBits / 8;
keyParameter = aeadParameters.getKey();
}
else if (parameters instanceof ParametersWithIV)
{
ParametersWithIV parametersWithIV = (ParametersWithIV)parameters;
N = parametersWithIV.getIV();
initialAssociatedText = null;
macSize = 16;
keyParameter = (KeyParameter)parametersWithIV.getParameters();
}
else
{
throw new IllegalArgumentException("invalid parameters passed to OCB");
}
this.hashBlock = new byte[16];
this.mainBlock = new byte[forEncryption ? BLOCK_SIZE : (BLOCK_SIZE + macSize)];
if (N == null)
{
N = new byte[0];
}
if (N.length > 15)
{
throw new IllegalArgumentException("IV must be no more than 15 bytes");
}
/*
* KEY-DEPENDENT INITIALISATION
*/
if (keyParameter != null)
{
// hashCipher always used in forward mode
hashCipher.init(true, keyParameter);
mainCipher.init(forEncryption, keyParameter);
KtopInput = null;
}
else if (oldForEncryption != forEncryption)
{
throw new IllegalArgumentException("cannot change encrypting state without providing key.");
}
this.L_Asterisk = new byte[16];
hashCipher.processBlock(L_Asterisk, 0, L_Asterisk, 0);
this.L_Dollar = OCB_double(L_Asterisk);
this.L = new Vector();
this.L.addElement(OCB_double(L_Dollar));
/*
* NONCE-DEPENDENT AND PER-ENCRYPTION/DECRYPTION INITIALISATION
*/
int bottom = processNonce(N);
int bits = bottom % 8, bytes = bottom / 8;
if (bits == 0)
{
System.arraycopy(Stretch, bytes, OffsetMAIN_0, 0, 16);
}
else
{
for (int i = 0; i < 16; ++i)
{
int b1 = Stretch[bytes] & 0xff;
int b2 = Stretch[++bytes] & 0xff;
this.OffsetMAIN_0[i] = (byte)((b1 << bits) | (b2 >>> (8 - bits)));
}
}
this.hashBlockPos = 0;
this.mainBlockPos = 0;
this.hashBlockCount = 0;
this.mainBlockCount = 0;
this.OffsetHASH = new byte[16];
this.Sum = new byte[16];
System.arraycopy(this.OffsetMAIN_0, 0, this.OffsetMAIN, 0, 16);
this.Checksum = new byte[16];
if (initialAssociatedText != null)
{
processAADBytes(initialAssociatedText, 0, initialAssociatedText.length);
}
}
protected int processNonce(byte[] N)
{
byte[] nonce = new byte[16];
System.arraycopy(N, 0, nonce, nonce.length - N.length, N.length);
nonce[0] = (byte)(macSize << 4);
nonce[15 - N.length] |= 1;
int bottom = nonce[15] & 0x3F;
nonce[15] &= 0xC0;
/*
* When used with incrementing nonces, the cipher is only applied once every 64 inits.
*/
if (KtopInput == null || !Arrays.areEqual(nonce, KtopInput))
{
byte[] Ktop = new byte[16];
KtopInput = nonce;
hashCipher.processBlock(KtopInput, 0, Ktop, 0);
System.arraycopy(Ktop, 0, Stretch, 0, 16);
for (int i = 0; i < 8; ++i)
{
Stretch[16 + i] = (byte)(Ktop[i] ^ Ktop[i + 1]);
}
}
return bottom;
}
public byte[] getMac()
{
if (macBlock == null)
{
return new byte[macSize];
}
return Arrays.clone(macBlock);
}
public int getOutputSize(int len)
{
int totalData = len + mainBlockPos;
if (forEncryption)
{
return totalData + macSize;
}
return totalData < macSize ? 0 : totalData - macSize;
}
public int getUpdateOutputSize(int len)
{
int totalData = len + mainBlockPos;
if (!forEncryption)
{
if (totalData < macSize)
{
return 0;
}
totalData -= macSize;
}
return totalData - totalData % BLOCK_SIZE;
}
public void processAADByte(byte input)
{
hashBlock[hashBlockPos] = input;
if (++hashBlockPos == hashBlock.length)
{
processHashBlock();
}
}
public void processAADBytes(byte[] input, int off, int len)
{
for (int i = 0; i < len; ++i)
{
hashBlock[hashBlockPos] = input[off + i];
if (++hashBlockPos == hashBlock.length)
{
processHashBlock();
}
}
}
public int processByte(byte input, byte[] output, int outOff)
throws DataLengthException
{
mainBlock[mainBlockPos] = input;
if (++mainBlockPos == mainBlock.length)
{
processMainBlock(output, outOff);
return BLOCK_SIZE;
}
return 0;
}
public int processBytes(byte[] input, int inOff, int len, byte[] output, int outOff)
throws DataLengthException
{
if (input.length < (inOff + len))
{
throw new DataLengthException("Input buffer too short");
}
int resultLen = 0;
for (int i = 0; i < len; ++i)
{
mainBlock[mainBlockPos] = input[inOff + i];
if (++mainBlockPos == mainBlock.length)
{
processMainBlock(output, outOff + resultLen);
resultLen += BLOCK_SIZE;
}
}
return resultLen;
}
public int doFinal(byte[] output, int outOff)
throws IllegalStateException,
InvalidCipherTextException
{
/*
* For decryption, get the tag from the end of the message
*/
byte[] tag = null;
if (!forEncryption)
{
if (mainBlockPos < macSize)
{
throw new InvalidCipherTextException("data too short");
}
mainBlockPos -= macSize;
tag = new byte[macSize];
System.arraycopy(mainBlock, mainBlockPos, tag, 0, macSize);
}
/*
* HASH: Process any final partial block; compute final hash value
*/
if (hashBlockPos > 0)
{
OCB_extend(hashBlock, hashBlockPos);
updateHASH(L_Asterisk);
}
/*
* OCB-ENCRYPT/OCB-DECRYPT: Process any final partial block
*/
if (mainBlockPos > 0)
{
if (forEncryption)
{
OCB_extend(mainBlock, mainBlockPos);
xor(Checksum, mainBlock);
}
xor(OffsetMAIN, L_Asterisk);
byte[] Pad = new byte[16];
hashCipher.processBlock(OffsetMAIN, 0, Pad, 0);
xor(mainBlock, Pad);
if (output.length < (outOff + mainBlockPos))
{
throw new OutputLengthException("Output buffer too short");
}
System.arraycopy(mainBlock, 0, output, outOff, mainBlockPos);
if (!forEncryption)
{
OCB_extend(mainBlock, mainBlockPos);
xor(Checksum, mainBlock);
}
}
/*
* OCB-ENCRYPT/OCB-DECRYPT: Compute raw tag
*/
xor(Checksum, OffsetMAIN);
xor(Checksum, L_Dollar);
hashCipher.processBlock(Checksum, 0, Checksum, 0);
xor(Checksum, Sum);
this.macBlock = new byte[macSize];
System.arraycopy(Checksum, 0, macBlock, 0, macSize);
/*
* Validate or append tag and reset this cipher for the next run
*/
int resultLen = mainBlockPos;
if (forEncryption)
{
if (output.length < (outOff + resultLen + macSize))
{
throw new OutputLengthException("Output buffer too short");
}
// Append tag to the message
System.arraycopy(macBlock, 0, output, outOff + resultLen, macSize);
resultLen += macSize;
}
else
{
// Compare the tag from the message with the calculated one
if (!Arrays.constantTimeAreEqual(macBlock, tag))
{
throw new InvalidCipherTextException("mac check in OCB failed");
}
}
reset(false);
return resultLen;
}
public void reset()
{
reset(true);
}
protected void clear(byte[] bs)
{
if (bs != null)
{
Arrays.fill(bs, (byte)0);
}
}
protected byte[] getLSub(int n)
{
while (n >= L.size())
{
L.addElement(OCB_double((byte[])L.lastElement()));
}
return (byte[])L.elementAt(n);
}
protected void processHashBlock()
{
/*
* HASH: Process any whole blocks
*/
updateHASH(getLSub(OCB_ntz(++hashBlockCount)));
hashBlockPos = 0;
}
protected void processMainBlock(byte[] output, int outOff)
{
if (output.length < (outOff + BLOCK_SIZE))
{
throw new OutputLengthException("Output buffer too short");
}
/*
* OCB-ENCRYPT/OCB-DECRYPT: Process any whole blocks
*/
if (forEncryption)
{
xor(Checksum, mainBlock);
mainBlockPos = 0;
}
xor(OffsetMAIN, getLSub(OCB_ntz(++mainBlockCount)));
xor(mainBlock, OffsetMAIN);
mainCipher.processBlock(mainBlock, 0, mainBlock, 0);
xor(mainBlock, OffsetMAIN);
System.arraycopy(mainBlock, 0, output, outOff, 16);
if (!forEncryption)
{
xor(Checksum, mainBlock);
System.arraycopy(mainBlock, BLOCK_SIZE, mainBlock, 0, macSize);
mainBlockPos = macSize;
}
}
protected void reset(boolean clearMac)
{
hashCipher.reset();
mainCipher.reset();
clear(hashBlock);
clear(mainBlock);
hashBlockPos = 0;
mainBlockPos = 0;
hashBlockCount = 0;
mainBlockCount = 0;
clear(OffsetHASH);
clear(Sum);
System.arraycopy(OffsetMAIN_0, 0, OffsetMAIN, 0, 16);
clear(Checksum);
if (clearMac)
{
macBlock = null;
}
if (initialAssociatedText != null)
{
processAADBytes(initialAssociatedText, 0, initialAssociatedText.length);
}
}
protected void updateHASH(byte[] LSub)
{
xor(OffsetHASH, LSub);
xor(hashBlock, OffsetHASH);
hashCipher.processBlock(hashBlock, 0, hashBlock, 0);
xor(Sum, hashBlock);
}
protected static byte[] OCB_double(byte[] block)
{
byte[] result = new byte[16];
int carry = shiftLeft(block, result);
/*
* NOTE: This construction is an attempt at a constant-time implementation.
*/
result[15] ^= (0x87 >>> ((1 - carry) << 3));
return result;
}
protected static void OCB_extend(byte[] block, int pos)
{
block[pos] = (byte)0x80;
while (++pos < 16)
{
block[pos] = 0;
}
}
protected static int OCB_ntz(long x)
{
if (x == 0)
{
return 64;
}
int n = 0;
while ((x & 1L) == 0L)
{
++n;
x >>>= 1;
}
return n;
}
protected static int shiftLeft(byte[] block, byte[] output)
{
int i = 16;
int bit = 0;
while (--i >= 0)
{
int b = block[i] & 0xff;
output[i] = (byte)((b << 1) | bit);
bit = (b >>> 7) & 1;
}
return bit;
}
protected static void xor(byte[] block, byte[] val)
{
for (int i = 15; i >= 0; --i)
{
block[i] ^= val[i];
}
}
}