GordianPEMCoder.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.base.GordianKeySpec;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianKnuthObfuscater;
import io.github.tonywasher.joceanus.gordianknot.api.key.GordianKey;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPair;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySet;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry.GordianKeyStoreCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry.GordianKeyStoreKey;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry.GordianKeyStorePair;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreEntry.GordianKeyStoreSet;
import io.github.tonywasher.joceanus.gordianknot.api.keystore.GordianKeyStoreGateway.GordianLockResolver;
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.exc.GordianIOException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianCoreKeyStoreEntry.GordianCoreKeyStoreCertificate;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianCoreKeyStoreEntry.GordianCoreKeyStoreKey;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianCoreKeyStoreEntry.GordianCoreKeyStorePair;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianCoreKeyStoreEntry.GordianCoreKeyStoreSet;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianPEMObject.GordianPEMObjectType;
import io.github.tonywasher.joceanus.gordianknot.impl.core.zip.GordianCoreZipLock;
import io.github.tonywasher.joceanus.gordianknot.impl.core.zip.GordianZipLockASN1;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.crmf.CertReqMsg;
import org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * PEM Coder/deCoder.
 */
public class GordianPEMCoder {
    /**
     * Unsupported objectType error.
     */
    private static final String ERROR_UNSUPPORTED = "Unsupported object type";

    /**
     * The Factory.
     */
    private final GordianBaseFactory theFactory;

    /**
     * The Parser.
     */
    private final GordianPEMParser theParser;

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

    /**
     * Constructor.
     *
     * @param pKeyStore the keyStore
     */
    GordianPEMCoder(final GordianCoreKeyStore pKeyStore) {
        /* Store details */
        theFactory = pKeyStore.getFactory();
        theParser = new GordianPEMParser();
    }

    /**
     * Set the lock resolver the lock resolver.
     *
     * @param pResolver the resolver
     */
    void setLockResolver(final GordianLockResolver pResolver) {
        theLockResolver = pResolver;
    }

    /**
     * Export a keyStoreEntry to stream.
     *
     * @param pEntry  the entry
     * @param pStream the output stream
     * @param pLock   the lock
     * @throws GordianException on error
     */
    public void exportKeyStoreEntry(final GordianKeyStoreEntry pEntry,
                                    final OutputStream pStream,
                                    final GordianCoreZipLock pLock) throws GordianException {
        /* Check that the lock is usable */
        if (pLock == null || !pLock.isFresh()) {
            throw new GordianDataException("Invalid lock");
        }
        pLock.markAsUsed();

        /* Encode and write the object */
        final List<GordianPEMObject> myObjects = encodeKeyStoreEntry(pEntry, pLock);
        theParser.writePEMFile(pStream, myObjects);
    }

    /**
     * Import a keyStoreEntry from stream.
     *
     * @param pStream the input stream
     * @return the decoded object.
     * @throws GordianException on error
     */
    public GordianKeyStoreEntry importKeyStoreEntry(final InputStream pStream) throws GordianException {
        final List<GordianPEMObject> myObjects = theParser.parsePEMFile(pStream);
        return decodePEMObjectList(myObjects);
    }

    /**
     * Import a list of certificates from stream.
     *
     * @param pStream the input stream
     * @return the list of certificates.
     * @throws GordianException on error
     */
    public List<GordianKeyStoreEntry> importCertificates(final InputStream pStream) throws GordianException {
        final List<GordianPEMObject> myObjects = theParser.parsePEMFile(pStream);
        return decodePEMCertificateList(myObjects);
    }

    /**
     * Encode a keyStoreEntry.
     *
     * @param pEntry the entry
     * @param pLock  the lock
     * @return the encoded object list.
     * @throws GordianException on error
     */
    private List<GordianPEMObject> encodeKeyStoreEntry(final GordianKeyStoreEntry pEntry,
                                                       final GordianCoreZipLock pLock) throws GordianException {
        /* Handle certificates */
        if (pEntry instanceof GordianKeyStoreCertificate myEntry) {
            final GordianCertificate myCert = myEntry.getCertificate();
            return Collections.singletonList(encodeCertificate(myCert));
        }

        /* Handle keyPair */
        if (pEntry instanceof GordianKeyStorePair myEntry) {
            return encodePrivateKeyPair(myEntry, pLock);
        }

        /* Handle keySet and key */
        if (pEntry instanceof GordianKeyStoreSet myEntry) {
            return Collections.singletonList(encodeKeySet(myEntry, pLock));
        }
        if (pEntry instanceof GordianKeyStoreKey<?> myEntry) {
            return Collections.singletonList(encodeKey(myEntry, pLock));
        }

        /* Unsupported entry */
        throw new GordianDataException(ERROR_UNSUPPORTED);
    }

