GordianSP800CTRDRBG.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.core.random;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymKeySpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianByteArrayInteger;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianDataConverter;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cipher.GordianCoreCipher;
import org.bouncycastle.crypto.prng.EntropySource;
import org.bouncycastle.util.Arrays;

/**
 * Implementation of CTRSP800DRBG based on the BouncyCastle Code.
 * <p>
 * This implementation is modified so that it accepts any GordianCipher.
 */
public class GordianSP800CTRDRBG
        implements GordianDRBGenerator {
    /**
     * The endData flag.
     */
    private static final byte ENDDATA = (byte) 0x80;

    /**
     * The Cipher.
     */
    private final GordianCoreCipher<GordianSymKeySpec> theCipher;

    /**
     * The Entropy Source.
     */
    private final EntropySource theEntropy;

    /**
     * The ReSeed Counter.
     */
    private final GordianByteArrayInteger theReseedCounter;

    /**
     * The Seed Length.
     */
    private final int theSeedLen;

    /**
     * The Key.
     */
    private final byte[] theKey;

    /**
     * The Variable Source.
     */
    private final GordianByteArrayInteger theV;

    /**
     * The cipher blockSize (bytes).
     */
    private final int theBlockLen;

    /**
     * Construct a SP800-90A Hash DRBG.
     *
     * @param pCipher        SIC Cipher to base the DRBG on.
     * @param pEntropy       source of entropy to use for seeding/reSeeding.
     * @param pSecurityBytes personalisation string to distinguish this DRBG (may be null).
     * @param pInitVector    nonce to further distinguish this DRBG (may be null).
     * @throws GordianException on error
     */
    public GordianSP800CTRDRBG(final GordianCoreCipher<GordianSymKeySpec> pCipher,
                               final EntropySource pEntropy,
                               final byte[] pSecurityBytes,
                               final byte[] pInitVector) throws GordianException {
        /* Store cipher and entropy source */
        theCipher = pCipher;
        theEntropy = pEntropy;

        /* Initialise buffers */
        final int myKeyLen = pCipher.getKeyType().getKeyLength().getByteLength();
        theBlockLen = theCipher.getKeyType().getBlockLength().getByteLength();
        theKey = new byte[myKeyLen];
        theV = new GordianByteArrayInteger(theBlockLen);
        theSeedLen = (myKeyLen + theBlockLen) * Byte.SIZE;

        /* Create Seed Material */
        final byte[] myEntropy = theEntropy.getEntropy();
        final byte[] mySeedInput = Arrays.concatenate(myEntropy, pInitVector, pSecurityBytes);
        final byte[] mySeed = blockCipherDF(mySeedInput, theSeedLen);

        /* Update the state */
        ctrDRBGUpdate(mySeed);

        /* Initialise reSeed counter */
        theReseedCounter = new GordianByteArrayInteger(Long.BYTES);
        theReseedCounter.iterate();
    }

    /**
     * Block Cipher derivation function.
     *
     * @param pInput   the seed material
     * @param pNumBits the number of bits to return
     * @return the derived seed
     * @throws GordianException on error
     */
    private byte[] blockCipherDF(final byte[] pInput,
                                 final int pNumBits) throws GordianException {
        /* Check valid # of bits */
        if (pNumBits > GordianCoreRandomFactory.MAX_BITS_REQUEST) {
            throw new IllegalArgumentException("Number of bits per request limited to "
                    + GordianCoreRandomFactory.MAX_BITS_REQUEST);
        }

        /* Allocate the lengths */
        final byte[] myL = GordianDataConverter.integerToByteArray(pInput.length);
        final byte[] myN = GordianDataConverter.integerToByteArray(pNumBits / Byte.SIZE);

        /* Create the input buffer */
        final int myKeyLen = theKey.length;
        final int myBaseLen = (2 * Integer.BYTES) + pInput.length + 1;
        final int myDataLen = theBlockLen * ((myBaseLen + theBlockLen - 1) / theBlockLen);
        final byte[] myBlock = new byte[myDataLen];
        System.arraycopy(myL, 0, myBlock, 0, Integer.BYTES);
        System.arraycopy(myN, 0, myBlock, Integer.BYTES, Integer.BYTES);
        System.arraycopy(pInput, 0, myBlock, 2 * Integer.BYTES, pInput.length);
        myBlock[2 * Integer.BYTES + pInput.length] = ENDDATA;

        /* Create the key buffer and initialise it */
        final byte[] myKey = new byte[myKeyLen];
        for (int i = 0; i < myKeyLen; i++) {
            myKey[i] = (byte) i;
        }

        /* Create the temporary buffers */
        final byte[] myTemp = new byte[myKeyLen + theBlockLen];
        final byte[] myOut = new byte[theBlockLen];
        final byte[] myIV = new byte[theBlockLen];

        /* while we need to generate more bytes */
        int myBuilt = 0;
        final GordianByteArrayInteger myI = new GordianByteArrayInteger();
        while (myBuilt < myTemp.length) {
            /* Update the buffer */
            System.arraycopy(myI.getBuffer(), 0, myIV, 0, Integer.BYTES);
            blockCC(myOut, myKey, myIV, myBlock);

            /* Determine how many bytes of this hash should be used */
            int myNeeded = myTemp.length
                    - myBuilt;
            if (myNeeded > theBlockLen) {
                myNeeded = theBlockLen;
            }

            /* Copy bytes across */
            System.arraycopy(myOut, 0, myTemp, myBuilt, myNeeded);
            myBuilt += myNeeded;
            myI.iterate();
        }

        /* Access key and IV from temp */
        System.arraycopy(myTemp, 0, myKey, 0, myKeyLen);
        System.arraycopy(myTemp, myKeyLen, myOut, 0, theBlockLen);

        /* while we need to generate more bytes */
        myBuilt = 0;
        while (myBuilt < myTemp.length) {
            /* Encrypt the bytes */
            theCipher.initKeyBytes(myKey);
            theCipher.finish(myOut, 0, myOut.length, myOut, 0);

            /* Determine how many bytes of this hash should be used */
            int myNeeded = myTemp.length
                    - myBuilt;
            if (myNeeded > theBlockLen) {
                myNeeded = theBlockLen;
            }

            /* Copy bytes across */
            System.arraycopy(myOut, 0, myTemp, myBuilt, myNeeded);
            myBuilt += myNeeded;
            myI.iterate();
        }

        /* Return the derived bytes */
        return myTemp;
    }

    /**
     * BCC Cipher Chaining.
     *
     * @param pOutput the output buffer
     * @param pKey    the key
     * @param pIV     the initVector
     * @param pData   the data
     * @throws GordianException on error
     */
    private void blockCC(final byte[] pOutput,
                         final byte[] pKey,
                         final byte[] pIV,
                         final byte[] pData) throws GordianException {
        /* Build the buffers */
        final byte[] myChain = new byte[theBlockLen];
        final byte[] myIn = new byte[theBlockLen];

        /* Encrypt the IV */
        theCipher.initKeyBytes(pKey);
        theCipher.finish(pIV, 0, theBlockLen, myChain, 0);

        /* Loop through the data */
        final int myNumBlocks = pData.length / theBlockLen;
        for (int i = 0; i < myNumBlocks; i++) {
            /* Xor in the value */
            final int offset = i * theBlockLen;
            for (int j = 0; j < theBlockLen; j++) {
                myIn[j] = (byte) (myChain[j] ^ pData[offset + j]);
            }

            /* Encrypt it */
            theCipher.finish(myIn, 0, theBlockLen, myChain, 0);
        }

        /* Copy the output */
        System.arraycopy(myChain, 0, pOutput, 0, theBlockLen);
    }

    /**
     * Update state.
     *
     * @param pSeed the seed material
     * @throws GordianException on error
     */
    private void ctrDRBGUpdate(final byte[] pSeed) throws GordianException {
        /* Create the buffers */
        final byte[] myOut = new byte[theBlockLen];
        final byte[] myResult = new byte[theSeedLen / Byte.SIZE];

        /* while we need to generate more bytes */
        int myBuilt = 0;
        while (myBuilt < myResult.length) {
            /* Encrypt the bytes */
            theCipher.initKeyBytes(theKey);
            theV.iterate();
            theCipher.finish(theV.getBuffer(), 0, theBlockLen, myOut, 0);

            /* Determine how many bytes of this hash should be used */
            int myNeeded = myResult.length
                    - myBuilt;
            if (myNeeded > theBlockLen) {
                myNeeded = theBlockLen;
            }

            /* Copy bytes across */
            System.arraycopy(myOut, 0, myResult, myBuilt, myNeeded);
            myBuilt += myNeeded;
        }

        /* Xor in the Seed */
        for (int i = 0; i < myResult.length; i++) {
            myResult[i] ^= pSeed[i];
        }

        /* Update key and IV from result*/
        System.arraycopy(myResult, 0, theKey, 0, theKey.length);
        System.arraycopy(myResult, theKey.length, theV.getBuffer(), 0, theBlockLen);
    }

    @Override
    public void reseed(final byte[] pXtraBytes) {
        try {
            /* Hash the new input and add to variable hash */
            final byte[] myEntropy = theEntropy.getEntropy();
            final byte[] mySeedInput = Arrays.concatenate(myEntropy, pXtraBytes);
            final byte[] mySeed = blockCipherDF(mySeedInput, theSeedLen);

            /* Update the state */
            ctrDRBGUpdate(mySeed);

            /* re-initialise reSeed counter */
            theReseedCounter.reset();
            theReseedCounter.iterate();
        } catch (GordianException e) {
            throw new IllegalStateException(e);
        }
    }

    @Override
    public int generate(final byte[] pOutput,
                        final byte[] pXtraBytes,
                        final boolean isPredictionResistant) {
        /* Check valid # of bits */
        final int myNumBits = pOutput.length << GordianCoreRandomFactory.BIT_SHIFT;
        if (myNumBits > GordianCoreRandomFactory.MAX_BITS_REQUEST) {
            throw new IllegalArgumentException("Number of bits per request limited to "
                    + GordianCoreRandomFactory.MAX_BITS_REQUEST);
        }

        /* Check for reSeed required */
        if (theReseedCounter.compareLimit(GordianCoreRandomFactory.RESEED_MAX)) {
            return -1;
        }

        /* If we are prediction resistant */
        byte[] mySeed = pXtraBytes;
        if (isPredictionResistant) {
            /* reSeed */
            reseed(pXtraBytes);
            mySeed = null;
        }

        try {
            /* if we have extra bytes */
            if (mySeed != null) {
                /* Derive the new input and process */
                mySeed = blockCipherDF(pXtraBytes, theSeedLen);
                ctrDRBGUpdate(mySeed);

                /* else allocate a buffer of zeroes */
            } else {
                mySeed = new byte[theBlockLen + theKey.length];
            }

            /* Create the buffers */
            final byte[] myOut = new byte[theBlockLen];
            final byte[] myResult = new byte[pOutput.length];

            /* while we need to generate more bytes */
            int myBuilt = 0;
            while (myBuilt < myResult.length) {
                /* Encrypt the bytes */
                theCipher.initKeyBytes(theKey);
                theV.iterate();
                theCipher.finish(theV.getBuffer(), 0, theBlockLen, myOut, 0);

                /* Determine how many bytes of this hash should be used */
                int myNeeded = myResult.length
                        - myBuilt;
                if (myNeeded > theBlockLen) {
                    myNeeded = theBlockLen;
                }

                /* Copy bytes across */
                System.arraycopy(myOut, 0, myResult, myBuilt, myNeeded);
                myBuilt += myNeeded;
            }

            /* Update the state */
            ctrDRBGUpdate(mySeed);

            /* Iterate the reSeed counter */
            theReseedCounter.iterate();

            /* Return the bytes */
            System.arraycopy(myResult, 0, pOutput, 0, pOutput.length);
        } catch (GordianException e) {
            throw new IllegalStateException(e);
        }

        /* Return the number of bits generated */
        return myNumBits;
    }

    @Override
    public int getBlockSize() {
        return theBlockLen * Byte.SIZE;
    }

    @Override
    public String getAlgorithm() {
        return GordianCoreRandomFactory.SP800_PREFIX + theCipher.getCipherSpec().toString();
    }
}