GordianCoreKeySetCipher.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.keyset;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySetCipher;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySetSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianLogicException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keyset.GordianKeySetRecipe.GordianKeySetParameters;

/**
 * Core keySetCipher.
 */
public class GordianCoreKeySetCipher
        implements GordianKeySetCipher {
    /**
     * The factory.
     */
    private final GordianBaseFactory theFactory;

    /**
     * The keySetSpec.
     */
    private final GordianKeySetSpec theSpec;

    /**
     * The Underlying cipher.
     */
    private final GordianMultiCipher theCipher;

    /**
     * The cached header.
     */
    private final byte[] theHeader;

    /**
     * number of bytes in the header cache.
     */
    private int hdrBytes;

    /**
     * Are we in AEAD mode?
     */
    private final boolean aead;

    /**
     * Are we initialised?
     */
    private boolean initialised;

    /**
     * Are we encrypting?
     */
    private boolean encrypting;

    /**
     * Has the header been processed?
     */
    private boolean hdrProcessed;

    /**
     * Constructor.
     *
     * @param pKeySet the keySet.
     * @param pAead   are we in AEAD mode
     * @throws GordianException on error
     */
    public GordianCoreKeySetCipher(final GordianBaseKeySet pKeySet,
                                   final boolean pAead) throws GordianException {
        theFactory = pKeySet.getFactory();
        aead = pAead;
        theSpec = pKeySet.getKeySetSpec();
        theCipher = new GordianMultiCipher(pKeySet);
        theHeader = new byte[GordianKeySetRecipe.HDRLEN];
    }

    /**
     * Obtain the factory.
     *
     * @return the factory
     */
    protected GordianFactory getFactory() {
        return theFactory;
    }

    /**
     * Is the cipher initialised?
     *
     * @return true/false
     */
    protected boolean isInitialised() {
        return initialised;
    }

    /**
     * Is the cipher encrypting?
     *
     * @return true/false
     */
    protected boolean isEncrypting() {
        return encrypting;
    }

    /**
     * Obtain the multi-cipher.
     *
     * @return the cipher
     */
    protected GordianMultiCipher getMultiCipher() {
        return theCipher;
    }

    @Override
    public void initForEncrypt() throws GordianException {
        encrypting = true;
        reset();
    }

    @Override
    public void initForDecrypt() throws GordianException {
        encrypting = false;
        reset();
    }

    /**
     * Reset the cipher.
     *
     * @throws GordianException on error
     */
    protected void reset() throws GordianException {
        /* Set flags */
        hdrBytes = 0;
        initialised = true;
        hdrProcessed = false;
    }

    /**
     * Initialise the ciphers.
     *
     * @param pParams the keySet parameters
     * @throws GordianException on error
     */
    protected void initCiphers(final GordianKeySetParameters pParams) throws GordianException {
        /* Initialise the ciphers */
        theCipher.initCiphers(pParams, encrypting);
    }

    /**
     * check status.
     *
     * @throws GordianException on error
     */
    protected void checkStatus() throws GordianException {
        /* Check we are initialised */
        if (!initialised) {
            throw new GordianLogicException("Cipher is not initialised");
        }
    }

    @Override
    public int getOutputLength(final int pLength) {
        /* Handle encryption */
        if (encrypting) {
            return hdrProcessed ? theCipher.getOutputLength(pLength)
                    : GordianKeySetData.getEncryptionLength(pLength);
        }

        /* Allow for cacheSpace */
        final int cacheSpace = GordianKeySetRecipe.HDRLEN - hdrBytes;
        return pLength < cacheSpace ? 0 : pLength - cacheSpace;
    }

    @Override
    public int update(final byte[] pBytes,
                      final int pOffset,
                      final int pLength,
                      final byte[] pOutput,
                      final int pOutOffset) throws GordianException {
        /* Check status */
        checkStatus();

        /* Make sure that there is no overlap between buffers */
        byte[] myInput = pBytes;
        int myOffset = pOffset;
        if (check4UpdateOverLap(pBytes, pOffset, pLength, pOutput, pOutOffset)) {
            myInput = new byte[pLength];
            myOffset = 0;
            System.arraycopy(pBytes, pOffset, myInput, myOffset, pLength);
        }

        /* process the bytes */
        return encrypting
                ? updateEncryption(myInput, myOffset, pLength, pOutput, pOutOffset)
                : updateDecryption(myInput, myOffset, pLength, pOutput, pOutOffset);
    }

    /**
     * Obtain buffer length (allowing for null).
     *
     * @param pBuffer the buffere
     * @return the length
     */
    private static int bufLength(final byte[] pBuffer) {
        return pBuffer == null ? 0 : pBuffer.length;
    }

    /**
     * Check for buffer overlap in update.
     *
     * @param pBytes     Bytes to update cipher with
     * @param pOffset    offset within pBytes to read bytes from
     * @param pLength    length of data to update with
     * @param pOutput    the output buffer to receive processed data
     * @param pOutOffset offset within pOutput to write bytes to
     * @return is there overlap between the two buffers? true/false overlap
     * @throws GordianException on error
     */
    private boolean check4UpdateOverLap(final byte[] pBytes,
                                        final int pOffset,
                                        final int pLength,
                                        final byte[] pOutput,
                                        final int pOutOffset) throws GordianException {
        /* Check that the buffers are sufficient */
        if (bufLength(pBytes) < (pLength + pOffset)) {
            throw new GordianLogicException("Input buffer too short.");
        }
        if (bufLength(pOutput) < (getOutputLength(pLength) + pOutOffset)) {
            throw new GordianLogicException("Output buffer too short.");
        }

        /* Only relevant when the two buffers are the same */
        if (pBytes != pOutput) {
            return false;
        }

        /* Check for overlap */
        return pOutOffset < pOffset + pLength
                && pOffset < pOutOffset + getOutputLength(pLength);
    }

    /**
     * Update for encryption.
     *
     * @param pBytes     the input buffer
     * @param pOffset    the offset from which to start processing
     * @param pLength    the length of data to process
     * @param pOutput    the output buffer
     * @param pOutOffset the offset from which to start writing output
     * @return the length of data written out
     * @throws GordianException on error
     */
    protected int updateEncryption(final byte[] pBytes,
                                   final int pOffset,
                                   final int pLength,
                                   final byte[] pOutput,
                                   final int pOutOffset) throws GordianException {
        /* If we have not initialised the ciphers yet */
        if (hdrBytes == 0) {
            /* Generate a new KeySetRecipe */
            final GordianKeySetRecipe myRecipe = GordianKeySetRecipe.newRecipe(theFactory, theSpec, aead);
            final GordianKeySetParameters myParams = myRecipe.getParameters();
            myRecipe.buildHeader(theHeader);
            hdrBytes = GordianKeySetRecipe.HDRLEN;

            /* Initialise the ciphers */
            initCiphers(myParams);
        }

        /* If we have not processed the header yet */
        int bytesWritten = 0;
        if (!hdrProcessed) {
            /* Process the header */
            System.arraycopy(theHeader, 0, pOutput, pOutOffset, hdrBytes);
            hdrProcessed = true;
            bytesWritten = hdrBytes;
        }

        /* Process the bytes */
        final int numBytesWritten = theCipher.update(pBytes, pOffset, pLength, pOutput, pOutOffset + bytesWritten);
        bytesWritten += numBytesWritten;

        /* Return the number of bytes processed */
        return bytesWritten;
    }


    /**
     * Process decryption bytes.
     *
     * @param pBytes     the input buffer
     * @param pOffset    the offset from which to start processing
     * @param pLength    the length of data to process
     * @param pOutput    the output buffer
     * @param pOutOffset the offset from which to start writing output
     * @return the length of data written out
     * @throws GordianException on error
     */
    protected int updateDecryption(final byte[] pBytes,
                                   final int pOffset,
                                   final int pLength,
                                   final byte[] pOutput,
                                   final int pOutOffset) throws GordianException {
        /* If we have not yet processed the header*/
        int numRead = 0;
        if (!hdrProcessed) {
            /* Work out how many bytes to copy to cache */
            final int cacheSpace = GordianKeySetRecipe.HDRLEN - hdrBytes;
            numRead = Math.min(cacheSpace, pLength);

            /* Copy to the header */
            System.arraycopy(pBytes, 0, theHeader, hdrBytes, numRead);
            hdrBytes += numRead;

            /* If we have a complete header */
            if (hdrBytes == GordianKeySetRecipe.HDRLEN) {
                /* Process the recipe */
                final GordianKeySetRecipe myRecipe = GordianKeySetRecipe.parseRecipe(theFactory, theSpec, theHeader, aead);
                final GordianKeySetParameters myParams = myRecipe.getParameters();

                /* Initialise the ciphers */
                initCiphers(myParams);
                hdrProcessed = true;
            }
        }

        /* Process the bytes */
        return theCipher.update(pBytes, pOffset + numRead, pLength - numRead, pOutput, pOutOffset);
    }

    @Override
    public int finish(final byte[] pOutput,
                      final int pOutOffset) throws GordianException {
        /* Check that the buffers are sufficient */
        if (bufLength(pOutput) < (getOutputLength(0) + pOutOffset)) {
            throw new GordianLogicException("Output buffer too short.");
        }

        /* finish the cipher */
        return doFinish(pOutput, pOutOffset);
    }

    /**
     * Complete the Cipher operation and return final results.
     *
     * @param pOutput    the output buffer to receive processed data
     * @param pOutOffset offset within pOutput to write bytes to
     * @return the number of bytes transferred to the output buffer
     * @throws GordianException on error
     */
    public int doFinish(final byte[] pOutput,
                        final int pOutOffset) throws GordianException {
        /* Finish the cipher */
        final int myLen = finishCipher(pOutput, pOutOffset);

        /* Reset the cipher */
        reset();

        /* return the number of bytes processed */
        return myLen;
    }

    /**
     * Finish underlying cipher.
     *
     * @param pOutput    the output buffer to receive processed data
     * @param pOutOffset offset within pOutput to write bytes to
     * @return the length of data processed
     * @throws GordianException on error
     */
    protected int finishCipher(final byte[] pOutput,
                               final int pOutOffset) throws GordianException {
        /* Check status */
        checkStatus();

        /* Reject if we have not fully processed the header on decrypt */
        if (!encrypting && !hdrProcessed) {
            throw new GordianDataException("data too short");
        }

        /* Finish the cipher */
        return theCipher.finish(pOutput, pOutOffset);
    }
}