GordianCoreKeyStore.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.cert.GordianCertificateId;
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.lock.GordianPasswordLockSpec;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipFactory;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipLock;
import io.github.tonywasher.joceanus.gordianknot.api.zip.GordianZipWriteFile;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
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.GordianKeyStoreElement.GordianKeyStoreCertificateElement;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianKeyStoreElement.GordianKeyStoreCertificateHolder;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianKeyStoreElement.GordianKeyStoreKeyElement;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianKeyStoreElement.GordianKeyStorePairElement;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianKeyStoreElement.GordianKeyStoreSetElement;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.x500.X500Name;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigInteger;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

/**
 * KeyStore implementation.
 * <p>
 * It should be noted that this implementation is based upon a number of assumptions.
 * </p>
 * <ol>
 *     <li>The combination of Subject Name/Subject Id is viewed as a unique identifier for a public key. Multiple Certificates for the same subject,
 *     issued by different authorities must describe the same publicKey. It is expected that different publicKeys for the same subjectName
 *     WILL have different subjectIDs.</li>
 *     <li>An issuerCertificate will only issue a single certificate at a time for a subjectName/subjectID combination.
 *     If two such certificates are received, the later one will overwrite the first one, (assuming the publicKey is the same).
 *     No attempt will be made to determine a BETTER certificate (e.g. longer validity)</li>
 * </ol>
 */
