GordianStreamManager.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.stream;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianLength;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianCipherParameters;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianPadding;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianStreamCipher;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianStreamCipherSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianStreamKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymCipher;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymCipherSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.digest.GordianDigest;
import io.github.tonywasher.joceanus.gordianknot.api.digest.GordianDigestSpec;
import io.github.tonywasher.joceanus.gordianknot.api.key.GordianKey;
import io.github.tonywasher.joceanus.gordianknot.api.key.GordianKeyGenerator;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMac;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacParameters;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianIdManager;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cipher.GordianCoreCipherFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.digest.GordianCoreDigestFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keyset.GordianCoreKeySet;
import io.github.tonywasher.joceanus.gordianknot.impl.core.mac.GordianCoreMacFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.stream.GordianStreamDefinition.GordianStreamType;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Stream Factory.
 */
public final class GordianStreamManager {
    /**
     * The keySet.
     */
    private final GordianCoreKeySet theKeySet;

    /**
     * Constructor.
     *
     * @param pKeySet the keySet
     */
    public GordianStreamManager(final GordianCoreKeySet pKeySet) {
        theKeySet = pKeySet;
    }

    /**
     * Analyse output stream.
     *
     * @param pStream the output stream
     * @return the Stream definition list
     * @throws GordianException on error
     */
    public List<GordianStreamDefinition> analyseStreams(final OutputStream pStream) throws GordianException {
        /* Allocate the list */
        final List<GordianStreamDefinition> myStreams = new ArrayList<>();

        /* Loop through the streams */
        OutputStream myStream = pStream;
        while (true) {
            /* If this is a Digest Output Stream */
            if (myStream instanceof GordianDigestOutputStream myDigest) {
                /* Process stream */
                myStreams.add(0, new GordianStreamDefinition(theKeySet, myDigest));
                myStream = myDigest.getNextStream();

                /* If this is a MAC Output Stream */
            } else if (myStream instanceof GordianMacOutputStream myMac) {
                /* Process stream */
                myStreams.add(0, new GordianStreamDefinition(theKeySet, myMac));
                myStream = myMac.getNextStream();

                /* If this is a LZMA Output Stream */
            } else if (myStream instanceof GordianLZMAOutputStream myLZMA) {
                /* Process stream */
                myStreams.add(0, new GordianStreamDefinition(GordianStreamType.LZMA));
                myStream = myLZMA.getNextStream();

                /* If this is a Encryption Output Stream */
            } else if (myStream instanceof GordianCipherOutputStream<?> myEnc) {
                /* Process stream */
                myStreams.add(0, new GordianStreamDefinition(theKeySet, myEnc));
                myStream = myEnc.getNextStream();

                /* Else stop loop */
            } else {
                break;
            }
        }

        /* Return the list */
        return myStreams;
    }

    /**
     * Build input stream.
     *
     * @param pStreamDefs the list of stream definitions
     * @param pBaseStream the base input stream
     * @return the new input stream
     * @throws GordianException on error
     */
    public InputStream buildInputStream(final List<GordianStreamDefinition> pStreamDefs,
                                        final InputStream pBaseStream) throws GordianException {
        /* Loop through the stream definitions */
        InputStream myCurrent = pBaseStream;
        GordianMacInputStream myMacStream = null;
        for (GordianStreamDefinition myDef : pStreamDefs) {
            /* Build the stream */
            myCurrent = myDef.buildInputStream(theKeySet, myCurrent, myMacStream);
            if (myCurrent instanceof GordianMacInputStream myMac) {
                myMacStream = myMac;
            }
        }

        /* Return the stream */
        return myCurrent;
    }

