GordianHKDFEngine.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.kdf;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.digest.GordianDigest;
import io.github.tonywasher.joceanus.gordianknot.api.digest.GordianDigestFactory;
import io.github.tonywasher.joceanus.gordianknot.api.digest.GordianDigestSpec;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMac;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacFactory;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacSpec;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacSpecBuilder;

import java.util.Arrays;
import java.util.Iterator;

/**
 * HKDF functions.
 */
public class GordianHKDFEngine {
    /**
     * The Digest.
     */
    private final GordianDigest theDigest;

    /**
     * The HMac.
     */
    private final GordianMac theHMac;

    /**
     * Is this a primary engine?
     */
    private boolean isPrimary;

    /**
     * Constructor.
     *
     * @param pFactory    the security factory
     * @param pDigestSpec the digestSpec
     * @throws GordianException on error
     */
    public GordianHKDFEngine(final GordianFactory pFactory,
                             final GordianDigestSpec pDigestSpec) throws GordianException {
        /* Create the digest */
        final GordianDigestFactory myDigestFactory = pFactory.getDigestFactory();
        theDigest = myDigestFactory.createDigest(pDigestSpec);

        /* Create the hMac */
        final GordianMacFactory myMacFactory = pFactory.getMacFactory();
        final GordianMacSpec myMacSpec = GordianMacSpecBuilder.hMac(pDigestSpec);
        theHMac = myMacFactory.createMac(myMacSpec);
    }

    /**
     * Derive bytes.
     *
     * @param pParams the parameters
     * @return the derived bytes
     * @throws GordianException on error
     */
    public byte[] deriveBytes(final GordianHKDFParams pParams) throws GordianException {
        /* Check parameters */
        if (pParams == null) {
            throw new IllegalStateException("Null HKDF parameters");
        }

        /* Determine the mode */
        final GordianHKDFMode myMode = pParams.getMode();
        byte[] myOutput = null;

        /* If we should extract the information */
        if (myMode.doExtract()) {
            myOutput = extractKeyingMaterial(pParams.saltIterator(), pParams.ikmIterator());
        }

        /* If we should expand the information */
        if (myMode.doExpand()) {
            /* Save the intermediate value */
            final byte[] myIntermediate = myOutput;

            /* Determine PRK and expand it */
            final byte[] myPRK = myOutput == null ? pParams.getPRK() : myOutput;
            myOutput = expandKeyingMaterial(pParams, myPRK);

            /* Clear intermediate result */
            if (myIntermediate != null) {
                Arrays.fill(myIntermediate, (byte) 0);
            }
        }

        /* Return the result */
        return myOutput;
    }

    /**
     * Extract keying material.
     *
     * @param saltIterator the iterator over the salts
     * @param ikmIterator  the iterator over the initial keying material
     * @return the extracted material
     * @throws GordianException on error
     */
    private byte[] extractKeyingMaterial(final Iterator<byte[]> saltIterator,
                                         final Iterator<byte[]> ikmIterator) throws GordianException {
        /* Determine the key */
        while (saltIterator.hasNext()) {
            theDigest.update(saltIterator.next());
        }
        theHMac.initKeyBytes(theDigest.finish());

        /* Extract the keying material */
        while (ikmIterator.hasNext()) {
            theHMac.update(ikmIterator.next());
        }
        return theHMac.finish();
    }

    /**
     * Expand the pseudo-random key.
     *
     * @param pParams the parameters
     * @param pPRK    the pseudo-random key
     * @return the expanded material
     * @throws GordianException on error
     */
    private byte[] expandKeyingMaterial(final GordianHKDFParams pParams,
                                        final byte[] pPRK) throws GordianException {
        /* Initialise the HMac */
        theHMac.initKeyBytes(pPRK);

        /* Allocate the output buffer */
        int myLenRemaining = pParams.getLength();
        final byte[] myOutput = new byte[myLenRemaining];
        final int myHashLen = theHMac.getMacSize();

        /* Initialise variables */
        byte[] myInput = null;
        int myOffset = 0;
        byte myCounter = 0;

        /* Loop while we have more data to obtain */
        while (myLenRemaining > 0) {
            /* Update with the results of the last loop */
            if (myInput != null) {
                theHMac.update(myInput);
                Arrays.fill(myInput, (byte) 0);
            }

            /* Update with the info */
            final Iterator<byte[]> myIterator = pParams.infoIterator();
            while (myIterator.hasNext()) {
                theHMac.update(myIterator.next());
            }

            /* Update with the counter */
            theHMac.update(myCounter++);

            /* Calculate the hash */
            myInput = theHMac.finish();

            /* Output the required bytes */
            final int myLenToCopy = Math.min(myLenRemaining, myHashLen);
            System.arraycopy(myInput, 0, myOutput, myOffset, myLenToCopy);
            myOffset += myLenToCopy;
            myLenRemaining -= myLenToCopy;
        }

        /* Clear final intermediate results */
        if (myInput != null) {
            Arrays.fill(myInput, (byte) 0);
        }

        /* Return the expanded key */
        return myOutput;
    }
}