public class GordianCoreKeyStore
        implements GordianBaseKeyStore {
    /**
     * The ZipFile entry name.
     */
    static final String ZIPENTRY = "KeyStore";

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

    /**
     * The passwordLockSpec.
     */
    private final GordianPasswordLockSpec thePasswordLockSpec;

    /**
     * The map of certificates by Subject.
     */
    private final Map<GordianCertificateId, Map<GordianCertificateId, GordianCertificate>> theSubjectCerts;

    /**
     * The map of certificates by Issuer.
     */
    private final Map<GordianCertificateId, Map<GordianCertificateId, GordianCertificate>> theIssuerCerts;

    /**
     * The aliases.
     */
    private final Map<String, GordianKeyStoreEntry> theAliases;

    /**
     * Constructor.
     *
     * @param pFactory the factory
     * @param pSpec    the passwordLockSpec
     */
    GordianCoreKeyStore(final GordianBaseFactory pFactory,
                        final GordianPasswordLockSpec pSpec) {
        /* Store parameters */
        theFactory = pFactory;
        thePasswordLockSpec = pSpec;

        /* Create the maps */
        theSubjectCerts = new LinkedHashMap<>();
        theIssuerCerts = new LinkedHashMap<>();
        theAliases = new LinkedHashMap<>();
    }

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

    /**
     * Obtain the passwordLockSpec.
     *
     * @return the passwordLockSpec
     */
    public GordianPasswordLockSpec getPasswordLockSpec() {
        return thePasswordLockSpec;
    }

    @Override
    public Map<GordianCertificateId, Map<GordianCertificateId, GordianCertificate>> getSubjectMapOfMaps() {
        return theSubjectCerts;
    }

    /**
     * Obtain the issuerMapofMaps.
     *
     * @return the map
     */
    private Map<GordianCertificateId, Map<GordianCertificateId, GordianCertificate>> getIssuerMapOfMaps() {
        return theIssuerCerts;
    }

    @Override
    public Map<String, GordianKeyStoreEntry> getAliasMap() {
        return theAliases;
    }

    @Override
    public GordianCertificate getCertificate(final GordianKeyStoreCertificateKey pKey) {
        final Map<GordianCertificateId, GordianCertificate> myCertMap = theSubjectCerts.get(pKey.getSubject());
        return myCertMap == null ? null : myCertMap.get(pKey.getIssuer());
    }

    @Override
    public List<String> getAliases() {
        return new ArrayList<>(theAliases.keySet());
    }

    @Override
    public boolean containsAlias(final String pAlias) {
        return theAliases.containsKey(pAlias);
    }

    @Override
    public int size() {
        return theAliases.size();
    }

    /**
     * Reset the store.
     */
    public void reset() {
        theAliases.clear();
        theSubjectCerts.clear();
        theIssuerCerts.clear();
    }

    @Override
    public void deleteEntry(final String pAlias) {
        /* Remove the existing entry */
        final GordianKeyStoreEntry myEntry = theAliases.remove(pAlias);

        /* Nothing more to do unless we are removing a certificate */
        if (!(myEntry instanceof GordianKeyStoreCertificateHolder myCertHolder)) {
            return;
        }

        /* Access the certificate */
        final GordianCertificate myCert = getCertificate(myCertHolder.getCertificateKey());

        /* If the certificate is not referenced by any other alias */
        if (getCertificateAlias(myCert) == null) {
            /* Remove the certificate from the maps */
            removeCertificate(myCert);
        }
    }

    /**
     * Remove certificate from maps.
     *
     * @param pCertificate the certificate to remove
     */
    private void removeCertificate(final GordianCertificate pCertificate) {
        /* Access the ids of the certificate */
        final GordianCertificateId mySubjectId = pCertificate.getSubject();
        final GordianCertificateId myIssuerId = pCertificate.getIssuer();

        /* If it is not referenced as issuer by any other certificate */
        Map<GordianCertificateId, GordianCertificate> myIssuedMap = theIssuerCerts.get(mySubjectId);
        if (isWithoutIssue(myIssuedMap)) {
            /* If the certificate is not self-signed */
            if (!pCertificate.isSelfSigned()) {
                /* Remove the certificate from the issuer map */
                myIssuedMap = theIssuerCerts.get(myIssuerId);
                myIssuedMap.remove(mySubjectId);

                /* If the issuer now has no issued certificates */
                if (isWithoutIssue(myIssuedMap)) {
                    /* Purge orphan issuers */
                    purgeOrphanIssuers(myIssuerId);
                }
            }

            /* Remove the certificate from the subject map */
            final Map<GordianCertificateId, GordianCertificate> myCertMap = theSubjectCerts.get(mySubjectId);
            myCertMap.remove(myIssuerId);
            if (myCertMap.isEmpty()) {
                theSubjectCerts.remove(mySubjectId);
            }
        }
    }

    /**
     * Determine whether an issuer has no children (other than self).
     *
     * @param pIssuer the issuer Map
     * @return true/false
     */
    private static boolean isWithoutIssue(final Map<GordianCertificateId, GordianCertificate> pIssuer) {
        /* If the map is null or empty then there are no children */
        if (pIssuer == null || pIssuer.isEmpty()) {
            return true;
        }

        /* If there is only one certificate in the map */
        if (pIssuer.size() == 1) {
            /* If the single certificate is self-signed then we are childless */
            final GordianCertificate myCert = pIssuer.values().iterator().next();
            return myCert.isSelfSigned();
        }

        /* Not childless */
        return false;
    }

    /**
     * Purge orphan issuers.
     *
     * @param pIssuerId the issuerId to purge
     */
    private void purgeOrphanIssuers(final GordianCertificateId pIssuerId) {
        /* Remove the entry */
        theIssuerCerts.remove(pIssuerId);

        /* Look for all issuers */
        final Map<GordianCertificateId, GordianCertificate> myCertMap = theSubjectCerts.get(pIssuerId);
        final List<GordianCertificate> myCerts = new ArrayList<>(myCertMap.values());

        /* Loop through all the certificates */
        for (GordianCertificate myCert : myCerts) {
            /* If the certificate is not referenced by any other alias */
            if (getCertificateAlias(myCert) == null) {
                /* Remove the certificate from the maps */
                removeCertificate(myCert);
            }
        }
    }

    @Override
    public void setCertificate(final String pAlias,
                               final GordianCertificate pCertificate) throws GordianException {
        /* Check the alias */
        checkAlias(pAlias);

        /* Check that we are not about to replace a non-certificate */
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        if (myEntry != null
                && !(myEntry instanceof GordianCoreKeyStoreCertificate)) {
            throw new GordianDataException("Alias already exists for non-certificate");
        }

        /* Remove any existing entry */
        deleteEntry(pAlias);

        /* Set the new value */
        final GordianKeyStoreCertificateElement myCert = new GordianKeyStoreCertificateElement(pCertificate);
        theAliases.put(pAlias, myCert);

        /* Store certificate */
        storeCertificate(pCertificate);
    }

    @Override
    public void setKeyPair(final String pAlias,
                           final GordianKeyPair pKeyPair,
                           final char[] pPassword,
                           final List<GordianCertificate> pCertificateChain) throws GordianException {
        /* Check the alias */
        checkAlias(pAlias);

        /* Make sure that the keyPair has a private key */
        if (pKeyPair.isPublicOnly()) {
            throw new GordianDataException("Private Key missing");
        }

        /* Make sure that we have a valid certificate chain */
        checkChain(pKeyPair, pCertificateChain);

        /* Remove any existing entry */
        deleteEntry(pAlias);

        /* Set the new value */
        final GordianKeyStorePairElement myPair = new GordianKeyStorePairElement(theFactory, thePasswordLockSpec, pKeyPair, pPassword, pCertificateChain);
        theAliases.put(pAlias, myPair);

        /* Store all the certificates in the chain */
        for (GordianCertificate myCert : pCertificateChain) {
            storeCertificate(myCert);
        }
    }

    @Override
    public void updateCertificateChain(final String pAlias,
                                       final List<GordianCertificate> pCertificateChain) throws GordianException {
        /* Obtain the keyStore Entry */
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        if (!(myEntry instanceof GordianKeyStorePairElement myKeyPairElement)) {
            throw new GordianDataException("Entry not found");
        }

        /* Access old keyPair */
        final List<GordianCertificate> myChain = myKeyPairElement.buildChain(this);
        final GordianKeyPair myKeyPair = myChain.getFirst().getKeyPair();

        /* Make sure that we have a valid certificate chain */
        checkChain(myKeyPair, pCertificateChain);

        /* Access old certificate */
        final GordianCertificate myOldCert = getCertificate(myKeyPairElement.getCertificateKey());

        /* If the certificate is not referenced by any other alias */
        if (getCertificateAlias(myOldCert) == null) {
            /* Remove the certificate from the maps */
            removeCertificate(myOldCert);
        }

        /* Update the chain */
        myKeyPairElement.updateChain(pCertificateChain);

        /* Store all the certificates in the chain */
        for (GordianCertificate myCert : pCertificateChain) {
            storeCertificate(myCert);
        }
    }

    @Override
    public <T extends GordianKeySpec> void setKey(final String pAlias,
                                                  final GordianKey<T> pKey,
                                                  final char[] pPassword) throws GordianException {
        /* Check the alias */
        checkAlias(pAlias);

        /* Remove any existing entry */
        deleteEntry(pAlias);

        /* Set the new value */
        final GordianKeyStoreKeyElement<T> myKey = new GordianKeyStoreKeyElement<>(theFactory, thePasswordLockSpec, pKey, pPassword);
        theAliases.put(pAlias, myKey);
    }

    @Override
    public void setKeySet(final String pAlias,
                          final GordianKeySet pKeySet,
                          final char[] pPassword) throws GordianException {
        /* Check the alias */
        checkAlias(pAlias);

        /* Remove any existing entry */
        deleteEntry(pAlias);

        /* Set the new value */
        final GordianKeyStoreSetElement mySet = new GordianKeyStoreSetElement(theFactory, thePasswordLockSpec, pKeySet, pPassword);
        theAliases.put(pAlias, mySet);
    }

    /**
     * Check that the alias is valid.
     *
     * @param pAlias the alias
     */
    private static void checkAlias(final String pAlias) {
        if (pAlias == null) {
            throw new NullPointerException();
        }
    }

    @Override
    public <T extends GordianKeyStoreEntry> boolean entryInstanceOf(final String pAlias,
                                                                    final Class<T> pClazz) {
        if (pClazz.isAssignableFrom(GordianKeyStoreCertificate.class)) {
            return isCertificateEntry(pAlias);
        }
        if (pClazz.isAssignableFrom(GordianKeyStorePair.class)) {
            return isKeyPairEntry(pAlias);
        }
        if (pClazz.isAssignableFrom(GordianKeyStoreKey.class)) {
            return isKeyEntry(pAlias);
        }
        if (pClazz.isAssignableFrom(GordianKeyStoreSet.class)) {
            return isKeySetEntry(pAlias);
        }
        return false;
    }

    @Override
    public boolean isCertificateEntry(final String pAlias) {
        return theAliases.get(pAlias) instanceof GordianKeyStoreCertificateElement;
    }

    @Override
    public boolean isKeyPairEntry(final String pAlias) {
        return theAliases.get(pAlias) instanceof GordianKeyStorePairElement;
    }

    @Override
    public boolean isKeyEntry(final String pAlias) {
        return theAliases.get(pAlias) instanceof GordianKeyStoreKeyElement;
    }

    @Override
    public boolean isKeySetEntry(final String pAlias) {
        return theAliases.get(pAlias) instanceof GordianKeyStoreSetElement;
    }

    @Override
    public GordianKeyStoreEntry getEntry(final String pAlias,
                                         final char[] pPassword) throws GordianException {
        if (isCertificateEntry(pAlias)) {
            return getKeyStoreCertificate(pAlias);
        }
        if (isKeyPairEntry(pAlias)) {
            return getKeyStorePair(pAlias, pPassword);
        }
        if (isKeyEntry(pAlias)) {
            return getKeyStoreKey(pAlias, pPassword);
        }
        return isKeySetEntry(pAlias)
                ? getKeyStoreSet(pAlias, pPassword)
                : null;
    }

    @Override
    public GordianCertificate getCertificate(final String pAlias) {
        final GordianKeyStoreCertificate myCert = getKeyStoreCertificate(pAlias);
        if (myCert != null) {
            return myCert.getCertificate();
        }
        final List<GordianCertificate> myChain = getCertificateChain(pAlias);
        return myChain == null || myChain.isEmpty() ? null : myChain.getFirst();
    }

    /**
     * Obtain the keyStorePairCertificate.
     *
     * @param pAlias the alias
     * @return the certificate entry (or null)
     */
    private GordianKeyStoreCertificate getKeyStoreCertificate(final String pAlias) {
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        return myEntry instanceof GordianKeyStoreCertificateElement myElement
                ? myElement.buildEntry(this)
                : null;
    }

    @Override
    public GordianKeyPair getKeyPair(final String pAlias,
                                     final char[] pPassword) throws GordianException {
        final GordianKeyStorePair myPair = getKeyStorePair(pAlias, pPassword);
        return myPair == null ? null : myPair.getKeyPair();
    }

    @Override
    public List<GordianCertificate> getCertificateChain(final String pAlias) {
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        return myEntry instanceof GordianKeyStorePairElement myElement
                ? myElement.buildChain(this)
                : null;
    }

    /**
     * Obtain the keyStorePair.
     *
     * @param pAlias    the alias
     * @param pPassword the password
     * @return the keyPair entry (or null)
     */
    private GordianKeyStorePair getKeyStorePair(final String pAlias,
                                                final char[] pPassword) throws GordianException {
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        return myEntry instanceof GordianKeyStorePairElement myElement
                ? myElement.buildEntry(this, pPassword)
                : null;
    }

    @Override
    public <T extends GordianKeySpec> GordianKey<T> getKey(final String pAlias,
                                                           final char[] pPassword) throws GordianException {
        final GordianKeyStoreKey<T> myKey = getKeyStoreKey(pAlias, pPassword);
        return myKey == null ? null : myKey.getKey();
    }

    /**
     * Obtain the keyStoreKey.
     *
     * @param <T>       the keyType
     * @param pAlias    the alias
     * @param pPassword the password
     * @return the key entry (or null)
     */
    @SuppressWarnings("unchecked")
    private <T extends GordianKeySpec> GordianKeyStoreKey<T> getKeyStoreKey(final String pAlias,
                                                                            final char[] pPassword) throws GordianException {
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        return myEntry instanceof GordianKeyStoreKeyElement
                ? ((GordianKeyStoreKeyElement<T>) myEntry).buildEntry(this, pPassword)
                : null;
    }

    @Override
    public GordianKeySet getKeySet(final String pAlias,
                                   final char[] pPassword) throws GordianException {
        final GordianKeyStoreSet mySet = getKeyStoreSet(pAlias, pPassword);
        return mySet == null ? null : mySet.getKeySet();
    }

    /**
     * Obtain the keyStoreKeySet.
     *
     * @param pAlias    the alias
     * @param pPassword the password
     * @return the keySet entry (or null)
     */
    private GordianKeyStoreSet getKeyStoreSet(final String pAlias,
                                              final char[] pPassword) throws GordianException {
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        return myEntry instanceof GordianKeyStoreSetElement myElement
                ? myElement.buildEntry(this, pPassword)
                : null;
    }

    @Override
    public LocalDate getCreationDate(final String pAlias) {
        final GordianKeyStoreEntry myEntry = theAliases.get(pAlias);
        return myEntry != null ? myEntry.getCreationDate() : null;
    }

    @Override
    public String getCertificateAlias(final GordianCertificate pCertificate) {
        /* Loop through the alias entries */
        for (Entry<String, GordianKeyStoreEntry> myRecord : theAliases.entrySet()) {
            /* Check for match on certificate entry */
            final GordianKeyStoreEntry myEntry = myRecord.getValue();
            if (myEntry instanceof GordianKeyStoreCertificateHolder myHolder) {
                final GordianKeyStoreCertificateKey myKey = myHolder.getCertificateKey();
                final GordianCertificate myCert = getCertificate(myKey);
                if (pCertificate.equals(myCert)) {
                    return myRecord.getKey();
                }
            }
        }

        /* Not found */
        return null;
    }

    @Override
    public void storeCertificate(final GordianCertificate pCertificate) {
        /* Access the ids */
        final GordianCertificateId mySubjectId = pCertificate.getSubject();
        final GordianCertificateId myIssuerId = pCertificate.getIssuer();

        /* Add the certificate to the list of certificates for this subject */
        Map<GordianCertificateId, GordianCertificate> myMap = theSubjectCerts.computeIfAbsent(mySubjectId, i -> new LinkedHashMap<>());
        myMap.put(myIssuerId, pCertificate);

        /* Add the certificate to the list of certificates for this issuer */
        myMap = theIssuerCerts.computeIfAbsent(myIssuerId, i -> new LinkedHashMap<>());
        myMap.put(mySubjectId, pCertificate);
    }

    /**
     * Check validity of certificate chain.
     *
     * @param pKeyPair the keyPair
     * @param pChain   the certificate chain
     * @throws GordianException on error
     */
    private void checkChain(final GordianKeyPair pKeyPair,
                            final List<GordianCertificate> pChain) throws GordianException {
        /* Make sure that we have a chain */
        if (pChain == null || pChain.isEmpty()) {
            throw new GordianDataException("Empty chain");
        }

        /* Make sure that the keyPair matches end-entity certificate */
        final GordianCoreCertificate myCert = (GordianCoreCertificate) pChain.getFirst();
        if (!myCert.checkMatchingPublicKey(pKeyPair)) {
            throw new GordianDataException("End-entity certificate does not match keyPair");
        }

        /* Loop through the certificates */
        final int mySize = pChain.size();
        for (int i = 0; i < mySize - 1; i++) {
            /* Access the certificate */
            final GordianCoreCertificate myTestCert = (GordianCoreCertificate) pChain.get(i);
            final GordianCoreCertificate mySignerCert = (GordianCoreCertificate) pChain.get(i + 1);

            /* Check the hierarchy */
            if (!myTestCert.validateCertificate(mySignerCert)) {
                throw new GordianDataException("Invalid certificate in path");
            }

            /* Look up any existing certificate for the signer */
            final Map<GordianCertificateId, GordianCertificate> myMap = theSubjectCerts.get(mySignerCert.getSubject());
            if (myMap != null) {
                final GordianCoreCertificate myExisting = (GordianCoreCertificate) myMap.values().iterator().next();
                if (!myExisting.checkMatchingPublicKey(mySignerCert.getKeyPair())) {
                    throw new GordianDataException("Intermediate certificate does not match existing keyPair");
                }
            }
        }

        /* Check that we are anchored by a root certificate */
        final GordianCertificate myRoot = pChain.get(mySize - 1);
        if (!((GordianCoreCertificate) myRoot).validateRootCertificate()) {
            throw new GordianDataException("Invalid root certificate");
        }

        /* Check that the root is known if the depth is greater than 1 */
        if (mySize > 1 && getCertificateAlias(myRoot) == null) {
            throw new GordianDataException("Unknown root certificate");
        }
    }

    @Override
    public String findIssuerCert(final IssuerAndSerialNumber pIssuer) throws GordianException {
        /* Loop through the alias entries */
        final X500Name myIssuer = pIssuer.getName();
        final BigInteger mySerial = pIssuer.getSerialNumber().getValue();
        for (Entry<String, GordianKeyStoreEntry> myEntry : theAliases.entrySet()) {
            /* If this is a keyPair(Set) entry */
            if (myEntry.getValue() instanceof GordianKeyStorePairElement myPair) {
                /* Access details */
                final GordianKeyStoreCertificateKey myCertKey = myPair.getCertificateChain().getFirst();
                final GordianCoreCertificate myCert = (GordianCoreCertificate) getCertificate(myCertKey);

                /* Return alias if we have a match */
                if (myIssuer.equals(myCert.getIssuer().getName())
                        && mySerial.equals(myCert.getSerialNo())) {
                    return myEntry.getKey();
                }
            }
        }

        /* Reject request */
        throw new GordianDataException("Issuer not found");
    }

    @Override
    public void storeToFile(final File pFile,
                            final char[] pPassword) throws GordianException {
        try {
            storeToStream(new FileOutputStream(pFile), pPassword);
        } catch (IOException e) {
            throw new GordianIOException("Failed to store to file", e);
        }
    }

    @Override
    public void storeToStream(final OutputStream pOutputStream,
                              final char[] pPassword) throws GordianException {
        /* Access the Factories */
        final GordianZipFactory myZipFactory = theFactory.getZipFactory();

        /* Create the lock */
        final GordianZipLock myLock = myZipFactory.factoryZipLock(pPassword);

        /* Create the Zip file */
        try (GordianZipWriteFile myZipFile = myZipFactory.createZipFile(myLock, pOutputStream)) {
            /* Create the XML representation */
            final GordianKeyStoreDocument myDocument = new GordianKeyStoreDocument(this);

            /* Write the document to the file */
            myZipFile.writeXMLDocument(new File(ZIPENTRY), myDocument.getDocument());

            /* Catch Exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to store to stream", e);
        }
    }

    @Override
    public boolean equals(final Object pThat) {
        /* Handle the trivial case */
        if (pThat == this) {
            return true;
        }
        if (pThat == null) {
            return false;
        }

        /* Ensure object is correct class */
        return pThat instanceof GordianCoreKeyStore myThat
                && theSubjectCerts.equals(myThat.getSubjectMapOfMaps())
                && theIssuerCerts.equals(myThat.getIssuerMapOfMaps())
                && theAliases.equals(myThat.getAliasMap());
    }

    @Override
    public int hashCode() {
        return Objects.hash(theSubjectCerts, theIssuerCerts, theAliases);
    }
}