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.GordianCipherParamsBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianStreamCipher;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymCipher;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianPadding;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamCipherSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianSymCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianSymCipherSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianSymKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.digest.GordianDigest;
import io.github.tonywasher.joceanus.gordianknot.api.digest.spec.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.GordianMacParamsBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.mac.spec.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.cipher.GordianCoreCipherParamsBuilder;
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.mac.GordianCoreMacParamsBuilder;
import io.github.tonywasher.joceanus.gordianknot.impl.core.spec.cipher.GordianCoreStreamCipherSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.impl.core.spec.cipher.GordianCoreSymCipherSpecBuilder;
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 */
            switch (myStream) {
                case GordianDigestOutputStream myDigest -> {
                    myStreams.addFirst(new GordianStreamDefinition(theKeySet, myDigest));
                    myStream = myDigest.getNextStream();
                }
                case GordianMacOutputStream myMac -> {
                    myStreams.addFirst(new GordianStreamDefinition(theKeySet, myMac));
                    myStream = myMac.getNextStream();
                }
                case GordianLZMAOutputStream myLZMA -> {
                    myStreams.addFirst(new GordianStreamDefinition(GordianStreamType.LZMA));
                    myStream = myLZMA.getNextStream();
                }
                case GordianCipherOutputStream<?> myEnc -> {
                    myStreams.addFirst(new GordianStreamDefinition(theKeySet, myEnc));
                    myStream = myEnc.getNextStream();
                }
                default -> {
                    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();
        final GordianSymCipherSpecBuilder mySymBuilder = GordianCoreSymCipherSpecBuilder.newInstance();
        final GordianStreamCipherSpecBuilder myStreamBuilder = GordianCoreStreamCipherSpecBuilder.newInstance();

        /* 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);
        final GordianMacParamsBuilder myMacBuilder = GordianCoreMacParamsBuilder.newInstance();
        myMac.init(myMacBuilder.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 GordianCipherParamsBuilder myCipherBuilder = GordianCoreCipherParamsBuilder.newInstance();
        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
                    ? mySymBuilder.sic(myKey.getKeyType())
                    : mySymBuilder.ecb(myKey.getKeyType(), myPadding);

            /* Build the cipher stream */
            final GordianSymCipher mySymCipher = myCiphers.createSymKeyCipher(mySymSpec);
            mySymCipher.initForEncrypt(myCipherBuilder.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(myStreamBuilder.streamCipher(myStreamKey.getKeyType()));
        myStreamCipher.initForEncrypt(myCipherBuilder.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 {
            if (pStream != null) {
                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 */
        }
    }
}