GordianCoreKeyStoreGateway.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.keystore;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianKeyPairUse;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry.GordianKeyStorePair;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipLock;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianDataConverter;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cert.GordianCoreCertificate;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianPEMObject.GordianPEMObjectType;
import io.github.tonywasher.joceanus.gordianknot.impl.core.zip.GordianCoreZipLock;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.crmf.CertReqMsg;
import org.bouncycastle.asn1.crmf.PKMACValue;
import org.bouncycastle.asn1.x500.X500Name;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

/**
 * keyStoreGateway implementation.
 */
public class GordianCoreKeyStoreGateway
        implements GordianBaseKeyStoreGateway {
    /**
     * The factory.
     */
    private final GordianBaseFactory theFactory;

    /**
     * The keyStoreMgr.
     */
    private final GordianCoreKeyStoreManager theKeyStoreMgr;

    /**
     * The keyStore.
     */
    private final GordianCoreKeyStore theKeyStore;

    /**
     * The encryptor.
     */
    private final GordianCRMEncryptor theEncryptor;

    /**
     * The builder.
     */
    private final GordianCRMBuilder theBuilder;

    /**
     * The parser.
     */
    private final GordianCRMParser theParser;

    /**
     * The next messageId.
     */
    private final AtomicInteger theNextId;

    /**
     * The requestMap.
     */
    private final Map<Integer, GordianRequestCache> theRequestMap;

    /**
     * The responseMap.
     */
    private final Map<Integer, GordianCoreCertificate> theResponseMap;

    /**
     * The encryption target certificate.
     */
    private GordianCoreCertificate theTarget;

    /**
     * The secret MAC key resolver.
     */
    private Function<X500Name, String> theMACSecretResolver;

    /**
     * The keyPairCertifier.
     */
    private GordianKeyStorePair theKeyPairCertifier;

    /**
     * The password callback.
     */
    private Function<String, char[]> thePasswordResolver;

    /**
     * The lock callback.
     */
    private GordianLockResolver theLockResolver;

    /**
     * Constructor.
     *
     * @param pFactory     the factory
     * @param pKeyStoreMgr the keyStoreMgr
     */
    GordianCoreKeyStoreGateway(final GordianBaseFactory pFactory,
                               final GordianCoreKeyStoreManager pKeyStoreMgr) {
        /* Store parameters */
        theFactory = pFactory;
        theKeyStoreMgr = pKeyStoreMgr;
        theKeyStore = theKeyStoreMgr.getKeyStore();

        /* Create underlying classes */
        theEncryptor = new GordianCRMEncryptor(theFactory);
        theBuilder = new GordianCRMBuilder(this);
        theParser = new GordianCRMParser(this, theBuilder);
        theNextId = new AtomicInteger(1);
        theRequestMap = new HashMap<>();
        theResponseMap = new HashMap<>();
    }

    @Override
    public GordianBaseFactory getFactory() {
        return theFactory;
    }

    @Override
    public GordianCoreKeyStore getKeyStore() {
        return theKeyStore;
    }

    @Override
    public GordianCoreKeyStoreManager getKeyStoreManager() {
        return theKeyStoreMgr;
    }

    @Override
    public GordianCRMEncryptor getEncryptor() {
        return theEncryptor;
    }

    @Override
    public GordianKeyStorePair getSigner() {
        return theKeyPairCertifier;
    }

    @Override
    public byte[] getMACSecret(final X500Name pName) {
        final String mySecret = theMACSecretResolver.apply(pName);
        return mySecret == null ? null : GordianDataConverter.stringToByteArray(mySecret);
    }

    @Override
    public GordianCoreCertificate getTarget() {
        return theTarget;
    }

    @Override
    public Function<String, char[]> getPasswordResolver() {
        return thePasswordResolver;
    }

    @Override
    public void exportEntry(final String pAlias,
                            final OutputStream pStream,
                            final GordianZipLock pLock) throws GordianException {
        final char[] myPassword = thePasswordResolver.apply(pAlias);
        final GordianKeyStoreEntry myEntry = theKeyStore.getEntry(pAlias, myPassword);
        final GordianPEMCoder myCoder = new GordianPEMCoder(theKeyStore);
        myCoder.exportKeyStoreEntry(myEntry, pStream, (GordianCoreZipLock) pLock);
    }

    @Override
    public void setEncryptionTarget(final String pAlias) throws GordianException {
        final List<GordianCertificate> myKeyPairChain = theKeyStore.getCertificateChain(pAlias);
        if (myKeyPairChain != null) {
            theTarget = (GordianCoreCertificate) myKeyPairChain.get(0);
            return;
        }
        throw new GordianDataException("Encryption target not found");
    }

    @Override
    public void setMACSecretResolver(final Function<X500Name, String> pResolver) {
        theMACSecretResolver = pResolver;
    }

    @Override
    public void createCertificateRequest(final String pAlias,
                                         final OutputStream pStream) throws GordianException {
        /* Access the requested entry */
        final char[] myPassword = thePasswordResolver.apply(pAlias);
        final GordianKeyStoreEntry myEntry = theKeyStore.getEntry(pAlias, myPassword);

        /* If it is a keyPair */
        if (myEntry instanceof GordianKeyStorePair myKeyPair) {
            /* Create the certificate request */
            final int myReqId = theNextId.getAndIncrement();
            final CertReqMsg myCertReq = theBuilder.createCertificateRequest(myKeyPair, myReqId);

            /* Store details in requestMap */
            theRequestMap.put(myReqId, new GordianRequestCache(pAlias, myKeyPair));

            /* Write request to output stream */
            final GordianPEMParser myParser = new GordianPEMParser();
            final GordianPEMObject myPEMObject = GordianPEMCoder.createPEMObject(GordianPEMObjectType.CERTREQ, myCertReq);
            myParser.writePEMFile(pStream, Collections.singletonList(myPEMObject));

            /* else reject request */
        } else {
            throw new GordianDataException("Alias not found");
        }
    }

    @Override
    public void setCertifier(final String pAlias) throws GordianException {
        final char[] myPassword = thePasswordResolver.apply(pAlias);
        final GordianKeyStoreEntry myEntry = theKeyStore.getEntry(pAlias, myPassword);
        if (myEntry instanceof GordianKeyStorePair myPair) {
            final GordianCertificate myCert = myPair.getCertificateChain().get(0);
            if (myCert.getUsage().hasUse(GordianKeyPairUse.CERTIFICATE)) {
                theKeyPairCertifier = myPair;
                return;
            }
        }
        throw new GordianDataException("Invalid keyPairCertifier");
    }

    @Override
    public void setPasswordResolver(final Function<String, char[]> pResolver) {
        thePasswordResolver = pResolver;
    }

    @Override
    public void setLockResolver(final GordianLockResolver pResolver) {
        theLockResolver = pResolver;
    }

    @Override
    public void processCertificateRequest(final InputStream pInStream,
                                          final OutputStream pOutStream) throws GordianException {
        /* Decode the certificate request */
        final GordianPEMParser myParser = new GordianPEMParser();
        final List<GordianPEMObject> myObjects = myParser.parsePEMFile(pInStream);
        final CertReqMsg myCertReq = GordianPEMCoder.decodeCertRequest(myObjects);

        /* Determine responseId and sign certificate */
        final int myRespId = theNextId.getAndIncrement();
        final List<GordianCertificate> myChain = theParser.processCertificateRequest(myCertReq);

        /* Create the certificate response */
        final int myReqId = myCertReq.getCertReq().getCertReqId().intValueExact();
        final GordianCertResponseASN1 myResponse = GordianCertResponseASN1.createCertResponse(myReqId, myRespId, myChain);

        /* Create PKMACValue if required */
        final X500Name mySubject = myCertReq.getCertReq().getCertTemplate().getSubject();
        final byte[] myMACSecret = getMACSecret(mySubject);
        if (myMACSecret != null) {
            final ASN1Object myMACData = myResponse.getMACData();
            final PKMACValue myMACValue = theBuilder.createPKMACValue(myMACSecret, myMACData);
            myResponse.setMACValue(myMACValue);
        }

        /* Access the new certificate */
        final GordianCoreCertificate myCert = (GordianCoreCertificate) myChain.get(0);

        /* If the certificate requires encryption */
        if (GordianCRMParser.requiresEncryption(myCertReq)) {
            /* Encrypt the certificate */
            myResponse.encryptCertificate(theEncryptor);

            /* Store in the response cache */
            theResponseMap.put(myRespId, myCert);

            /* else store the certificate */
        } else {
            theKeyStore.setCertificate(getCertificateAlias(myRespId), myCert);
        }

        /* Write out the response */
        final GordianPEMObject myPEMObject = GordianPEMCoder.createPEMObject(GordianPEMObjectType.CERTRESP, myResponse);
        myParser.writePEMFile(pOutStream, Collections.singletonList(myPEMObject));
    }

    @Override
    public Integer processCertificateResponse(final InputStream pInStream,
                                              final OutputStream pOutStream) throws GordianException {
        /* Decode the certificate response */
        final GordianPEMParser myParser = new GordianPEMParser();
        final List<GordianPEMObject> myObjects = myParser.parsePEMFile(pInStream);
        final GordianCertResponseASN1 myResponse = GordianPEMCoder.decodeCertResponse(myObjects);

        /* Access the original keyPair for the request */
        final GordianRequestCache myCache = theRequestMap.get(myResponse.getReqId());
        if (myCache == null) {
            throw new GordianDataException("Unrecognised request Id");
        }
        theRequestMap.remove(myResponse.getReqId());

        /* Process the certificate response */
        theParser.processCertificateResponse(myResponse, myCache.getKeyPair());
        final GordianCertificate[] myChain = myResponse.getCertificateChain(theEncryptor);

        /* Update the keyStore with the new certificate chain */
        final List<GordianCertificate> myList = List.of(myChain);
        theKeyStore.updateCertificateChain(myCache.getAlias(), myList);

        /* calculate the Digest value */
        final byte[] myDigest = theBuilder.calculateAckValue((GordianCoreCertificate) myChain[0]);
        final GordianCertAckASN1 myAck = new GordianCertAckASN1(myResponse.getRespId(), myDigest);

        /* Write out the response */
        final GordianPEMObject myPEMObject = GordianPEMCoder.createPEMObject(GordianPEMObjectType.CERTACK, myAck);
        myParser.writePEMFile(pOutStream, Collections.singletonList(myPEMObject));

        /* Return the response id */
        return myResponse.getRespId();
    }

    @Override
    public void processCertificateAck(final InputStream pInStream) throws GordianException {
        /* Decode the certificate ack */
        final GordianPEMParser myParser = new GordianPEMParser();
        final List<GordianPEMObject> myObjects = myParser.parsePEMFile(pInStream);
        final GordianCertAckASN1 myAck = GordianPEMCoder.decodeCertAck(myObjects);
        final Integer myRespId = myAck.getRespId();

        /* Access the original certificate for the ack */
        final GordianCoreCertificate myCert = theResponseMap.get(myRespId);
        if (myCert == null) {
            throw new GordianDataException("Unrecognised response Id");
        }
        theResponseMap.remove(myRespId);

        /* Check the Digest value */
        final byte[] myDigest = theBuilder.calculateAckValue(myCert);
        if (!Arrays.equals(myDigest, myAck.getDigestValue())) {
            throw new GordianDataException("Invalid Digest");
        }

        /* Store the certificate */
        theKeyStore.setCertificate(getCertificateAlias(myRespId), myCert);
    }

    @Override
    public GordianKeyStoreEntry importEntry(final InputStream pStream) throws GordianException {
        final GordianPEMCoder myCoder = new GordianPEMCoder(theKeyStore);
        myCoder.setLockResolver(theLockResolver);
        return myCoder.importKeyStoreEntry(pStream);
    }

    @Override
    public List<GordianKeyStoreEntry> importCertificates(final InputStream pStream) throws GordianException {
        final GordianPEMCoder myCoder = new GordianPEMCoder(theKeyStore);
        return myCoder.importCertificates(pStream);
    }

    /**
     * Get certificate alias.
     *
     * @param pRespId the response id
     * @return the alias
     */
    public String getCertificateAlias(final Integer pRespId) {
        return "AllocatedCertificate_" + pRespId;
    }

    /**
     * RequestMapCache.
     */
    static final class GordianRequestCache {
        /**
         * The alias.
         */
        private final String theAlias;

        /**
         * The KeyPair.
         */
        private final GordianKeyStorePair theKeyPair;

        /**
         * Constructor.
         *
         * @param pAlias   the alias
         * @param pKeyPair the keyPair
         */
        GordianRequestCache(final String pAlias,
                            final GordianKeyStorePair pKeyPair) {
            theAlias = pAlias;
            theKeyPair = pKeyPair;
        }

        /**
         * Obtain the alias.
         *
         * @return the alias
         */
        String getAlias() {
            return theAlias;
        }

        /**
         * Obtain the keyPair.
         *
         * @return the keyPair
         */
        GordianKeyStorePair getKeyPair() {
            return theKeyPair;
        }
    }
}