    /**
     * Build output stream.
     *
     * @param pBaseStream the base output stream
     * @param pCompress   should we compress this file?
     * @return the new output stream
     * @throws GordianException on error
     */
    public OutputStream buildOutputStream(final OutputStream pBaseStream,
                                          final boolean pCompress) throws GordianException {
        /* Loop through the stream definitions */
        OutputStream myCurrent = pBaseStream;

        /* Access factory and bump the random engine */
        final GordianBaseFactory myFactory = theKeySet.getFactory();
        final GordianCoreCipherFactory myCiphers = (GordianCoreCipherFactory) myFactory.getCipherFactory();
        final GordianCoreMacFactory myMacs = (GordianCoreMacFactory) myFactory.getMacFactory();
        final GordianIdManager myIdMgr = myFactory.getIdManager();

        /* Create an initial MAC stream */
        final GordianMacSpec myMacSpec = myIdMgr.generateRandomMacSpec(theKeySet.getKeySetSpec().getKeyLength(), true);

        /* Determine a random key */
        final GordianKeyGenerator<GordianMacSpec> myGenerator = myMacs.getKeyGenerator(myMacSpec);
        final GordianKey<GordianMacSpec> myMacKey = myGenerator.generateKey();

        /* Create and initialise the MAC */
        final GordianMac myMac = myMacs.createMac(myMacSpec);
        myMac.init(GordianMacParameters.keyWithRandomNonce(myMacKey));
        final GordianMacOutputStream myMacStream = new GordianMacOutputStream(myMac, myCurrent);
        myCurrent = myMacStream;

        /* Generate a list of encryption types */
        final List<GordianKey<GordianSymKeySpec>> mySymKeys = generateRandomSymKeyList();
        boolean bFirst = true;

        /* For each encryption key */
        final Iterator<GordianKey<GordianSymKeySpec>> myIterator = mySymKeys.iterator();
        while (myIterator.hasNext()) {
            final GordianKey<GordianSymKeySpec> myKey = myIterator.next();
            final boolean bLast = !myIterator.hasNext();

            /* Determine mode and padding */
            GordianPadding myPadding = GordianPadding.NONE;
            if (!bFirst
                    && (bLast || myKey.getKeyType().getBlockLength() != GordianLength.LEN_128)) {
                myPadding = GordianPadding.ISO7816D4;
            }
            final GordianSymCipherSpec mySymSpec = bFirst
                    ? GordianSymCipherSpecBuilder.sic(myKey.getKeyType())
                    : GordianSymCipherSpecBuilder.ecb(myKey.getKeyType(), myPadding);

            /* Build the cipher stream */
            final GordianSymCipher mySymCipher = myCiphers.createSymKeyCipher(mySymSpec);
            mySymCipher.initForEncrypt(GordianCipherParameters.keyWithRandomNonce(myKey));
            myCurrent = new GordianCipherOutputStream<>(mySymCipher, myCurrent);

            /* Note that this is no longer the first */
            bFirst = false;
        }

        /* Create the encryption stream for a stream key */
        final GordianLength myKeyLen = theKeySet.getKeySetSpec().getKeyLength();
        final GordianStreamKeySpec myStreamKeySpec = myIdMgr.generateRandomStreamKeySpec(myKeyLen, true);
        final GordianKeyGenerator<GordianStreamKeySpec> myStreamGenerator = myCiphers.getKeyGenerator(myStreamKeySpec);
        final GordianKey<GordianStreamKeySpec> myStreamKey = myStreamGenerator.generateKey();
        final GordianStreamCipher myStreamCipher = myCiphers.createStreamKeyCipher(GordianStreamCipherSpecBuilder.stream(myStreamKey.getKeyType()));
        myStreamCipher.initForEncrypt(GordianCipherParameters.keyWithRandomNonce(myStreamKey));
        myCurrent = new GordianCipherOutputStream<>(myStreamCipher, myCurrent);

        /* If we are compressing */
        if (pCompress) {
            /* Attach an LZMA output stream onto the output */
            myCurrent = new GordianLZMAOutputStream(myCurrent);
        }

        /* Create a digest stream */
        final GordianDigest myDigest = generateRandomDigest();
        myCurrent = new GordianDigestOutputStream(myDigest, myCurrent, myMacStream);

        /* Return the stream */
        return myCurrent;
    }

    /**
     * generate random GordianDigest.
     *
     * @return the new Digest
     * @throws GordianException on error
     */
    private GordianDigest generateRandomDigest() throws GordianException {
        /* Access factory */
        final GordianBaseFactory myFactory = theKeySet.getFactory();
        final GordianCoreDigestFactory myDigests = (GordianCoreDigestFactory) myFactory.getDigestFactory();
        final GordianIdManager myIdMgr = myFactory.getIdManager();

        /* Generate the random digest */
        final GordianDigestSpec mySpec = myIdMgr.generateRandomDigestSpec(true);
        return myDigests.createDigest(mySpec);
    }

    /**
     * generate random SymKeyList.
     *
     * @return the list of keys
     * @throws GordianException on error
     */
    public List<GordianKey<GordianSymKeySpec>> generateRandomSymKeyList() throws GordianException {
        /* Access factory */
        final GordianBaseFactory myFactory = theKeySet.getFactory();
        final GordianCoreCipherFactory myCiphers = (GordianCoreCipherFactory) myFactory.getCipherFactory();
        final GordianIdManager myIdMgr = myFactory.getIdManager();

        /* Determine a random set of keyType */
        final int myCount = theKeySet.getKeySetSpec().getCipherSteps() - 1;
        final GordianLength myKeyLen = theKeySet.getKeySetSpec().getKeyLength();
        final GordianSymKeySpec[] mySpecs = myIdMgr.generateRandomKeySetSymKeySpecs(myKeyLen, myCount);

        /* Loop through the keys */
        final List<GordianKey<GordianSymKeySpec>> myKeyList = new ArrayList<>();
        for (int i = 0; i < myCount; i++) {
            /* Generate a random key */
            final GordianKeyGenerator<GordianSymKeySpec> myGenerator = myCiphers.getKeyGenerator(mySpecs[0]);
            myKeyList.add(myGenerator.generateKey());
        }

        /* Return the list */
        return myKeyList;
    }

    /**
     * Close an inputStream on error exit.
     *
     * @param pStream the file to delete
     */
    public static void cleanUpInputStream(final InputStream pStream) {
        try {
            pStream.close();
        } catch (IOException e) {
            /* NoOp */
        }
    }

    /**
     * Close an outputStream on error exit.
     *
     * @param pStream the file to delete
     */
    public static void cleanUpOutputStream(final OutputStream pStream) {
        try {
            pStream.close();
        } catch (IOException e) {
            /* NoOp */
        }
    }
}