    /**
     * Decode a PEMObject list.
     *
     * @param pObjects the object list
     * @return the decoded object.
     * @throws GordianException on error
     */
    private GordianKeyStoreEntry decodePEMObjectList(final List<GordianPEMObject> pObjects) throws GordianException {
        /* List must be non-empty */
        if (pObjects.isEmpty()) {
            throw new GordianDataException("Empty list");
        }

        /* Access first element and switch on object type */
        final GordianPEMObject myFirst = pObjects.get(0);
        switch (myFirst.getObjectType()) {
            /* Decode objects */
            case PRIVATEKEY:
                return decodeKeyPair(pObjects);
            case CERT:
                return decodeCertificate(pObjects);
            case KEYSET:
                return decodeKeySet(pObjects);
            case KEY:
                return decodeKey(pObjects);

            /* Unsupported entry */
            default:
                throw new GordianDataException(ERROR_UNSUPPORTED);
        }
    }

    /**
     * Decode a PEMCertificate list.
     *
     * @param pObjects the object list
     * @return the decoded list.
     * @throws GordianException on error
     */
    private List<GordianKeyStoreEntry> decodePEMCertificateList(final List<GordianPEMObject> pObjects) throws GordianException {
        /* List must be non-empty */
        if (pObjects.isEmpty()) {
            throw new GordianDataException("Empty list");
        }

        /* Prepare for loop */
        final List<GordianKeyStoreEntry> myChain = new ArrayList<>();
        final GordianPEMObjectType myType = pObjects.get(0).getObjectType();
        final LocalDate myDate = LocalDate.now();

        /* Loop through the objects */
        for (GordianPEMObject myObject : pObjects) {
            /* Check that the list is homogenous */
            if (myObject.getObjectType() != myType) {
                throw new GordianDataException("Inconsistent chain");
            }

            /* Decode objects */
            if (myObject.getObjectType() == GordianPEMObjectType.CERT) {
                final GordianCoreCertificate myKeyPairCert = decodeCertificate(myObject);
                myChain.add(new GordianCoreKeyStoreCertificate(myKeyPairCert, myDate));

                /* Unsupported entry */
            } else {
                throw new GordianDataException(ERROR_UNSUPPORTED);
            }
        }

        /* Return the chain */
        return myChain;
    }

    /**
     * Encode a Certificate.
     *
     * @param pCertificate the certificate
     * @return the encoded object.
     */
    static GordianPEMObject encodeCertificate(final GordianCertificate pCertificate) {
        return new GordianPEMObject(GordianPEMObjectType.CERT, pCertificate.getEncoded());
    }

