GordianCRMEncryptor.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.agree.GordianAgreement;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementFactory;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementParams;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementSpec;
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.cert.GordianCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianKeyPairUsage;
import io.github.tonywasher.joceanus.gordianknot.api.cert.GordianKeyPairUse;
import io.github.tonywasher.joceanus.gordianknot.api.encrypt.GordianEncryptor;
import io.github.tonywasher.joceanus.gordianknot.api.encrypt.GordianEncryptorSpec;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianAsyncFactory;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPair;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairSpec;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySet;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySetSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.agree.GordianCoreAgreementFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianParameters;
import io.github.tonywasher.joceanus.gordianknot.impl.core.cert.GordianCoreCertificate;
import io.github.tonywasher.joceanus.gordianknot.impl.core.encrypt.GordianCoreEncryptorFactory;
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.keyset.GordianKeySetSpecASN1;
import org.bouncycastle.asn1.BEROctetString;
import org.bouncycastle.asn1.cms.EncryptedContentInfo;
import org.bouncycastle.asn1.cms.IssuerAndSerialNumber;
import org.bouncycastle.asn1.cms.KeyTransRecipientInfo;
import org.bouncycastle.asn1.cms.RecipientIdentifier;
import org.bouncycastle.asn1.cms.RecipientInfo;
import org.bouncycastle.asn1.crmf.CRMFObjectIdentifiers;
import org.bouncycastle.asn1.crmf.EncKeyWithID;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.GeneralName;

import java.io.IOException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Arrays;

/**
 * Encryptor.
 */
public class GordianCRMEncryptor {
    /**
     * Server X500Name.
     */
    static final X500Name SERVER = new X500NameBuilder(BCStyle.INSTANCE).addRDN(BCStyle.CN, "Server").build();

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

    /**
     * Constructor.
     *
     * @param pFactory the factory
     */
    GordianCRMEncryptor(final GordianBaseFactory pFactory) {
        theFactory = pFactory;
    }

    /**
     * convert a certificate.
     *
     * @param pCertificate the certificate
     * @return the converted certificate
     * @throws GordianException on error
     */
    GordianCoreCertificate convertCertificate(final Certificate pCertificate) throws GordianException {
        return new GordianCoreCertificate(theFactory, pCertificate);
    }

    /**
     * Prepare for encryption.
     *
     * @param pCertificate the target certificate
     * @return the CRM result
     * @throws GordianException on error
     */
    GordianCRMResult prepareForEncryption(final GordianCoreCertificate pCertificate) throws GordianException {
        /* Try to send an encrypted proof */
        final GordianKeyPair myKeyPair = pCertificate.getKeyPair();
        final GordianKeyPairSpec mySpec = myKeyPair.getKeyPairSpec();
        final GordianEncryptorSpec myEncSpec = theFactory.getAsyncFactory().getEncryptorFactory().defaultForKeyPair(mySpec);
        if (myEncSpec != null) {
            return prepareForEncryption(myEncSpec, pCertificate);
        }

        /* Try to send an agreed proof */
        final GordianAgreementSpec myAgreeSpec = theFactory.getAsyncFactory().getAgreementFactory().defaultForKeyPair(mySpec);
        if (myAgreeSpec != null) {
            return prepareAgreedEncryption(myAgreeSpec, pCertificate);
        }

        /* Reject the request */
        throw new GordianDataException("Unable to prepare for encryption");
    }

    /**
     * Prepare an agreed encryption.
     *
     * @param pAgreeSpec   the agreementSpec
     * @param pCertificate the target certificate
     * @return the CRM result
     * @throws GordianException on error
     */
    private GordianCRMResult prepareAgreedEncryption(final GordianAgreementSpec pAgreeSpec,
                                                     final GordianCoreCertificate pCertificate) throws GordianException {
        /* Create the agreement */
        final GordianAsyncFactory myFactory = theFactory.getAsyncFactory();
        final GordianCoreAgreementFactory myAgreeFactory = (GordianCoreAgreementFactory) myFactory.getAgreementFactory();
        final GordianCertificate myCert = myAgreeFactory.newMiniCertificate(SERVER, pCertificate.getKeyPair(),
                new GordianKeyPairUsage(GordianKeyPairUse.AGREEMENT));
        final GordianAgreementParams myParams = myAgreeFactory.newAgreementParams(pAgreeSpec, new GordianKeySetSpec())
                .setServerCertificate(myCert);
        final GordianAgreement myAgree = myAgreeFactory.createAgreement(myParams);
        final byte[] myHello = myAgree.nextMessage();
        final GordianKeySet myKeySet = myAgree.getKeySetResult();

        /* Create the recipient info */
        final AlgorithmIdentifier myAlgId = myAgreeFactory.getIdentifierForSpec(pAgreeSpec);
        final IssuerAndSerialNumber myId = new IssuerAndSerialNumber(pCertificate.getIssuer().getName(), pCertificate.getSerialNo());
        final KeyTransRecipientInfo myKTInfo = new KeyTransRecipientInfo(new RecipientIdentifier(myId), myAlgId, new BEROctetString(myHello));
        final RecipientInfo myInfo = new RecipientInfo(myKTInfo);

        /* Return the result */
        return new GordianCRMResult(myInfo, myKeySet);
    }

