JcaCipherFactory.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.jca;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.base.GordianLength;
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.GordianWrapper;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianCipherMode;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianPadding;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamKeySubType.GordianChaCha20Key;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamKeySubType.GordianSalsa20Key;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamKeySubType.GordianVMPCKey;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.spec.GordianStreamKeyType;
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.key.GordianKey;
import io.github.tonywasher.joceanus.gordianknot.api.key.GordianKeyGenerator;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseData;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cipher.GordianCoreCipherFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianCryptoException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.spec.cipher.GordianCoreStreamCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.spec.cipher.GordianCoreSymCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.spec.cipher.GordianCoreSymCipherSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.impl.jca.JcaAEADCipher.JcaStreamAEADCipher;
import io.github.tonywasher.joceanus.gordianknot.impl.jca.JcaAEADCipher.JcaSymAEADCipher;
import io.github.tonywasher.joceanus.gordianknot.impl.jca.JcaCipher.JcaStreamCipher;
import io.github.tonywasher.joceanus.gordianknot.impl.jca.JcaCipher.JcaSymCipher;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

/**
 * Jca Cipher Factory.
 */
public class JcaCipherFactory
        extends GordianCoreCipherFactory {
    /**
     * Cipher Algorithm Separator.
     */
    private static final Character ALGO_SEP = '/';

    /**
     * Kalyna algorithm name.
     */
    private static final String KALYNA_ALGORITHM = "DSTU7624";

    /**
     * KeyGenerator Cache.
     */
    private final Map<GordianKeySpec, JcaKeyGenerator<? extends GordianKeySpec>> theCache;

    /**
     * Constructor.
     *
     * @param pFactory the factory
     */
    JcaCipherFactory(final GordianBaseFactory pFactory) {
        /* Initialise underlying class */
        super(pFactory);

        /* Create the cache */
        theCache = new HashMap<>();
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T extends GordianKeySpec> GordianKeyGenerator<T> getKeyGenerator(final T pKeySpec) throws GordianException {
        /* Look up in the cache */
        JcaKeyGenerator<T> myGenerator = (JcaKeyGenerator<T>) theCache.get(pKeySpec);
        if (myGenerator == null) {
            /* Check the KeySpec */
            checkKeySpec(pKeySpec);

            /* Create the new generator */
            final String myAlgorithm = getKeyAlgorithm(pKeySpec);
            final KeyGenerator myJavaGenerator = getJavaKeyGenerator(myAlgorithm);
            myGenerator = new JcaKeyGenerator<>(getFactory(), pKeySpec, myJavaGenerator);

            /* Add to cache */

            theCache.put(pKeySpec, myGenerator);
        }
        return myGenerator;
    }

    @Override
    public GordianSymCipher createSymKeyCipher(final GordianSymCipherSpec pCipherSpec) throws GordianException {
        /* Check validity of SymKeySpec */
        checkSymCipherSpec(pCipherSpec);
        final GordianCoreSymCipherSpec mySpec = (GordianCoreSymCipherSpec) pCipherSpec;

        /* If this is an AAD cipher */
        if (mySpec.isAAD()) {
            /* Create the cipher */
            final Cipher myBCCipher = getJavaCipher(pCipherSpec);
            return new JcaSymAEADCipher(getFactory(), mySpec, myBCCipher);

            /* else create the standard cipher */
        } else {
            /* Create the cipher */
            final Cipher myBCCipher = getJavaCipher(pCipherSpec);
            return new JcaSymCipher(getFactory(), mySpec, myBCCipher);
        }
    }

    @Override
    public GordianStreamCipher createStreamKeyCipher(final GordianStreamCipherSpec pCipherSpec) throws GordianException {
        /* Check validity of StreamKeySpec */
        checkStreamCipherSpec(pCipherSpec);
        final GordianCoreStreamCipherSpec mySpec = (GordianCoreStreamCipherSpec) pCipherSpec;

        final Cipher myJCACipher = getJavaCipher(pCipherSpec);
        return pCipherSpec.isAEAD()
                ? new JcaStreamAEADCipher(getFactory(), mySpec, myJCACipher)
                : new JcaStreamCipher(getFactory(), mySpec, myJCACipher);
    }

    @Override
    public GordianWrapper createKeyWrapper(final GordianKey<GordianSymKeySpec> pKey) throws GordianException {
        /* Create the cipher */
        final JcaKey<GordianSymKeySpec> myKey = JcaKey.accessKey(pKey);
        final GordianSymCipherSpecBuilder myBuilder = GordianCoreSymCipherSpecBuilder.newInstance();
        final GordianSymCipherSpec mySpec = myBuilder.ecb(myKey.getKeyType(), GordianPadding.NONE);
        final JcaSymCipher myJcaCipher = (JcaSymCipher) createSymKeyCipher(mySpec);
        return createKeyWrapper(myKey, myJcaCipher);
    }

    /**
     * Obtain the algorithm for the keySpec.
     *
     * @param pKeySpec the keySpec
     * @param <T>      the SpecType
     * @return the name of the algorithm
     * @throws GordianException on error
     */
    private static <T extends GordianKeySpec> String getKeyAlgorithm(final T pKeySpec) throws GordianException {
        if (pKeySpec instanceof GordianStreamKeySpec mySpec) {
            return getStreamKeyAlgorithm(mySpec);
        }
        if (pKeySpec instanceof GordianSymKeySpec mySpec) {
            return getSymKeyAlgorithm(mySpec);
        }
        throw new GordianDataException(GordianBaseData.getInvalidText(pKeySpec));
    }

    /**
     * Create the BouncyCastle KeyGenerator via JCA.
     *
     * @param pAlgorithm the Algorithm
     * @return the KeyGenerator
     * @throws GordianException on error
     */
    private static KeyGenerator getJavaKeyGenerator(final String pAlgorithm) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Massage the keyGenerator name */
            String myAlgorithm = pAlgorithm;

            /* Note that DSTU7624 has only a single keyGenerator */
            if (myAlgorithm.startsWith(KALYNA_ALGORITHM)) {
                myAlgorithm = KALYNA_ALGORITHM;
            }

            /* Return a KeyGenerator for the algorithm */
            return KeyGenerator.getInstance(myAlgorithm, JcaProvider.BCPROV);

            /* Catch exceptions */
        } catch (NoSuchAlgorithmException e) {
            /* Throw the exception */
            throw new GordianCryptoException("Failed to create KeyGenerator", e);
        }
    }

    /**
     * Create the BouncyCastle SymKey Cipher via JCA.
     *
     * @param pCipherSpec the cipherSpec
     * @return the Cipher
     * @throws GordianException on error
     */
    private static Cipher getJavaCipher(final GordianSymCipherSpec pCipherSpec) throws GordianException {
        final String myAlgo = getSymKeyAlgorithm(pCipherSpec.getKeySpec())
                + ALGO_SEP
                + getCipherModeAlgorithm(pCipherSpec)
                + ALGO_SEP
                + getPaddingAlgorithm(pCipherSpec.getPadding());
        return getJavaCipher(myAlgo);
    }

    /**
     * Create the BouncyCastle StreamKey Cipher via JCA.
     *
     * @param pCipherSpec the StreamCipherSpec
     * @return the Cipher
     * @throws GordianException on error
     */
    private static Cipher getJavaCipher(final GordianStreamCipherSpec pCipherSpec) throws GordianException {
        final GordianStreamKeySpec myKeySpec = pCipherSpec.getKeySpec();
        String myAlgo = getStreamKeyAlgorithm(myKeySpec);
        if (pCipherSpec.isAEAD()
                && GordianStreamKeyType.CHACHA20 == myKeySpec.getStreamKeyType()) {
            myAlgo = "CHACHA20-POLY1305";
        }
        return getJavaCipher(myAlgo);
    }

    /**
     * Create the StreamKey Cipher via JCA.
     *
     * @param pAlgorithm the Algorithm
     * @return the KeyGenerator
     * @throws GordianException on error
     */
    private static Cipher getJavaCipher(final String pAlgorithm) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Return a Cipher for the algorithm */
            return Cipher.getInstance(pAlgorithm, JcaProvider.BCPROV);

            /* Catch exceptions */
        } catch (NoSuchAlgorithmException
                 | NoSuchPaddingException e) {
            /* Throw the exception */
            throw new GordianCryptoException("Failed to create Cipher", e);
        }
    }

    /**
     * Obtain the SymKey algorithm.
     *
     * @param pKeySpec the keySpec
     * @return the Algorithm
     * @throws GordianException on error
     */
    static String getSymKeyAlgorithm(final GordianSymKeySpec pKeySpec) throws GordianException {
        return switch (pKeySpec.getSymKeyType()) {
            case TWOFISH -> "TwoFish";
            case SERPENT -> "Serpent";
            case THREEFISH -> "ThreeFish-" + pKeySpec.getBlockLength().getLength();
            case GOST -> "GOST28147";
            case SHACAL2 -> "Shacal-2";
            case KUZNYECHIK -> "GOST3412-2015";
            case KALYNA -> KALYNA_ALGORITHM + "-" + pKeySpec.getBlockLength().getLength();
            case RC5 -> GordianLength.LEN_128.equals(pKeySpec.getBlockLength())
                    ? "RC5-64"
                    : "RC5";
            case AES, CAMELLIA, CAST6, RC6, ARIA, NOEKEON, SM4, SEED, SKIPJACK, TEA, XTEA, IDEA, RC2, CAST5, BLOWFISH,
                 DESEDE -> pKeySpec.getSymKeyType().name();
            default -> throw new GordianDataException(GordianBaseData.getInvalidText(pKeySpec));
        };
    }

    /**
     * Obtain the CipherMode algorithm.
     *
     * @param pSpec the cipherSpec
     * @return the Algorithm
     * @throws GordianException on error
     */
    private static String getCipherModeAlgorithm(final GordianSymCipherSpec pSpec) throws GordianException {
        final GordianCipherMode myMode = pSpec.getCipherMode();
        return switch (pSpec.getCipherMode()) {
            case ECB, SIC, EAX, CCM, GCM, OCB, GOFB, GCFB, CFB8, OFB8 -> myMode.name();
            case CBC, G3413CBC -> GordianCipherMode.CBC.name();
            case CFB, G3413CFB -> "CFB";
            case OFB, G3413OFB -> GordianCipherMode.OFB.name();
            case KCTR, G3413CTR -> "CTR";
            case KCCM -> GordianCipherMode.CCM.name();
            case KGCM -> GordianCipherMode.GCM.name();
            default -> throw new GordianDataException(GordianBaseData.getInvalidText(myMode));
        };
    }

    /**
     * Obtain the Padding algorithm.
     *
     * @param pPadding use padding true/false
     * @return the Algorithm
     */
    private static String getPaddingAlgorithm(final GordianPadding pPadding) {
        return switch (pPadding) {
            case CTS -> "withCTS";
            case X923 -> "X923Padding";
            case PKCS7 -> "PKCS7Padding";
            case ISO7816D4 -> "ISO7816-4Padding";
            case TBC -> "TBCPadding";
            default -> "NoPadding";
        };
    }

    /**
     * Obtain the StreamKey algorithm.
     *
     * @param pKeySpec the keySpec
     * @return the Algorithm
     * @throws GordianException on error
     */
    private static String getStreamKeyAlgorithm(final GordianStreamKeySpec pKeySpec) throws GordianException {
        return switch (pKeySpec.getStreamKeyType()) {
            case HC -> GordianLength.LEN_128 == pKeySpec.getKeyLength()
                    ? "HC128"
                    : "HC256";
            case ZUC -> GordianLength.LEN_128 == pKeySpec.getKeyLength()
                    ? "ZUC-128"
                    : "ZUC-256";
            case CHACHA20 -> pKeySpec.getSubKeyType() == GordianChaCha20Key.STD
                    ? "CHACHA"
                    : "CHACHA7539";
            case SALSA20 -> pKeySpec.getSubKeyType() == GordianSalsa20Key.STD
                    ? pKeySpec.getStreamKeyType().name()
                    : "XSALSA20";
            case VMPC -> pKeySpec.getSubKeyType() == GordianVMPCKey.STD
                    ? pKeySpec.getStreamKeyType().name()
                    : "VMPC-KSA3";
            case GRAIN -> "Grain128";
            case ISAAC, RC4 -> pKeySpec.getStreamKeyType().name();
            default -> throw new GordianDataException(GordianBaseData.getInvalidText(pKeySpec));
        };
    }

    @Override
    protected boolean validStreamKeySpec(final GordianStreamKeySpec pKeySpec) {
        /* Check basic validity */
        if (!super.validStreamKeySpec(pKeySpec)) {
            return false;
        }

        /* Reject XChaCha20 */
        return pKeySpec.getStreamKeyType() != GordianStreamKeyType.CHACHA20
                || pKeySpec.getSubKeyType() != GordianChaCha20Key.XCHACHA;
    }

    @Override
    protected boolean validSymCipherSpec(final GordianSymCipherSpec pCipherSpec) {
        /* Check standard features */
        if (!super.validSymCipherSpec(pCipherSpec)) {
            return false;
        }

        /* Disallow GCM-SIV */
        final GordianCipherMode myMode = pCipherSpec.getCipherMode();
        if (GordianCipherMode.GCMSIV.equals(myMode)) {
            return false;
        }

        /* Additional Checks */
        return switch (pCipherSpec.getKeySpec().getSymKeyType()) {
            case KALYNA ->
                /* Disallow OCB, CCM and GCM */
                    !GordianCipherMode.OCB.equals(myMode)
                            && !GordianCipherMode.KCCM.equals(myMode)
                            && !GordianCipherMode.KGCM.equals(myMode)
                            && !GordianCipherMode.CCM.equals(myMode)
                            && !GordianCipherMode.GCM.equals(myMode);
            case GOST ->
                /* Disallow OFB and CFB */
                    !GordianCipherMode.OFB.equals(myMode)
                            && !GordianCipherMode.CFB.equals(myMode);
            case KUZNYECHIK ->
                /* Disallow OCB, OFB, CFB and CBC */
                    !GordianCipherMode.OCB.equals(myMode)
                            && !GordianCipherMode.OFB.equals(myMode)
                            && !GordianCipherMode.CFB.equals(myMode)
                            && !GordianCipherMode.CBC.equals(myMode);
            default -> true;
        };
    }
}