    /**
     * Create a PEM Object.
     *
     * @param pObjectType the objectType
     * @param pObject     the object
     * @return the PEM Object
     * @throws GordianException on error
     */
    static GordianPEMObject createPEMObject(final GordianPEMObjectType pObjectType,
                                            final ASN1Object pObject) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Create a PEM Object */
            return new GordianPEMObject(pObjectType, pObject.getEncoded());

        } catch (IOException e) {
            throw new GordianIOException("Failed to create PEMObject", e);
        }
    }

    /**
     * Encode a keyPair.
     *
     * @param pKeyPair the keyPair
     * @param pLock    the lock
     * @return the encoded object.
     * @throws GordianException on error
     */
    private List<GordianPEMObject> encodePrivateKeyPair(final GordianKeyStorePair pKeyPair,
                                                        final GordianCoreZipLock pLock) throws GordianException {
        /* Create the list */
        final List<GordianPEMObject> myList = new ArrayList<>();

        /* Add the private key entry */
        myList.add(encodePrivateKey(pKeyPair, pLock));

        /* Loop through the certificates */
        for (GordianCertificate myCert : pKeyPair.getCertificateChain()) {
            /* Add the encoded certificate */
            myList.add(encodeCertificate(myCert));
        }

        /* Return the list */
        return myList;
    }

    /**
     * Encode a privateKey.
     *
     * @param pKeyPair the keyPair
     * @param pLock    the lock
     * @return the encoded object.
     * @throws GordianException on error
     */
    private GordianPEMObject encodePrivateKey(final GordianKeyStorePair pKeyPair,
                                              final GordianCoreZipLock pLock) throws GordianException {
        /* Protect against exception */
        try {
            /* Build encoded object and return it */
            final GordianKeySet myKeySet = pLock.getKeySet();
            final byte[] mySecuredKey = myKeySet.securePrivateKey(pKeyPair.getKeyPair());
            final EncryptedPrivateKeyInfo myInfo = buildPrivateKeyInfo(pLock, mySecuredKey);
            return new GordianPEMObject(GordianPEMObjectType.PRIVATEKEY, myInfo.getEncoded());

            /* Handle exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to encode privateKey", e);
        }
    }

    /**
     * Encode a keySet.
     *
     * @param pKeySet the keySet
     * @param pLock   the lock
     * @return the encoded object.
     * @throws GordianException on error
     */
    private GordianPEMObject encodeKeySet(final GordianKeyStoreSet pKeySet,
                                          final GordianCoreZipLock pLock) throws GordianException {
        /* Protect against exception */
        try {
            /* Build encoded object and return it */
            final GordianKeySet myKeySet = pLock.getKeySet();
            final byte[] mySecuredKeySet = myKeySet.secureKeySet(pKeySet.getKeySet());
            final EncryptedPrivateKeyInfo myInfo = buildPrivateKeyInfo(pLock, mySecuredKeySet);
            return new GordianPEMObject(GordianPEMObjectType.KEYSET, myInfo.getEncoded());

            /* Handle exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to encode keySet", e);
        }
    }

    /**
     * Encode a key.
     *
     * @param pKey  the key
     * @param pLock the Lock
     * @return the encoded object.
     * @throws GordianException on error
     */
    private GordianPEMObject encodeKey(final GordianKeyStoreKey<?> pKey,
                                       final GordianCoreZipLock pLock) throws GordianException {
        /* Protect against exception */
        try {
            /* Access keyType */
            final GordianKey<?> myKey = pKey.getKey();
            final GordianKnuthObfuscater myObfuscater = theFactory.getObfuscater();
            final int myId = myObfuscater.deriveExternalIdFromType(myKey.getKeyType());
            final byte[] myTypeDef = GordianDataConverter.integerToByteArray(myId);

            /* Secure the key */
            final GordianKeySet myKeySet = pLock.getKeySet();
            final byte[] mySecuredKey = myKeySet.secureKey(myKey);

            /* Build key definition */
            final byte[] myKeyDef = new byte[mySecuredKey.length + Integer.BYTES];
            System.arraycopy(mySecuredKey, 0, myKeyDef, Integer.BYTES, mySecuredKey.length);
            System.arraycopy(myTypeDef, 0, myKeyDef, 0, Integer.BYTES);

            /* Build encoded object and return it */
            final EncryptedPrivateKeyInfo myInfo = buildPrivateKeyInfo(pLock, myKeyDef);
            return new GordianPEMObject(GordianPEMObjectType.KEY, myInfo.getEncoded());

            /* Handle exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to encode keySet", e);
        }
    }

    /**
     * Decode a Certificate.
     *
     * @param pObjects the PEM object list
     * @return the Certificate.
     * @throws GordianException on error
     */
    private GordianKeyStoreCertificate decodeCertificate(final List<GordianPEMObject> pObjects) throws GordianException {
        /* Reject if not singleton list */
        checkSingletonList(pObjects);

        /* parse the certificate */
        return new GordianCoreKeyStoreCertificate(decodeCertificate(pObjects.get(0)), LocalDate.now());
    }

    /**
     * Decode a Certificate.
     *
     * @param pObject the PEM object
     * @return the Certificate.
     * @throws GordianException on error
     */
    private GordianCoreCertificate decodeCertificate(final GordianPEMObject pObject) throws GordianException {
        /* Reject if not keySetCertificate */
        checkObjectType(pObject, GordianPEMObjectType.CERT);

        /* parse the encoded bytes */
        return new GordianCoreCertificate(theFactory, pObject.getEncoded());
    }

    /**
     * Decode a Certificate Request.
     *
     * @param pObjects the PEM object list
     * @return the Certificate Request.
     * @throws GordianException on error
     */
    static CertReqMsg decodeCertRequest(final List<GordianPEMObject> pObjects) throws GordianException {
        /* Reject if not singleton list */
        checkSingletonList(pObjects);
        final GordianPEMObject myObject = pObjects.get(0);

        /* Reject if not certificateRequest */
        checkObjectType(myObject, GordianPEMObjectType.CERTREQ);

        /* parse the encoded bytes */
        return CertReqMsg.getInstance(myObject.getEncoded());
    }

    /**
     * Decode a Certificate Response.
     *
     * @param pObjects the PEM object list
     * @return the Certificate Response.
     * @throws GordianException on error
     */
    static GordianCertResponseASN1 decodeCertResponse(final List<GordianPEMObject> pObjects) throws GordianException {
        /* Reject if not singleton list */
        checkSingletonList(pObjects);
        final GordianPEMObject myObject = pObjects.get(0);

        /* Reject if not certificateResponse */
        checkObjectType(myObject, GordianPEMObjectType.CERTRESP);

        /* parse the encoded bytes */
        return GordianCertResponseASN1.getInstance(myObject.getEncoded());
    }

    /**
     * Decode a Certificate Ack.
     *
     * @param pObjects the PEM object list
     * @return the Certificate Ack.
     * @throws GordianException on error
     */
    static GordianCertAckASN1 decodeCertAck(final List<GordianPEMObject> pObjects) throws GordianException {
        /* Reject if not singleton list */
        checkSingletonList(pObjects);
        final GordianPEMObject myObject = pObjects.get(0);

        /* Reject if not certificateAck */
        checkObjectType(myObject, GordianPEMObjectType.CERTACK);

        /* parse the encoded bytes */
        return GordianCertAckASN1.getInstance(myObject.getEncoded());
    }

    /**
     * Decode a keyPair.
     *
     * @param pObjects the list of objects
     * @return the keyPair.
     * @throws GordianException on error
     */
    private GordianKeyStorePair decodeKeyPair(final List<GordianPEMObject> pObjects) throws GordianException {
        /* Initialise variables */
        EncryptedPrivateKeyInfo myPrivateInfo = null;
        final List<GordianCertificate> myChain = new ArrayList<>();

        /* Loop through the entries */
        for (GordianPEMObject myObject : pObjects) {
            /* Decode private key if first element */
            if (myPrivateInfo == null) {
                myPrivateInfo = EncryptedPrivateKeyInfo.getInstance(myObject.getEncoded());

                /* else decode next certificate in chain */
            } else {
                myChain.add(decodeCertificate(myObject));
            }
        }

        /* Check that we have a privateKey and at least one certificate */
        if (myPrivateInfo == null || myChain.isEmpty()) {
            throw new GordianDataException("Insufficient entries");
        }

        /* Derive the keyPair */
        final GordianKeySet mySecuringKeySet = deriveSecuringKeySet(myPrivateInfo);
        final GordianCoreCertificate myCert = (GordianCoreCertificate) myChain.get(0);
        final GordianKeyPair myPair = mySecuringKeySet.deriveKeyPair(myCert.getX509KeySpec(), myPrivateInfo.getEncryptedData());

        /* Return the new keyPair */
        return new GordianCoreKeyStorePair(myPair, myChain, LocalDate.now());
    }

    /**
     * Decode a keySet.
     *
     * @param pObjects the PEM object list
     * @return the keySet.
     * @throws GordianException on error
     */
    private GordianKeyStoreSet decodeKeySet(final List<GordianPEMObject> pObjects) throws GordianException {
        checkSingletonList(pObjects);
        return decodeKeySet(pObjects.get(0));
    }

    /**
     * Decode a keySet.
     *
     * @param pObject the PEM object
     * @return the keySet.
     * @throws GordianException on error
     */
    private GordianCoreKeyStoreSet decodeKeySet(final GordianPEMObject pObject) throws GordianException {
        /* Reject if not KeySet */
        checkObjectType(pObject, GordianPEMObjectType.KEYSET);

        /* Derive the securing keySet */
        final EncryptedPrivateKeyInfo myInfo = EncryptedPrivateKeyInfo.getInstance(pObject.getEncoded());
        final GordianKeySet mySecuringKeySet = deriveSecuringKeySet(myInfo);

        /* Derive the keySet */
        final GordianKeySet myKeySet = mySecuringKeySet.deriveKeySet(myInfo.getEncryptedData());
        return new GordianCoreKeyStoreSet(myKeySet, LocalDate.now());
    }

    /**
     * Decode a key.
     *
     * @param pObjects the PEM object list
     * @return the key.
     * @throws GordianException on error
     */
    private GordianKeyStoreKey<?> decodeKey(final List<GordianPEMObject> pObjects) throws GordianException {
        checkSingletonList(pObjects);
        return decodeKey(pObjects.get(0));
    }

    /**
     * Decode a key.
     *
     * @param pObject the PEM object
     * @return the key.
     * @throws GordianException on error
     */
    private GordianCoreKeyStoreKey<?> decodeKey(final GordianPEMObject pObject) throws GordianException {
        /* Reject if not Key */
        checkObjectType(pObject, GordianPEMObjectType.KEY);

        /* Derive the securing keySet */
        final EncryptedPrivateKeyInfo myInfo = EncryptedPrivateKeyInfo.getInstance(pObject.getEncoded());
        final GordianKeySet mySecuringKeySet = deriveSecuringKeySet(myInfo);

        /* Extract key definition */
        final byte[] mySecured = myInfo.getEncryptedData();
        final byte[] myTypeDef = new byte[Integer.BYTES];
        System.arraycopy(mySecured, 0, myTypeDef, 0, Integer.BYTES);
        final byte[] myKeyDef = new byte[mySecured.length - Integer.BYTES];
        System.arraycopy(mySecured, Integer.BYTES, myKeyDef, 0, mySecured.length - Integer.BYTES);

        /* Obtain the keySpec */
        final GordianKnuthObfuscater myObfuscater = theFactory.getObfuscater();
        final int myType = GordianDataConverter.byteArrayToInteger(myTypeDef);
        final GordianKeySpec myKeyType = (GordianKeySpec) myObfuscater.deriveTypeFromExternalId(myType);

        /* Derive the key */
        final GordianKey<?> myKey = mySecuringKeySet.deriveKey(myKeyDef, myKeyType);
        return new GordianCoreKeyStoreKey<>(myKey, LocalDate.now());
    }

    /**
     * Build EncryptedPrivateKeyInfo.
     *
     * @param pLock the Lock
     * @param pInfo the encryptedInfo
     * @return the algorithmId.
     */
    private static EncryptedPrivateKeyInfo buildPrivateKeyInfo(final GordianCoreZipLock pLock,
                                                               final byte[] pInfo) {
        return new EncryptedPrivateKeyInfo(pLock.getAlgorithmId(), pInfo);
    }

    /**
     * Derive securing keySet.
     *
     * @param pInfo the encrypted private keyInfo
     * @return the keySet
     * @throws GordianException on error
     */
    private GordianKeySet deriveSecuringKeySet(final EncryptedPrivateKeyInfo pInfo) throws GordianException {
        /* Validate the algorithmId */
        final AlgorithmIdentifier myId = pInfo.getEncryptionAlgorithm();
        if (!myId.getAlgorithm().equals(GordianZipLockASN1.LOCKOID)) {
            throw new GordianDataException("Unsupported algorithm");
        }
        if (theLockResolver == null) {
            throw new GordianDataException("No lock resolver set");
        }

        /* Resolve the lock */
        final GordianCoreZipLock myLock = new GordianCoreZipLock(theFactory, myId.getParameters());
        theLockResolver.resolveLock(myLock);
        if (myLock.isLocked()) {
            throw new GordianDataException("Lock was not resolved");
        }

        /* Derive the securing keySet */
        return myLock.getKeySet();
    }

    /**
     * Check for singleton list.
     *
     * @param pObjects the object list
     * @throws GordianException on error
     */
    private static void checkSingletonList(final List<GordianPEMObject> pObjects) throws GordianException {
        /* Throw error on non-singleton */
        if (pObjects.size() != 1) {
            throw new GordianDataException("Too many objects");
        }
    }

    /**
     * Check PEM objectType.
     *
     * @param pObject   the objectType
     * @param pRequired the required objectType
     * @throws GordianException on error
     */
    static void checkObjectType(final GordianPEMObject pObject,
                                final GordianPEMObjectType pRequired) throws GordianException {
        /* Throw error on mismatch */
        final GordianPEMObjectType myType = pObject.getObjectType();
        if (myType != pRequired) {
            throw new GordianDataException("unexpected objectType " + myType + " - Expected " + pRequired);
        }
    }
}