    /**
     * Prepare for encryption.
     *
     * @param pEncryptSpec the encryptionSpec
     * @param pCertificate the target certificate
     * @return the CRM result
     * @throws GordianException on error
     */
    private GordianCRMResult prepareForEncryption(final GordianEncryptorSpec pEncryptSpec,
                                                  final GordianCoreCertificate pCertificate) throws GordianException {
        /* Create the random key */
        final byte[] myKey = createKeyForKeySet();

        /* Derive the keySet from the key */
        final GordianKeySet myKeySet = deriveKeySetFromKey(myKey.clone());

        /* Create recipientInfo */
        final RecipientInfo myInfo = createRecipientInfo(myKey, pCertificate, pEncryptSpec);

        /* Return the result */
        return new GordianCRMResult(myInfo, myKeySet);
    }

    /**
     * Create a random key for KeySet.
     *
     * @return the new key
     */
    private byte[] createKeyForKeySet() {
        final byte[] myKey = new byte[GordianLength.LEN_1024.getByteLength()];
        theFactory.getRandomSource().getRandom().nextBytes(myKey);
        return myKey;
    }

    /**
     * Derive a keySet from a key.
     *
     * @param pKey the key
     * @return the keySet
     * @throws GordianException on error
     */
    private GordianKeySet deriveKeySetFromKey(final byte[] pKey) throws GordianException {
        /* Create a new Factory using the key */
        final GordianParameters myParams = new GordianParameters(pKey);
        final GordianFactory myFactory = theFactory.newFactory(myParams);
        return myFactory.getEmbeddedKeySet();
    }

    /**
     * Encrypt the key with a keyPair.
     *
     * @param pKey         the key to encrypt
     * @param pCertificate the target certificate
     * @param pSpec        the encryptorSpec
     * @return the encrypted key
     * @throws GordianException on error
     */
    private RecipientInfo createRecipientInfo(final byte[] pKey,
                                              final GordianCoreCertificate pCertificate,
                                              final GordianEncryptorSpec pSpec) throws GordianException {
        /* Create the encrypted key */
        final GordianAsyncFactory myFactory = theFactory.getAsyncFactory();
        final GordianCoreEncryptorFactory myEncFactory = (GordianCoreEncryptorFactory) myFactory.getEncryptorFactory();
        final GordianEncryptor myEncryptor = myEncFactory.createEncryptor(pSpec);

        /* Create the encrypted key */
        myEncryptor.initForEncrypt(pCertificate.getKeyPair());
        final byte[] myEncryptedKey = myEncryptor.encrypt(pKey);
        Arrays.fill(pKey, (byte) 0);

        /* Build recipientInfo */
        final AlgorithmIdentifier myAlgId = myEncFactory.getIdentifierForSpec(pSpec);
        final IssuerAndSerialNumber myId = new IssuerAndSerialNumber(pCertificate.getIssuer().getName(), pCertificate.getSerialNo());
        final KeyTransRecipientInfo myKTInfo = new KeyTransRecipientInfo(new RecipientIdentifier(myId), myAlgId, new BEROctetString(myEncryptedKey));
        return new RecipientInfo(myKTInfo);
    }

    /**
     * Build the encryptedContentInfo for a PrivateKey.
     *
     * @param pKeySet        the keySet to encrypt with
     * @param pPKCS8Encoding the PKCS8Encoded privateKey
     * @param pCertificate   the local certificate
     * @return the encryptedContentInfo
     * @throws GordianException on error
     */
    public static EncryptedContentInfo buildEncryptedContentInfo(final GordianKeySet pKeySet,
                                                                 final PKCS8EncodedKeySpec pPKCS8Encoding,
                                                                 final GordianCertificate pCertificate) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Obtain the PrivateKeyInfo */
            final PrivateKeyInfo myInfo = PrivateKeyInfo.getInstance(pPKCS8Encoding.getEncoded());
            final EncKeyWithID myKeyWithId = new EncKeyWithID(myInfo, new GeneralName(pCertificate.getSubject().getName()));
            final byte[] myData = pKeySet.encryptBytes(myKeyWithId.getEncoded());
            final GordianKeySetSpecASN1 myASN1 = new GordianKeySetSpecASN1(pKeySet.getKeySetSpec());
            final AlgorithmIdentifier myAlgId = myASN1.getAlgorithmId();
            return new EncryptedContentInfo(CRMFObjectIdentifiers.id_ct_encKeyWithID, myAlgId, new BEROctetString(myData));

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

