GordianKangarooDigest.java
/*
* GordianKnot: Security Suite
* Copyright 2012-2026. Tony Washer
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package io.github.tonywasher.joceanus.gordianknot.impl.ext.digests;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianLength;
import io.github.tonywasher.joceanus.gordianknot.impl.ext.params.GordianKeccakParameters;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.Xof;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Pack;
/**
* Kangaroo. Donated to BouncyCastle.
*/
public abstract class GordianKangarooDigest
implements Digest {
/**
* Default digest length.
*/
private static final int DIGESTLEN = 32;
/**
* Private constructor.
*/
private GordianKangarooDigest() {
}
/**
* KangarooTwelve.
*/
public static class GordianKangarooTwelve
extends GordianKangarooBase {
/**
* # of rounds.
*/
private static final int ROUNDS = 12;
/**
* Constructor.
*/
public GordianKangarooTwelve() {
this(DIGESTLEN);
}
/**
* Constructor.
*
* @param pLength the digest length
*/
public GordianKangarooTwelve(final int pLength) {
super(GordianLength.LEN_128.getLength(), ROUNDS, pLength);
}
@Override
public String getAlgorithmName() {
return getClass().getSimpleName();
}
}
/**
* MarsupilamiFourteen.
*/
public static class GordianMarsupilamiFourteen
extends GordianKangarooBase {
/**
* # of rounds.
*/
private static final int ROUNDS = 14;
/**
* Constructor.
*/
public GordianMarsupilamiFourteen() {
this(DIGESTLEN);
}
/**
* Constructor.
*
* @param pLength the digest length
*/
public GordianMarsupilamiFourteen(final int pLength) {
super(GordianLength.LEN_256.getLength(), ROUNDS, pLength);
}
@Override
public String getAlgorithmName() {
return getClass().getSimpleName();
}
}
/**
* The Kangaroo Base.
*/
public abstract static class GordianKangarooBase
implements Digest, Xof {
/**
* Block Size.
*/
private static final int BLKSIZE = 8192;
/**
* Single marker.
*/
private static final byte[] SINGLE = new byte[]{7};
/**
* Intermediate marker.
*/
private static final byte[] INTERMEDIATE = new byte[]{0xb};
/**
* Final marker.
*/
private static final byte[] FINAL = new byte[]{-1, -1, 6};
/**
* First marker.
*/
private static final byte[] FIRST = new byte[]{3, 0, 0, 0, 0, 0, 0, 0};
/**
* The single byte buffer.
*/
private final byte[] singleByte = new byte[1];
/**
* The Tree Sponge.
*/
private final GordianKangarooSponge theTree;
/**
* The Leaf Sponge.
*/
private final GordianKangarooSponge theLeaf;
/**
* The chain length.
*/
private final int theChainLen;
/**
* The personalisation.
*/
private byte[] thePersonal;
/**
* Are we squeezing?.
*/
private boolean squeezing;
/**
* The current node.
*/
private int theCurrNode;
/**
* The data processed in the current node.
*/
private int theProcessed;
/**
* Constructor.
*
* @param pStrength the strength
* @param pRounds the rounds.
* @param pLength the digest length
*/
GordianKangarooBase(final int pStrength,
final int pRounds,
final int pLength) {
/* Create underlying digests */
theTree = new GordianKangarooSponge(pStrength, pRounds);
theLeaf = new GordianKangarooSponge(pStrength, pRounds);
theChainLen = pStrength >> 2;
/* Build personalisation */
buildPersonal(null);
}
/**
* Constructor.
*
* @param pPersonal the personalisation
*/
private void buildPersonal(final byte[] pPersonal) {
/* Build personalisation */
final int myLen = pPersonal == null ? 0 : pPersonal.length;
final byte[] myEnc = lengthEncode(myLen);
thePersonal = pPersonal == null
? new byte[myLen + myEnc.length]
: Arrays.copyOf(pPersonal, myLen + myEnc.length);
System.arraycopy(myEnc, 0, thePersonal, myLen, myEnc.length);
}
@Override
public int getByteLength() {
return theTree.theRateBytes;
}
@Override
public int getDigestSize() {
//return theXofLen == 0 ? theChainLen >> 1 : (int) theXofLen;
return theChainLen >> 1;
}
/**
* Initialise the digest.
*
* @param pParams the parameters
*/
public void init(final GordianKeccakParameters pParams) {
/* Build the new personalisation */
buildPersonal(pParams.getPersonalisation());
/* Reset everything */
reset();
}
@Override
public void update(final byte pIn) {
singleByte[0] = pIn;
update(singleByte, 0, 1);
}
@Override
public void update(final byte[] pIn,
final int pInOff,
final int pLen) {
processData(pIn, pInOff, pLen);
}
@Override
public int doFinal(final byte[] pOut,
final int pOutOffset) {
/* finalise the digest */
return doFinal(pOut, pOutOffset, getDigestSize());
}
@Override
public int doFinal(final byte[] pOut,
final int pOutOffset,
final int pOutLen) {
/* Build the required output */
final int length = doOutput(pOut, pOutOffset, pOutLen);
/* reset the underlying digest and return the length */
reset();
return length;
}
@Override
public int doOutput(final byte[] pOut,
final int pOutOffset,
final int pOutLen) {
/* If we are not currently squeezing, switch to squeezing */
if (!squeezing) {
switchToSqueezing();
}
/* Reject if there is insufficient Xof remaining */
if (pOutLen < 0) {
throw new IllegalArgumentException("Invalid output length");
}
/* Squeeze out the data and return the length */
theTree.squeeze(pOut, pOutOffset, pOutLen);
return pOutLen;
}
/**
* Process data.
*
* @param pIn the input buffer
* @param pInOffSet the starting offset in the input buffer
* @param pLen the length of data to process
*/
private void processData(final byte[] pIn,
final int pInOffSet,
final int pLen) {
/* Check validity */
if (squeezing) {
throw new IllegalStateException("attempt to absorb while squeezing");
}
/* Determine current sponge */
final GordianKangarooSponge mySponge = theCurrNode == 0 ? theTree : theLeaf;
/* Determine space in current block */
final int mySpace = BLKSIZE - theProcessed;
/* If all data can be processed by the current sponge*/
if (mySpace >= pLen) {
/* Absorb and return */
mySponge.absorb(pIn, pInOffSet, pLen);
theProcessed += pLen;
return;
}
/* Absorb as much as possible into current sponge */
if (mySpace > 0) {
mySponge.absorb(pIn, pInOffSet, mySpace);
theProcessed += mySpace;
}
/* Loop while we have data remaining */
int myProcessed = mySpace;
while (myProcessed < pLen) {
/* Switch Leaf if the current sponge is full */
if (theProcessed == BLKSIZE) {
switchLeaf(true);
}
/* Process next block */
final int myDataLen = Math.min(pLen - myProcessed, BLKSIZE);
theLeaf.absorb(pIn, pInOffSet + myProcessed, myDataLen);
theProcessed += myDataLen;
myProcessed += myDataLen;
}
}
@Override
public void reset() {
theTree.initSponge();
theLeaf.initSponge();
theCurrNode = 0;
theProcessed = 0;
squeezing = false;
}
/**
* Complete Leaf.
*
* @param pMoreToCome is there more data to come? true/false
*/
private void switchLeaf(final boolean pMoreToCome) {
/* If we are the first node */
if (theCurrNode == 0) {
/* Absorb the padding */
theTree.absorb(FIRST, 0, FIRST.length);
/* else intermediate node */
} else {
/* Absorb intermediate node marker */
theLeaf.absorb(INTERMEDIATE, 0, INTERMEDIATE.length);
/* Complete the node */
final byte[] myHash = new byte[theChainLen];
theLeaf.squeeze(myHash, 0, theChainLen);
theTree.absorb(myHash, 0, theChainLen);
/* Re-init the leaf */
theLeaf.initSponge();
}
/* Switch to next node */
if (pMoreToCome) {
theCurrNode++;
}
theProcessed = 0;
}
/**
* Switch to squeezing.
*/
private void switchToSqueezing() {
/* Absorb the personalisation */
processData(thePersonal, 0, thePersonal.length);
/* Complete the absorption */
if (theCurrNode == 0) {
switchSingle();
} else {
switchFinal();
}
/* Set flag */
squeezing = true;
}
/**
* Switch single node to squeezing.
*/
private void switchSingle() {
/* Absorb single node marker */
theTree.absorb(SINGLE, 0, 1);
/* Switch to squeezing */
theTree.padAndSwitchToSqueezingPhase();
}
/**
* Switch multiple node to squeezing.
*/
private void switchFinal() {
/* Complete the current leaf */
switchLeaf(false);
/* Absorb length */
final byte[] myLength = lengthEncode(theCurrNode);
theTree.absorb(myLength, 0, myLength.length);
/* Absorb final node marker */
theTree.absorb(FINAL, 0, FINAL.length);
/* Switch to squeezing */
theTree.padAndSwitchToSqueezingPhase();
}
/**
* right Encode a length.
*
* @param strLen the length to encode
* @return the encoded length
*/
private static byte[] lengthEncode(final long strLen) {
/* Calculate # of bytes required to hold length */
byte n = 0;
long v = strLen;
if (v != 0) {
n = 1;
while ((v >>= Byte.SIZE) != 0) {
n++;
}
}
/* Allocate byte array and store length */
final byte[] b = new byte[n + 1];
b[n] = n;
/* Encode the length */
for (int i = 0; i < n; i++) {
b[i] = (byte) (strLen >> (Byte.SIZE * (n - i - 1)));
}
/* Return the encoded length */
return b;
}
}
/**
* The Kangaroo Sponge.
*/
@SuppressWarnings("checkstyle:MagicNumber")
private static class GordianKangarooSponge {
/**
* The round constants.
*/
private static long[] keccakRoundConstants = {
0x0000000000000001L, 0x0000000000008082L,
0x800000000000808aL, 0x8000000080008000L, 0x000000000000808bL, 0x0000000080000001L, 0x8000000080008081L,
0x8000000000008009L, 0x000000000000008aL, 0x0000000000000088L, 0x0000000080008009L, 0x000000008000000aL,
0x000000008000808bL, 0x800000000000008bL, 0x8000000000008089L, 0x8000000000008003L, 0x8000000000008002L,
0x8000000000000080L, 0x000000000000800aL, 0x800000008000000aL, 0x8000000080008081L, 0x8000000000008080L,
0x0000000080000001L, 0x8000000080008008L
};
/**
* The number of rounds.
*/
private final int theRounds;
/**
* The rateBytes.
*/
private final int theRateBytes;
/**
* The state.
*/
private final long[] theState = new long[25];
/**
* The queue.
*/
private final byte[] theQueue;
/**
* The numnber of bytes in the queue.
*/
private int bytesInQueue;
/**
* Are we squeezing?
*/
private boolean squeezing;
/**
* Constructor.
*
* @param pStrength the strength
* @param pRounds the rounds.
*/
GordianKangarooSponge(final int pStrength,
final int pRounds) {
theRateBytes = (1600 - (pStrength << 1)) >> 3;
theRounds = pRounds;
theQueue = new byte[theRateBytes];
initSponge();
}
/**
* Initialise the sponge.
*/
private void initSponge() {
Arrays.fill(theState, 0L);
Arrays.fill(theQueue, (byte) 0);
bytesInQueue = 0;
squeezing = false;
}
/**
* Absorb data into sponge.
*
* @param data the data buffer
* @param off the starting offset in the buffer.
* @param len the length of data to absorb
*/
private void absorb(final byte[] data,
final int off,
final int len) {
/* Sanity checks */
if (squeezing) {
throw new IllegalStateException("attempt to absorb while squeezing");
}
/* Loop through bytes */
int count = 0;
while (count < len) {
/* Handle full buffer */
if (bytesInQueue == theRateBytes) {
kangarooAbsorb(theQueue, 0);
bytesInQueue = 0;
}
/* If we have full blocks */
if (bytesInQueue == 0 && count <= (len - theRateBytes)) {
/* Process full blocks */
do {
kangarooAbsorb(data, off + count);
count += theRateBytes;
} while (count <= (len - theRateBytes));
/* else process partial blocks */
} else {
final int partialBlock = Math.min(theRateBytes - bytesInQueue, len - count);
System.arraycopy(data, off + count, theQueue, bytesInQueue, partialBlock);
bytesInQueue += partialBlock;
count += partialBlock;
}
}
}
/**
* Handle padding.
*/
private void padAndSwitchToSqueezingPhase() {
/* Fill any remaining space in queue with zeroes */
for (int i = bytesInQueue; i < theRateBytes; i++) {
theQueue[i] = 0;
}
theQueue[theRateBytes - 1] ^= 0x80;
kangarooAbsorb(theQueue, 0);
kangarooExtract();
bytesInQueue = theRateBytes;
squeezing = true;
}
/**
* Squeeze data out.
*
* @param output the output buffer
* @param offset the offset in the output buffer
* @param outputLength the output length
*/
private void squeeze(final byte[] output,
final int offset,
final int outputLength) {
if (!squeezing) {
padAndSwitchToSqueezingPhase();
}
int i = 0;
while (i < outputLength) {
if (bytesInQueue == 0) {
kangarooPermutation();
kangarooExtract();
bytesInQueue = theRateBytes;
}
final int partialBlock = Math.min(bytesInQueue, outputLength - i);
System.arraycopy(theQueue, theRateBytes - bytesInQueue, output, offset + i, partialBlock);
bytesInQueue -= partialBlock;
i += partialBlock;
}
}
/**
* Absorb a block of data.
*
* @param data the data to absorb
* @param off the starting offset in the data
*/
private void kangarooAbsorb(final byte[] data,
final int off) {
final int count = theRateBytes >> 3;
int offSet = off;
for (int i = 0; i < count; ++i) {
theState[i] ^= Pack.littleEndianToLong(data, offSet);
offSet += 8;
}
kangarooPermutation();
}
/**
* Extract a block of data to the queue.
*/
private void kangarooExtract() {
Pack.longToLittleEndian(theState, 0, theRateBytes >> 3, theQueue, 0);
}
/**
* Permutation (KP).
*/
private void kangarooPermutation() {
final long[] a = theState;
long a00 = a[0];
long a01 = a[1];
long a02 = a[2];
long a03 = a[3];
long a04 = a[4];
long a05 = a[5];
long a06 = a[6];
long a07 = a[7];
long a08 = a[8];
long a09 = a[9];
long a10 = a[10];
long a11 = a[11];
long a12 = a[12];
long a13 = a[13];
long a14 = a[14];
long a15 = a[15];
long a16 = a[16];
long a17 = a[17];
long a18 = a[18];
long a19 = a[19];
long a20 = a[20];
long a21 = a[21];
long a22 = a[22];
long a23 = a[23];
long a24 = a[24];
final int myBase = keccakRoundConstants.length - theRounds;
for (int i = 0; i < theRounds; i++) {
// theta
long c0 = a00 ^ a05 ^ a10 ^ a15 ^ a20;
long c1 = a01 ^ a06 ^ a11 ^ a16 ^ a21;
final long c2 = a02 ^ a07 ^ a12 ^ a17 ^ a22;
final long c3 = a03 ^ a08 ^ a13 ^ a18 ^ a23;
final long c4 = a04 ^ a09 ^ a14 ^ a19 ^ a24;
final long d1 = (c1 << 1 | c1 >>> -1) ^ c4;
final long d2 = (c2 << 1 | c2 >>> -1) ^ c0;
final long d3 = (c3 << 1 | c3 >>> -1) ^ c1;
final long d4 = (c4 << 1 | c4 >>> -1) ^ c2;
final long d0 = (c0 << 1 | c0 >>> -1) ^ c3;
a00 ^= d1;
a05 ^= d1;
a10 ^= d1;
a15 ^= d1;
a20 ^= d1;
a01 ^= d2;
a06 ^= d2;
a11 ^= d2;
a16 ^= d2;
a21 ^= d2;
a02 ^= d3;
a07 ^= d3;
a12 ^= d3;
a17 ^= d3;
a22 ^= d3;
a03 ^= d4;
a08 ^= d4;
a13 ^= d4;
a18 ^= d4;
a23 ^= d4;
a04 ^= d0;
a09 ^= d0;
a14 ^= d0;
a19 ^= d0;
a24 ^= d0;
// rho/pi
c1 = a01 << 1 | a01 >>> 63;
a01 = a06 << 44 | a06 >>> 20;
a06 = a09 << 20 | a09 >>> 44;
a09 = a22 << 61 | a22 >>> 3;
a22 = a14 << 39 | a14 >>> 25;
a14 = a20 << 18 | a20 >>> 46;
a20 = a02 << 62 | a02 >>> 2;
a02 = a12 << 43 | a12 >>> 21;
a12 = a13 << 25 | a13 >>> 39;
a13 = a19 << 8 | a19 >>> 56;
a19 = a23 << 56 | a23 >>> 8;
a23 = a15 << 41 | a15 >>> 23;
a15 = a04 << 27 | a04 >>> 37;
a04 = a24 << 14 | a24 >>> 50;
a24 = a21 << 2 | a21 >>> 62;
a21 = a08 << 55 | a08 >>> 9;
a08 = a16 << 45 | a16 >>> 19;
a16 = a05 << 36 | a05 >>> 28;
a05 = a03 << 28 | a03 >>> 36;
a03 = a18 << 21 | a18 >>> 43;
a18 = a17 << 15 | a17 >>> 49;
a17 = a11 << 10 | a11 >>> 54;
a11 = a07 << 6 | a07 >>> 58;
a07 = a10 << 3 | a10 >>> 61;
a10 = c1;
// chi
c0 = a00 ^ (~a01 & a02);
c1 = a01 ^ (~a02 & a03);
a02 ^= ~a03 & a04;
a03 ^= ~a04 & a00;
a04 ^= ~a00 & a01;
a00 = c0;
a01 = c1;
c0 = a05 ^ (~a06 & a07);
c1 = a06 ^ (~a07 & a08);
a07 ^= ~a08 & a09;
a08 ^= ~a09 & a05;
a09 ^= ~a05 & a06;
a05 = c0;
a06 = c1;
c0 = a10 ^ (~a11 & a12);
c1 = a11 ^ (~a12 & a13);
a12 ^= ~a13 & a14;
a13 ^= ~a14 & a10;
a14 ^= ~a10 & a11;
a10 = c0;
a11 = c1;
c0 = a15 ^ (~a16 & a17);
c1 = a16 ^ (~a17 & a18);
a17 ^= ~a18 & a19;
a18 ^= ~a19 & a15;
a19 ^= ~a15 & a16;
a15 = c0;
a16 = c1;
c0 = a20 ^ (~a21 & a22);
c1 = a21 ^ (~a22 & a23);
a22 ^= ~a23 & a24;
a23 ^= ~a24 & a20;
a24 ^= ~a20 & a21;
a20 = c0;
a21 = c1;
// iota
a00 ^= keccakRoundConstants[myBase + i];
}
a[0] = a00;
a[1] = a01;
a[2] = a02;
a[3] = a03;
a[4] = a04;
a[5] = a05;
a[6] = a06;
a[7] = a07;
a[8] = a08;
a[9] = a09;
a[10] = a10;
a[11] = a11;
a[12] = a12;
a[13] = a13;
a[14] = a14;
a[15] = a15;
a[16] = a16;
a[17] = a17;
a[18] = a18;
a[19] = a19;
a[20] = a20;
a[21] = a21;
a[22] = a22;
a[23] = a23;
a[24] = a24;
}
}
}