GordianCubeHashDigest.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 org.bouncycastle.crypto.ExtendedDigest;
import org.bouncycastle.util.Memoable;

import java.util.Arrays;

/**
 * CubeHash Digest.
 */
@SuppressWarnings("checkstyle:MagicNumber")
public class GordianCubeHashDigest
        implements ExtendedDigest, Memoable {
    /**
     * The State length.
     */
    private static final int STATELEN = 32;

    /**
     * The Swap length.
     */
    private static final int SWAPLEN = STATELEN / 2;

    /**
     * The state.
     */
    private final int[] theState = new int[STATELEN];

    /**
     * The swap buffer.
     */
    private final int[] theSwap = new int[SWAPLEN];

    /**
     * The initial state.
     */
    private final int[] theInitState;

    /**
     * The block length.
     */
    private final int theBlockLen;

    /**
     * The hash length.
     */
    private final int theHashLen;

    /**
     * The number of final rounds.
     */
    private final int theNumRounds;

    /**
     * The number of final rounds.
     */
    private final int theNumFinalRounds;

    /**
     * The input buffer.
     */
    private final byte[] theInputBuffer;

    /**
     * The current byte index into input buffer.
     */
    private int theByteIndex;

    /**
     * Constructor.
     *
     * @param pHashLen the hash lengyh
     */
    public GordianCubeHashDigest(final int pHashLen) {
        this(pHashLen, 32, 16, 16, 32);
    }

    /**
     * Constructor.
     *
     * @param pSource the source digest
     */
    private GordianCubeHashDigest(final GordianCubeHashDigest pSource) {
        /* Store configuration */
        theHashLen = pSource.theHashLen;
        theBlockLen = pSource.theBlockLen;
        theNumRounds = pSource.theNumRounds;
        theNumFinalRounds = pSource.theNumFinalRounds;

        /* Create the input buffer */
        theInputBuffer = new byte[theBlockLen];

        /* Copy the state */
        System.arraycopy(pSource.theState, 0, theState, 0, theState.length);
        System.arraycopy(pSource.theInputBuffer, 0, theInputBuffer, 0, theBlockLen);
        theByteIndex = pSource.theByteIndex;
        theInitState = Arrays.copyOf(pSource.theInitState, theState.length);
    }

    /**
     * Constructor.
     *
     * @param pHashLen     the hashLen in bits
     * @param pBlockLen    the blockLen in bytes
     * @param pNumRounds   the number of rounds per block
     * @param pInitRounds  the number of initial rounds
     * @param pFinalRounds the number of final rounds;
     */
    private GordianCubeHashDigest(final int pHashLen,
                                  final int pBlockLen,
                                  final int pNumRounds,
                                  final int pInitRounds,
                                  final int pFinalRounds) {
        /* Store configuration */
        theHashLen = pHashLen / Byte.SIZE;
        theBlockLen = pBlockLen;
        theNumRounds = pNumRounds;
        theNumFinalRounds = pFinalRounds;

        /* Create the input buffer */
        theInputBuffer = new byte[pBlockLen];

        /* Initialise the state */
        theState[0] = theHashLen;
        theState[1] = pBlockLen;
        theState[2] = pNumRounds;
        performRounds(pInitRounds);

        /* Save the initial state */
        theInitState = Arrays.copyOf(theState, theState.length);
    }

    @Override
    public String getAlgorithmName() {
        return "CubeHash-" + theHashLen * Byte.SIZE;
    }

    @Override
    public int getDigestSize() {
        return theHashLen;
    }

    @Override
    public int getByteLength() {
        return theHashLen;
    }

    @Override
    public void update(final byte pByte) {
        theInputBuffer[theByteIndex++] = pByte;
        if (theByteIndex == theBlockLen) {
            processBlock();
            theByteIndex = 0;
        }
    }

    @Override
    public void update(final byte[] pData, final int pOffset, final int pLength) {
        for (int i = 0; i < pLength; i++) {
            update(pData[pOffset + i]);
        }
    }

    @Override
    public int doFinal(final byte[] pHash, final int pOffset) {
        finaliseHash();
        outputHash(pHash, pOffset);
        return getDigestSize();
    }

    @Override
    public void reset() {
        System.arraycopy(theInitState, 0, theState, 0, theState.length);
        theByteIndex = 0;
    }

    @Override
    public GordianCubeHashDigest copy() {
        return new GordianCubeHashDigest(this);
    }

    @Override
    public void reset(final Memoable pState) {
        final GordianCubeHashDigest d = (GordianCubeHashDigest) pState;

        /* Copy the state */
        System.arraycopy(d.theState, 0, theState, 0, theState.length);
        System.arraycopy(d.theInputBuffer, 0, theInputBuffer, 0, theBlockLen);
        theByteIndex = d.theByteIndex;
    }

    /**
     * Decode a 32-bit value from a buffer (little-endian).
     *
     * @param buf the input buffer
     * @param off the input offset
     * @return the decoded value
     */
    private static int decode32le(final byte[] buf,
                                  final int off) {
        return (buf[off] & 0xFF)
                | ((buf[off + 1] & 0xFF) << 8)
                | ((buf[off + 2] & 0xFF) << 16)
                | ((buf[off + 3] & 0xFF) << 24);
    }

    /**
     * Encode a 32-bit value into a buffer (little-endian).
     *
     * @param val the value to encode
     * @param buf the output buffer
     * @param off the output offset
     */
    private static void encode32le(final int val,
                                   final byte[] buf,
                                   final int off) {
        buf[off] = (byte) val;
        buf[off + 1] = (byte) (val >> 8);
        buf[off + 2] = (byte) (val >> 16);
        buf[off + 3] = (byte) (val >> 24);
    }

    /**
     * Process a block.
     */
    private void processBlock() {
        /* Loop through the bytes in the block */
        for (int i = 0, j = 0; j < theBlockLen; i++, j += Integer.BYTES) {
            theState[i] ^= decode32le(theInputBuffer, j);
        }

        /* Perform the required rounds */
        performRounds(theNumRounds);
    }

    /**
     * Finalise the hash.
     */
    private void finaliseHash() {
        /* Set the marker */
        theInputBuffer[theByteIndex++] = (byte) 0x80;

        /* Fill remainder of buffer with zeroes */
        while (theByteIndex < theBlockLen) {
            theInputBuffer[theByteIndex++] = 0;
        }

        /* Process the block */
        processBlock();

        /* Adjust the final State word */
        theState[STATELEN - 1] ^= 1;

        /* Perform the required rounds */
        performRounds(theNumFinalRounds);
    }

    /**
     * Output the hash.
     *
     * @param pOutput the output buffer
     * @param pOffSet the offset with the output buffer to write to
     */
    private void outputHash(final byte[] pOutput,
                            final int pOffSet) {
        /* Loop through the bytes in the block */
        for (int i = 0, j = 0; j < theHashLen; i++, j += Integer.BYTES) {
            encode32le(theState[i], pOutput, j + pOffSet);
        }

        /* Reset back to initial state */
        reset();
    }

    /**
     * Perform the required number of rounds.
     *
     * @param pNumRounds the number of rounds
     */
    private void performRounds(final int pNumRounds) {
        /* Loop to perform the round */
        for (int i = 0; i < pNumRounds; i++) {
            performRound();
        }
    }

    /**
     * Perform the round.
     */
    private void performRound() {
        /* 1. Add x[0jklm] into x[1jklm] modulo 2^32, for each (j,k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i + SWAPLEN] += theState[i];
        }

        /* 2. Rotate x[0jklm] upwards by 7 bits, for each (j,k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theSwap[i] = theState[i] << 7 | theState[i] >>> 25;
        }

        /* 3. Swap x[00klm] with x[01klm], for each (k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i] = theSwap[i ^ 8];
        }

        /* 4. Xor x[1jklm] into x[0jklm], for each (j,k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i] ^= theState[i + SWAPLEN];
        }

        /* 5. Swap x[1jk0m] with x[1jk1m], for each (j,k,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theSwap[i] = theState[i + SWAPLEN];
        }
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i + SWAPLEN] = theSwap[i ^ 2];
        }

        /* 6. Add x[0jklm] into x[1jklm] modulo 2^32, for each (j,k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i + SWAPLEN] += theState[i];
        }

        /* 7. Rotate x[0jklm] upwards by 11 bits, for each (j,k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theSwap[i] = theState[i] << 11 | theState[i] >>> 21;
        }

        /* 8. Swap x[0j0lm] with x[0j1lm], for each (j,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i] = theSwap[i ^ 4];
        }

        /* 9. Xor x[1jklm] into x[0jklm], for each (j,k,l,m) */
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i] ^= theState[i + SWAPLEN];
        }

        /* 10. Swap x[1jkl0] with x[1jkl1], for each (j,k,l) */
        for (int i = 0; i < SWAPLEN; i++) {
            theSwap[i] = theState[i + SWAPLEN];
        }
        for (int i = 0; i < SWAPLEN; i++) {
            theState[i + SWAPLEN] = theSwap[i ^ 1];
        }
    }
}