    /**
     * Build the encryptedContentInfo.
     *
     * @param pKeySet      the keySet to encrypt with
     * @param pCertificate the certificate to encrypt
     * @return the encryptedContentInfo
     * @throws GordianException on error
     */
    public static EncryptedContentInfo buildEncryptedContentInfo(final GordianKeySet pKeySet,
                                                                 final GordianCertificate pCertificate) throws GordianException {
        /* Obtain the encrypted certificate */
        final byte[] myData = pKeySet.encryptBytes(pCertificate.getEncoded());
        final GordianKeySetSpecASN1 myASN1 = new GordianKeySetSpecASN1(pKeySet.getKeySetSpec());
        final AlgorithmIdentifier myAlgId = myASN1.getAlgorithmId();
        return new EncryptedContentInfo(PKCSObjectIdentifiers.x509Certificate, myAlgId, new BEROctetString(myData));
    }

    /**
     * Derive the keySet via a keyPairSet issuer.
     *
     * @param pRecInfo     the recipient info
     * @param pCertificate the receiving certificate
     * @param pKeyPair     the keyPair
     * @return the keySet
     * @throws GordianException on error
     */
    public GordianKeySet deriveKeySetFromRecInfo(final KeyTransRecipientInfo pRecInfo,
                                                 final GordianCertificate pCertificate,
                                                 final GordianKeyPair pKeyPair) throws GordianException {
        /* Extract details */
        final AlgorithmIdentifier myAlgId = pRecInfo.getKeyEncryptionAlgorithm();
        final byte[] myEncryptedKey = pRecInfo.getEncryptedKey().getOctets();

        /* Derive the keySet appropriately */
        final GordianKeyPairUsage myUsage = pCertificate.getUsage();
        return myUsage.hasUse(GordianKeyPairUse.KEYENCRYPT)
                ? deriveEncryptedKeySet(pKeyPair, myAlgId, myEncryptedKey)
                : deriveAgreedKeySet(pKeyPair, myEncryptedKey);
    }

    /**
     * Derive an encrypted keySet.
     *
     * @param pKeyPair      the keyPair
     * @param pAlgId        the algorithm Identifier
     * @param pEncryptedKey the encrypted key
     * @return the derived keySet
     * @throws GordianException on error
     */
    private GordianKeySet deriveEncryptedKeySet(final GordianKeyPair pKeyPair,
                                                final AlgorithmIdentifier pAlgId,
                                                final byte[] pEncryptedKey) throws GordianException {
        /* Handle decryption */
        final GordianAsyncFactory myFactory = theFactory.getAsyncFactory();
        final GordianCoreEncryptorFactory myEncFactory = (GordianCoreEncryptorFactory) myFactory.getEncryptorFactory();
        final GordianEncryptorSpec myEncSpec = myEncFactory.getSpecForIdentifier(pAlgId);
        final GordianEncryptor myEncryptor = myEncFactory.createEncryptor(myEncSpec);
        myEncryptor.initForDecrypt(pKeyPair);
        final byte[] myKey = myEncryptor.decrypt(pEncryptedKey);
        final GordianKeySet myKeySet = deriveKeySetFromKey(myKey);
        Arrays.fill(myKey, (byte) 0);
        return myKeySet;
    }

    /**
     * Derive an agreed keySet.
     *
     * @param pKeyPair the keyPair
     * @param pHello   the clientHello
     * @return the derived keySet
     * @throws GordianException on error
     */
    private GordianKeySet deriveAgreedKeySet(final GordianKeyPair pKeyPair,
                                             final byte[] pHello) throws GordianException {
        /* Handle agreement */
        final GordianAsyncFactory myFactory = theFactory.getAsyncFactory();
        final GordianAgreementFactory myAgreeFactory = myFactory.getAgreementFactory();
        final GordianCertificate myCert = myAgreeFactory.newMiniCertificate(SERVER, pKeyPair, new GordianKeyPairUsage(GordianKeyPairUse.AGREEMENT));
        final GordianAgreement myAgree = myAgreeFactory.parseAgreementMessage(pHello);
        final GordianAgreementParams myParams = myAgree.getAgreementParams().setServerCertificate(myCert);
        myAgree.updateParams(myParams);
        return myAgree.getKeySetResult();
    }

    /**
     * Result class.
     */
    public static class GordianCRMResult {
        /**
         * Recipient info.
         */
        private final RecipientInfo theRecipient;

        /**
         * The keySet.
         */
        private final GordianKeySet theKeySet;

        /**
         * Constructor.
         *
         * @param pKeySet    the keySet
         * @param pRecipient the recipient.
         */
        GordianCRMResult(final RecipientInfo pRecipient,
                         final GordianKeySet pKeySet) {
            theRecipient = pRecipient;
            theKeySet = pKeySet;
        }

        /**
         * Obtain the recipient.
         *
         * @return the recipient
         */
        public RecipientInfo getRecipient() {
            return theRecipient;
        }

        /**
         * Obtain the keySet.
         *
         * @return the keySet
         */
        public GordianKeySet getKeySet() {
            return theKeySet;
        }
    }
}