GordianSkeinBase.java
/*
* GordianKnot: Security Suite
* Copyright 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.impl.ext.params.GordianSkeinParameters;
import org.bouncycastle.crypto.OutputLengthException;
import org.bouncycastle.crypto.engines.ThreefishEngine;
import org.bouncycastle.crypto.macs.SkeinMac;
import org.bouncycastle.crypto.params.SkeinParameters;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Integers;
import org.bouncycastle.util.Memoable;
import org.bouncycastle.util.Pack;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.Vector;
/**
* Implementation of the Skein family of parameterised hash functions in 256, 512 and 1024 bit block
* sizes, based on the {@link ThreefishEngine Threefish} tweakable block cipher.
* <p>
* This is the 1.3 version of Skein defined in the Skein hash function submission to the NIST SHA-3
* competition in October 2010.
* <p>
* Skein was designed by Niels Ferguson - Stefan Lucks - Bruce Schneier - Doug Whiting - Mihir
* Bellare - Tadayoshi Kohno - Jon Callas - Jesse Walker.
* <p>
* This implementation is the basis for SkeinDigest and {@link SkeinMac}, implementing the
* parameter based configuration system that allows Skein to be adapted to multiple applications. <br>
* Initialising the engine with {@link GordianSkeinParameters} allows standard and arbitrary parameters to
* be applied during the Skein hash function.
* <p>
* Implemented:
* <ul>
* <li>256, 512 and 1024 bit internal states.</li>
* <li>Full 96 bit input length.</li>
* <li>Parameters defined in the Skein specification, and arbitrary other pre and post message
* parameters.</li>
* <li>Arbitrary output size in 1 byte intervals.</li>
* </ul>
* <p>
* Not implemented:
* <ul>
* <li>Sub-byte length input (bit padding).</li>
* <li>Tree hashing.</li>
* </ul>
*
* @see GordianSkeinParameters
*/
@SuppressWarnings("checkstyle:MagicNumber")
public class GordianSkeinBase
implements Memoable {
/**
* 256 bit block size - Skein 256.
*/
public static final int SKEIN_256 = ThreefishEngine.BLOCKSIZE_256;
/**
* 512 bit block size - Skein 512.
*/
public static final int SKEIN_512 = ThreefishEngine.BLOCKSIZE_512;
/**
* 1024 bit block size - Skein 1024.
*/
public static final int SKEIN_1024 = ThreefishEngine.BLOCKSIZE_1024;
/**
* Configuration. Minimal at present, but more complex when tree hashing is implemented
*/
static class Configuration {
/**
* Configuration bytes.
*/
private byte[] bytes = new byte[32];
/**
* Constructor.
*
* @param outputSizeBits the output size in bits
*/
Configuration(final long outputSizeBits) {
// 0..3 = ASCII SHA3
bytes[0] = (byte) 'S';
bytes[1] = (byte) 'H';
bytes[2] = (byte) 'A';
bytes[3] = (byte) '3';
// 4..5 = version number in LSB order
bytes[4] = 1;
bytes[5] = 0;
// 8..15 = output length
Pack.longToLittleEndian(outputSizeBits, bytes, 8);
}
/**
* Obtain bytes.
*
* @return the bytes.
*/
public byte[] getBytes() {
return bytes;
}
}
/**
* Parameter class.
*/
public static class Parameter {
/**
* The type.
*/
private final int type;
/**
* The value.
*/
private final byte[] value;
/**
* Constructor.
*
* @param pType the type
* @param pValue the value
*/
public Parameter(final int pType, final byte[] pValue) {
this.type = pType;
this.value = pValue;
}
/**
* Obtain the type.
*
* @return the type
*/
public int getType() {
return type;
}
/**
* Obtain the value.
*
* @return the value
*/
public byte[] getValue() {
return value;
}
}
/**
* The parameter type for the Skein key.
*/
private static final int PARAM_TYPE_KEY = 0;
/**
* The parameter type for the Skein configuration block.
*/
private static final int PARAM_TYPE_CONFIG = 4;
/**
* The parameter type for the message.
*/
private static final int PARAM_TYPE_MESSAGE = 48;
/**
* The parameter type for the output transformation.
*/
private static final int PARAM_TYPE_OUTPUT = 63;
/**
* Precalculated UBI(CFG) states for common state/output combinations without key or other
* pre-message params.
*/
private static final Map<Integer, long[]> INITIAL_STATES = new HashMap<>();
static {
// From Appendix C of the Skein 1.3 NIST submission
final long[] skein256o128 = {
0xe1111906964d7260L,
0x883daaa77c8d811cL,
0x10080df491960f7aL,
0xccf7dde5b45bc1c2L
};
initialState(SKEIN_256, 128, skein256o128);
final long[] skein256o160 = {
0x1420231472825e98L,
0x2ac4e9a25a77e590L,
0xd47a58568838d63eL,
0x2dd2e4968586ab7dL
};
initialState(SKEIN_256, 160, skein256o160);
final long[] skein256o224 = {
0xc6098a8c9ae5ea0bL,
0x876d568608c5191cL,
0x99cb88d7d7f53884L,
0x384bddb1aeddb5deL
};
initialState(SKEIN_256, 224, skein256o224);
final long[] skein256o256 = {
0xfc9da860d048b449L,
0x2fca66479fa7d833L,
0xb33bc3896656840fL,
0x6a54e920fde8da69L
};
initialState(SKEIN_256, 256, skein256o256);
final long[] skein512o128 = {
0xa8bc7bf36fbf9f52L,
0x1e9872cebd1af0aaL,
0x309b1790b32190d3L,
0xbcfbb8543f94805cL,
0x0da61bcd6e31b11bL,
0x1a18ebead46a32e3L,
0xa2cc5b18ce84aa82L,
0x6982ab289d46982dL
};
initialState(SKEIN_512, 128, skein512o128);
final long[] skein512o160 = {
0x28b81a2ae013bd91L,
0xc2f11668b5bdf78fL,
0x1760d8f3f6a56f12L,
0x4fb747588239904fL,
0x21ede07f7eaf5056L,
0xd908922e63ed70b8L,
0xb8ec76ffeccb52faL,
0x01a47bb8a3f27a6eL
};
initialState(SKEIN_512, 160, skein512o160);
final long[] skein512o224 = {
0xccd0616248677224L,
0xcba65cf3a92339efL,
0x8ccd69d652ff4b64L,
0x398aed7b3ab890b4L,
0x0f59d1b1457d2bd0L,
0x6776fe6575d4eb3dL,
0x99fbc70e997413e9L,
0x9e2cfccfe1c41ef7L
};
initialState(SKEIN_512, 224, skein512o224);
final long[] skein512o384 = {
0xa3f6c6bf3a75ef5fL,
0xb0fef9ccfd84faa4L,
0x9d77dd663d770cfeL,
0xd798cbf3b468fddaL,
0x1bc4a6668a0e4465L,
0x7ed7d434e5807407L,
0x548fc1acd4ec44d6L,
0x266e17546aa18ff8L
};
initialState(SKEIN_512, 384, skein512o384);
final long[] skein512o512 = {
0x4903adff749c51ceL,
0x0d95de399746df03L,
0x8fd1934127c79bceL,
0x9a255629ff352cb1L,
0x5db62599df6ca7b0L,
0xeabe394ca9d5c3f4L,
0x991112c71a75b523L,
0xae18a40b660fcc33L
};
initialState(SKEIN_512, 512, skein512o512);
}
private static void initialState(final int blockSize, final int outputSize, final long[] state) {
INITIAL_STATES.put(variantIdentifier(blockSize / 8, outputSize / 8), state);
}
private static Integer variantIdentifier(final int blockSizeBytes, final int outputSizeBytes) {
return Integers.valueOf((outputSizeBytes << 16) | blockSizeBytes);
}
/**
* Ubi Tweak class.
*/
private static class UbiTweak {
/**
* Point at which position might overflow long, so switch to add with carry logic.
*/
private static final long LOW_RANGE = Long.MAX_VALUE - Integer.MAX_VALUE;
/**
* Bit 127 = final.
*/
private static final long T1_FINAL = 1L << 63;
/**
* Bit 126 = first.
*/
private static final long T1_FIRST = 1L << 62;
/**
* UBI uses a 128 bit tweak.
*/
private long[] tweak = new long[2];
/**
* Whether 64 bit position exceeded.
*/
private boolean extendedPosition;
/**
* Constructor.
*/
UbiTweak() {
reset();
}
/**
* Reset according to source.
*
* @param pTweak the source
*/
public void reset(final UbiTweak pTweak) {
this.tweak = Arrays.clone(pTweak.tweak, this.tweak);
this.extendedPosition = pTweak.extendedPosition;
}
/**
* Reset.
*/
public void reset() {
tweak[0] = 0;
tweak[1] = 0;
extendedPosition = false;
setFirst(true);
}
public void setType(final int type) {
// Bits 120..125 = type
tweak[1] = (tweak[1] & 0xFFFFFFC000000000L) | ((type & 0x3FL) << 56);
}
public int getType() {
return (int) ((tweak[1] >>> 56) & 0x3FL);
}
public void setTreeLocation(final int level, final int hiOffset, final long loOffset) {
// Bits 112..119 = level
tweak[1] = (tweak[1] & 0xFF00FFC000000000L) | ((level & 0xFFL) << 48);
/* Calculate high and lo parts of offset */
tweak[0] = loOffset;
tweak[1] |= hiOffset;
/* Determine whether we have an extended position */
extendedPosition = loOffset < 0 || loOffset > LOW_RANGE || hiOffset != 0;
}
public int getLevel() {
return (int) ((tweak[1] >>> 48) & 0xFFL);
}
public void setFirst(final boolean first) {
if (first) {
tweak[1] |= T1_FIRST;
} else {
tweak[1] &= ~T1_FIRST;
}
}
public boolean isFirst() {
return ((tweak[1] & T1_FIRST) != 0);
}
public void setFinal(final boolean last) {
if (last) {
tweak[1] |= T1_FINAL;
} else {
tweak[1] &= ~T1_FINAL;
}
}
public boolean isFinal() {
return ((tweak[1] & T1_FINAL) != 0);
}
/**
* Advances the position in the tweak by the specified value.
*
* @param advance the advance count
*/
public void advancePosition(final int advance) {
// Bits 0..95 = position
if (extendedPosition) {
final long[] parts = new long[3];
parts[0] = tweak[0] & 0xFFFFFFFFL;
parts[1] = (tweak[0] >>> 32) & 0xFFFFFFFFL;
parts[2] = tweak[1] & 0xFFFFFFFFL;
long carry = advance;
for (int i = 0; i < parts.length; i++) {
carry += parts[i];
parts[i] = carry;
carry >>>= 32;
}
tweak[0] = ((parts[1] & 0xFFFFFFFFL) << 32) | (parts[0] & 0xFFFFFFFFL);
tweak[1] = (tweak[1] & 0xFFFFFFFF00000000L) | (parts[2] & 0xFFFFFFFFL);
} else {
long position = tweak[0];
position += advance;
tweak[0] = position;
if (position > LOW_RANGE) {
extendedPosition = true;
}
}
}
public long[] getWords() {
return tweak;
}
public String toString() {
return getType() + " first: " + isFirst() + ", final: " + isFinal();
}
}
/**
* The Unique Block Iteration chaining mode.
*/
private class UBI {
/**
* The tweak.
*/
private final UbiTweak tweak = new UbiTweak();
/**
* Buffer for the current block of message data.
*/
private byte[] currentBlock;
/**
* Offset into the current message block.
*/
private int currentOffset;
/**
* Buffer for message words for feedback into encrypted block.
*/
private long[] message;
UBI(final int blockSize) {
currentBlock = new byte[blockSize];
message = new long[currentBlock.length / 8];
}
public void reset(final UBI pUbi) {
currentBlock = Arrays.clone(pUbi.currentBlock, currentBlock);
currentOffset = pUbi.currentOffset;
message = Arrays.clone(pUbi.message, this.message);
tweak.reset(pUbi.tweak);
}
public void reset(final int type) {
tweak.reset();
tweak.setType(type);
currentOffset = 0;
}
public void setTreeLocation(final int level, final long offSet, final int pShift) {
int shift = getBlockSize() << 3;
shift += pShift; //level == 0 ? leafLen : fanOut;
final int hiOffset = (int) offSet >>> 64 - shift;
final long loOffset = offSet << shift;
tweak.setTreeLocation(level, hiOffset, loOffset);
}
public void update(final byte[] value, final int offset, final int len, final long[] output) {
/*
* Buffer complete blocks for the underlying Threefish cipher, only flushing when there
* are subsequent bytes (last block must be processed in doFinal() with final=true set).
*/
int copied = 0;
while (len > copied) {
if (currentOffset == currentBlock.length) {
processBlock(output);
tweak.setFirst(false);
currentOffset = 0;
}
final int toCopy = Math.min((len - copied), currentBlock.length - currentOffset);
System.arraycopy(value, offset + copied, currentBlock, currentOffset, toCopy);
copied += toCopy;
currentOffset += toCopy;
tweak.advancePosition(toCopy);
}
}
private void processBlock(final long[] output) {
threefish.init(true, chain, tweak.getWords());
for (int i = 0; i < message.length; i++) {
message[i] = Pack.littleEndianToLong(currentBlock, i * 8);
}
threefish.processBlock(message, output);
for (int i = 0; i < output.length; i++) {
output[i] ^= message[i];
}
}
public void doFinal(final long[] output) {
// Pad remainder of current block with zeroes
for (int i = currentOffset; i < currentBlock.length; i++) {
currentBlock[i] = 0;
}
tweak.setFinal(true);
processBlock(output);
}
}
/**
* Underlying Threefish tweakable block cipher.
*/
private final ThreefishEngine threefish;
/**
* Size of the digest output, in bytes.
*/
private final int outputSizeBytes;
/**
* The current chaining/state value.
*/
private long[] chain;
/**
* The initial state value.
*/
private long[] initialState;
/**
* The (optional) key parameter.
*/
private byte[] key;
/**
* Parameters to apply prior to the message.
*/
private Parameter[] preMessageParameters;
/**
* Parameters to apply after the message, but prior to output.
*/
private Parameter[] postMessageParameters;
/**
* The current UBI operation.
*/
private final UBI ubi;
/**
* Buffer for single byte update method.
*/
private final byte[] singleByte = new byte[1];
/**
* The explicit configuration.
*/
private Configuration theConfig;
/**
* Constructs a Skein engine.
*
* @param blockSizeBits the internal state size in bits - one of {@link #SKEIN_256}, {@link #SKEIN_512} or
* {@link #SKEIN_1024}.
* @param outputSizeBits the output/digest size to produce in bits, which must be an integral number of
* bytes.
*/
public GordianSkeinBase(final int blockSizeBits, final int outputSizeBits) {
if (outputSizeBits % 8 != 0) {
throw new IllegalArgumentException("Output size must be a multiple of 8 bits. :" + outputSizeBits);
}
this.outputSizeBytes = outputSizeBits / Byte.SIZE;
this.threefish = new ThreefishEngine(blockSizeBits);
this.ubi = new UBI(threefish.getBlockSize());
}
/**
* Creates a SkeinEngine as an exact copy of an existing instance.
*
* @param engine the base engine
*/
public GordianSkeinBase(final GordianSkeinBase engine) {
this(engine.getBlockSize() * 8, engine.getOutputSize() * 8);
copyIn(engine);
}
private void copyIn(final GordianSkeinBase engine) {
this.ubi.reset(engine.ubi);
this.chain = Arrays.clone(engine.chain, this.chain);
this.initialState = Arrays.clone(engine.initialState, this.initialState);
this.key = Arrays.clone(engine.key, this.key);
this.preMessageParameters = clone(engine.preMessageParameters, this.preMessageParameters);
this.postMessageParameters = clone(engine.postMessageParameters, this.postMessageParameters);
}
private static Parameter[] clone(final Parameter[] data, final Parameter[] existing) {
if (data == null) {
return null;
}
Parameter[] myExisting = existing;
if ((myExisting == null) || (myExisting.length != data.length)) {
myExisting = new Parameter[data.length];
}
System.arraycopy(data, 0, myExisting, 0, myExisting.length);
return existing;
}
@Override
public Memoable copy() {
return new GordianSkeinBase(this);
}
@Override
public void reset(final Memoable other) {
final GordianSkeinBase s = (GordianSkeinBase) other;
if ((getBlockSize() != s.getBlockSize()) || (outputSizeBytes != s.outputSizeBytes)) {
throw new IllegalArgumentException("Incompatible parameters in provided SkeinEngine.");
}
copyIn(s);
}
/**
* Get output size.
*
* @return the output size
*/
public int getOutputSize() {
return outputSizeBytes;
}
/**
* Obtain the block size.
*
* @return the block size
*/
public int getBlockSize() {
return threefish.getBlockSize();
}
/**
* Set the extended configuration.
*
* @param pConfig the extended configuration
*/
void setConfiguration(final Configuration pConfig) {
theConfig = pConfig;
}
/**
* Initialises the Skein engine with the provided parameters. See {@link GordianSkeinParameters} for
* details on the parameterisation of the Skein hash function.
*
* @param params the parameters to apply to this engine, or <code>null</code> to use no parameters.
*/
@SuppressWarnings("unchecked")
public void init(final GordianSkeinParameters params) {
this.chain = null;
this.key = null;
this.preMessageParameters = null;
this.postMessageParameters = null;
if (params != null) {
final byte[] theKey = params.getKey();
if (theKey != null && theKey.length < 16) {
throw new IllegalArgumentException("Skein key must be at least 128 bits.");
}
initParams(params.getParameters());
}
createInitialState();
// Initialise message block
ubiInit(PARAM_TYPE_MESSAGE);
}
private void initParams(final Hashtable<Integer, byte[]> parameters) {
final Enumeration<Integer> keys = parameters.keys();
final Vector<Parameter> pre = new Vector<>();
final Vector<Parameter> post = new Vector<>();
while (keys.hasMoreElements()) {
final Integer type = keys.nextElement();
final byte[] value = parameters.get(type);
if (type == PARAM_TYPE_KEY) {
this.key = value;
} else if (type < PARAM_TYPE_MESSAGE) {
pre.addElement(new Parameter(type, value));
} else {
post.addElement(new Parameter(type, value));
}
}
preMessageParameters = new Parameter[pre.size()];
pre.copyInto(preMessageParameters);
sort(preMessageParameters);
postMessageParameters = new Parameter[post.size()];
post.copyInto(postMessageParameters);
sort(postMessageParameters);
}
private static void sort(final Parameter[] params) {
if (params == null) {
return;
}
// Insertion sort, for Java 1.1 compatibility
for (int i = 1; i < params.length; i++) {
final Parameter param = params[i];
int hole = i;
while (hole > 0 && param.getType() < params[hole - 1].getType()) {
params[hole] = params[hole - 1];
hole = hole - 1;
}
params[hole] = param;
}
}
/**
* Calculate the initial (pre message block) chaining state.
*/
private void createInitialState() {
final boolean xtendedConfig = theConfig != null;
final long[] precalc = xtendedConfig ? null : INITIAL_STATES.get(variantIdentifier(getBlockSize(), getOutputSize()));
if ((key == null) && (precalc != null)) {
// Precalculated UBI(CFG)
chain = Arrays.clone(precalc);
} else {
// Blank initial state
chain = new long[getBlockSize() / 8];
// Process key block
if (key != null) {
ubiComplete(SkeinParameters.PARAM_TYPE_KEY, key);
}
// Process configuration block
final Configuration myConfig = xtendedConfig ? theConfig : new Configuration(outputSizeBytes * 8L);
ubiComplete(PARAM_TYPE_CONFIG, myConfig.getBytes());
}
// Process additional pre-message parameters
if (preMessageParameters != null) {
for (int i = 0; i < preMessageParameters.length; i++) {
final Parameter param = preMessageParameters[i];
ubiComplete(param.getType(), param.getValue());
}
}
initialState = Arrays.clone(chain);
}
/**
* Reset the engine to the initial state (with the key and any pre-message parameters , ready to
* accept message input.
*/
public void reset() {
System.arraycopy(initialState, 0, chain, 0, chain.length);
ubiInit(PARAM_TYPE_MESSAGE);
}
void initTreeNode(final int level, final long offset, final int shift) {
reset();
ubi.setTreeLocation(level, offset, shift);
}
private void ubiComplete(final int type, final byte[] value) {
ubiInit(type);
this.ubi.update(value, 0, value.length, chain);
ubiFinal();
}
private void ubiInit(final int type) {
this.ubi.reset(type);
}
private void ubiFinal() {
ubi.doFinal(chain);
}
private void checkInitialised() {
if (this.ubi == null) {
throw new IllegalArgumentException("Skein engine is not initialised.");
}
}
/**
* Update the digest.
*
* @param in the byte to update with
*/
public void update(final byte in) {
singleByte[0] = in;
update(singleByte, 0, 1);
}
/**
* Update the digest.
*
* @param in the input buffer
* @param inOff the input offset
* @param len the input length
*/
public void update(final byte[] in, final int inOff, final int len) {
checkInitialised();
ubi.update(in, inOff, len, chain);
}
void calculateNode(final byte[] out, final int outOff) {
checkInitialised();
if (out.length < (outOff + outputSizeBytes)) {
throw new OutputLengthException("Output buffer is too short to hold output");
}
// Finalise message block
ubiFinal();
/* Output the state */
for (int i = 0; i < chain.length; i++) {
Pack.longToLittleEndian(chain[i], out, outOff + (i * 8));
}
}
/**
* Finalise the digest.
*
* @param out the output buffer
* @param outOff the output offset
* @return the number of bytes returned
*/
public int doFinal(final byte[] out, final int outOff) {
checkInitialised();
if (out.length < (outOff + outputSizeBytes)) {
throw new OutputLengthException("Output buffer is too short to hold output");
}
// Initiate output
initiateOutput();
// Perform the output transform
final int blockSize = getBlockSize();
final int blocksRequired = ((outputSizeBytes + blockSize - 1) / blockSize);
for (int i = 0; i < blocksRequired; i++) {
final int toWrite = Math.min(blockSize, outputSizeBytes - (i * blockSize));
output(i, out, outOff + (i * blockSize), toWrite);
}
reset();
return outputSizeBytes;
}
void postProcessMessage() {
// Process additional post-message parameters
if (postMessageParameters != null) {
for (int i = 0; i < postMessageParameters.length; i++) {
final Parameter param = postMessageParameters[i];
ubiComplete(param.getType(), param.getValue());
}
}
}
void initiateOutput() {
// Finalise message block
ubiFinal();
// Process additional post-message parameters
postProcessMessage();
}
void restoreForOutput(final byte[] pState) {
/* Restore the state */
for (int i = 0; i < chain.length; i++) {
chain[i] = Pack.littleEndianToLong(pState, i * 8);
}
// Initiate output
initiateOutput();
}
void output(final long outputSequence, final byte[] out, final int outOff, final int outputBytes) {
final byte[] currentBytes = new byte[8];
Pack.longToLittleEndian(outputSequence, currentBytes, 0);
// Output is a sequence of UBI invocations all of which use and preserve the pre-output
// state
final long[] outputWords = new long[chain.length];
ubiInit(PARAM_TYPE_OUTPUT);
this.ubi.update(currentBytes, 0, currentBytes.length, outputWords);
ubi.doFinal(outputWords);
final int wordsRequired = ((outputBytes + 8 - 1) / 8);
for (int i = 0; i < wordsRequired; i++) {
final int toWrite = Math.min(8, outputBytes - (i * 8));
if (toWrite == 8) {
Pack.longToLittleEndian(outputWords[i], out, outOff + (i * 8));
} else {
Pack.longToLittleEndian(outputWords[i], currentBytes, 0);
System.arraycopy(currentBytes, 0, out, outOff + (i * 8), toWrite);
}
}
}
}