GordianCoreAgreementBuilder.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.agree;

import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementSpec;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementStatus;
import io.github.tonywasher.joceanus.gordianknot.api.agree.GordianAgreementType;
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.keypair.GordianKeyPair;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairFactory;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairGenerator;
import io.github.tonywasher.joceanus.gordianknot.api.keypair.GordianKeyPairType;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMac;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacFactory;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacSpec;
import io.github.tonywasher.joceanus.gordianknot.api.mac.GordianMacSpecBuilder;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignParams;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignature;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignatureSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.agree.GordianCoreAgreementCalculator.GordianDerivationId;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianBaseFactory;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.sign.GordianCoreSignatureFactory;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.util.Arrays;

import java.security.SecureRandom;
import java.security.spec.X509EncodedKeySpec;

/**
 * Agreement Builder.
 */
public class GordianCoreAgreementBuilder {
    /**
     * InitVectorLength.
     */
    private static final int INITLEN = 32;

    /**
     * The Supplier.
     */
    private final GordianCoreAgreementSupplier theSupplier;

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

    /**
     * Secure Random.
     */
    private final SecureRandom theRandom;

    /**
     * The KeyPair factory.
     */
    private final GordianKeyPairGenerator theKeyPairGenerator;

    /**
     * The state.
     */
    private final GordianCoreAgreementState theState;

    /**
     * The result calculator.
     */
    private final GordianCoreAgreementCalculator theResultCalc;

    /**
     * Should we fail signature during testing?
     */
    private boolean failSignature;

    /**
     * Should we fail confirmation during testing?
     */
    private boolean failConfirmation;

    /**
     * Constructor.
     *
     * @param pSupplier the supplier
     * @param pSpec     the agreement spec
     * @throws GordianException on error
     */
    GordianCoreAgreementBuilder(final GordianCoreAgreementSupplier pSupplier,
                                final GordianAgreementSpec pSpec) throws GordianException {
        /* Store parameters and access factory and random */
        theSupplier = pSupplier;
        theFactory = pSupplier.getFactory();
        theRandom = theFactory.getRandomSource().getRandom();

        /* Create the state */
        theState = new GordianCoreAgreementState(pSpec);
        final GordianKeyPairFactory myFactory = theFactory.getAsyncFactory().getKeyPairFactory();
        theKeyPairGenerator = myFactory.getKeyPairGenerator(pSpec.getKeyPairSpec());

        /* Create the result calculator */
        theResultCalc = new GordianCoreAgreementCalculator(theFactory, theState);
    }

    /**
     * Obtain the supplier.
     *
     * @return the supplier
     */
    public GordianCoreAgreementSupplier getSupplier() {
        return theSupplier;
    }

    /**
     * Obtain the state.
     *
     * @return the state
     */
    public GordianCoreAgreementState getState() {
        return theState;
    }

    /**
     * Obtain the random.
     *
     * @return the random
     */
    public SecureRandom getRandom() {
        return theFactory.getRandomSource().getRandom();
    }

    /**
     * Ask to fail signature during testing.
     */
    void failSignature() {
        failSignature = true;
    }

    /**
     * Ask to fail confirmation during testing.
     */
    void failConfirmation() {
        failConfirmation = true;
    }

    /**
     * Set the status.
     *
     * @param pStatus the status
     */
    void setStatus(final GordianAgreementStatus pStatus) {
        theState.setStatus(pStatus);
    }

    /**
     * Set the resultType.
     *
     * @param pResultType the resultType
     * @throws GordianException on error
     */
    void setResultType(final Object pResultType) throws GordianException {
        theState.setResultType(pResultType);
    }

    /**
     * Store secret.
     *
     * @param pSecret the secret
     * @throws GordianException on error
     */
    public void storeSecret(final byte[] pSecret) throws GordianException {
        /* Protect against failure */
        try {
            /* Just process the secret */
            processSecret(pSecret);

            /* Clear buffers */
        } finally {
            /* Clear the secret */
            Arrays.fill(pSecret, (byte) 0);
        }
    }

    /**
     * Process the secret.
     *
     * @param pSecret the secret
     * @throws GordianException on error
     */
    private void processSecret(final byte[] pSecret) throws GordianException {
        /* If we are using confirmation */
        boolean bSuccess = true;
        final GordianAgreementSpec mySpec = theState.getSpec();
        if (Boolean.TRUE.equals(mySpec.withConfirm())
                && mySpec.getAgreementType() != GordianAgreementType.SM2) {
            /* calculate the confirmation tags */
            bSuccess = calculateConfirmationTags(pSecret);
        }

        /* Calculate result */
        if (bSuccess) {
            theState.setResult(theResultCalc.processSecret(pSecret, theState.getResultType()));
        }
    }

    /**
     * Set the result as an error.
     *
     * @param pError the error
     */
    public void setError(final String pError) {
        /* Store details of the error */
        theState.setResultType(pError);
        theState.setResult(new GordianDataException(pError));
    }

    /**
     * Is the agreement rejected?.
     *
     * @return true/false
     */
    public boolean isRejected() {
        return theState.getResultType() instanceof String;
    }

    /**
     * Set the clientCertificate.
     *
     * @param pCert the Certificate
     */
    void setClientCertificate(final GordianCertificate pCert) {
        theState.getClient().setCertificate(pCert);
    }

    /**
     * Set the serverCertificate.
     *
     * @param pCert the Certificate
     */
    void setServerCertificate(final GordianCertificate pCert) {
        theState.getServer().setCertificate(pCert);
    }

    /**
     * Set the signerCertificate.
     *
     * @param pCert the Certificate
     * @return the Builder
     */
    GordianCoreAgreementBuilder setSignerCertificate(final GordianCertificate pCert) {
        theState.setSignerCertificate(pCert);
        return this;
    }

    /**
     * Set the signSpec.
     *
     * @param pSpec the signSpec
     * @return the Builder
     */
    GordianCoreAgreementBuilder setSignSpec(final GordianSignatureSpec pSpec) {
        theState.setSignSpec(pSpec);
        return this;
    }

    /**
     * Create a new clientId.
     */
    void newClientId() {
        theState.getClient().setId(theSupplier.getNextId());
    }

    /**
     * Create a new serverId.
     */
    void newServerId() {
        theState.getServer().setId(theSupplier.getNextId());
    }

    /**
     * Create a new client initVector.
     */
    void newClientIV() {
        final byte[] myClientIV = new byte[INITLEN];
        theRandom.nextBytes(myClientIV);
        theState.getClient().setInitVector(myClientIV);
    }

    /**
     * Create a new server initVector.
     */
    void newServerIV() {
        final byte[] myServerIV = new byte[INITLEN];
        theRandom.nextBytes(myServerIV);
        theState.getServer().setInitVector(myServerIV);
    }

    /**
     * Create a new client ephemeral.
     *
     * @throws GordianException on error
     */
    public void newClientEphemeral() throws GordianException {
        final GordianKeyPair myPair = theKeyPairGenerator.generateKeyPair();
        theState.getClient()
                .setEphemeralKeyPair(myPair)
                .setEphemeralKeySpec(theKeyPairGenerator.getX509Encoding(myPair));
    }

    /**
     * Set the client ephemeral as Encapsulated.
     *
     * @param pEphemeral the keyPair
     * @throws GordianException on error
     */
    public void setClientEphemeralAsEncapsulated(final GordianKeyPair pEphemeral) throws GordianException {
        final X509EncodedKeySpec myKeySpec = theKeyPairGenerator.getX509Encoding(pEphemeral);
        theState.setEncapsulated(myKeySpec.getEncoded());
    }

    /**
     * Create a new client ephemeral.
     *
     * @throws GordianException on error
     */
    public void newServerEphemeral() throws GordianException {
        final GordianKeyPair myPair = theKeyPairGenerator.generateKeyPair();
        theState.getServer()
                .setEphemeralKeyPair(myPair)
                .setEphemeralKeySpec(theKeyPairGenerator.getX509Encoding(myPair));
    }

    /**
     * Copy ephemeral keyPairs to main keyPairs.
     */
    void copyEphemerals() {
        theState.getClient().copyEphemeral();
        theState.getServer().copyEphemeral();
    }

    /**
     * Set the client confirm.
     *
     * @param pConfirm the clientConfirm
     * @return noError true/false
     */
    boolean setClientConfirm(final byte[] pConfirm) {
        /* Access any expected value */
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final byte[] myExpected = myClient.getConfirm();

        /* If we have an expected value, reject any difference */
        if (failConfirmation
                || (myExpected != null && !Arrays.constantTimeAreEqual(myExpected, pConfirm))) {
            setError("Client Confirmation failed");
            return false;
        }

        /* Store the value */
        myClient.setConfirm(pConfirm);
        return true;
    }

    /**
     * Set the server confirm.
     *
     * @param pConfirm the serverConfirm
     * @return noError true/false
     */
    boolean setServerConfirm(final byte[] pConfirm) {
        /* Access any expected value */
        final GordianCoreAgreementParticipant myServer = theState.getServer();
        final byte[] myExpected = myServer.getConfirm();

        /* If we have an expected value, reject any difference */
        if (myExpected != null
                && (failConfirmation || !Arrays.constantTimeAreEqual(myExpected, pConfirm))) {
            setError("Server Confirmation failed");
            return false;
        }

        /* Store the value */
        myServer.setConfirm(pConfirm);
        return true;
    }

    /**
     * Build the clientHello.
     *
     * @return the clientHello message
     * @throws GordianException on error
     */
    GordianCoreAgreementMessageASN1 newClientHello() throws GordianException {
        /* Access details */
        final GordianCoreAgreementMessageASN1 myMsg = GordianCoreAgreementMessageASN1.newClientHello();
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final GordianCoreAgreementParticipant myServer = theState.getServer();

        /* Set standard details */
        myMsg.setAgreementId(theSupplier.getIdentifierForSpec(theState.getSpec()))
                .setResultId(theSupplier.getIdentifierForResultType(theState.getResultType()))
                .setClientId(myClient.getId())
                .setInitVector(myClient.getInitVector())
                .setEphemeral(myClient.getEphemeralKeySpec())
                .setEncapsulated(theState.getEncapsulated());

        /* Store certificates */
        myMsg.setClientCertificate(myClient.getCertificate())
                .setServerCertificate(myServer.getCertificate());

        /* Return the message */
        return myMsg;
    }

    /**
     * Build the serverHello.
     *
     * @return the serverHello message
     * @throws GordianException on error
     */
    GordianCoreAgreementMessageASN1 newServerHello() throws GordianException {
        /* Access details */
        final GordianCoreAgreementMessageASN1 myMsg = GordianCoreAgreementMessageASN1.newServerHello();
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final GordianCoreAgreementParticipant myServer = theState.getServer();

        /* Set standard details */
        myMsg.setAgreementId(theSupplier.getIdentifierForSpec(theState.getSpec()))
                .setClientId(myClient.getId());

        /* Handle error result */
        if (theState.getResultType() instanceof String myType) {
            myMsg.setResultId(theSupplier.getIdentifierForResultType(myType));
            return myMsg;
        }

        /* Set server details */
        myMsg.setServerId(myServer.getId())
                .setInitVector(myServer.getInitVector())
                .setEphemeral(myServer.getEphemeralKeySpec())
                .setConfirmation(myServer.getConfirm());

        /* Store signing details */
        final GordianCertificate mySignerCert = theState.getSignerCertificate();
        if (mySignerCert != null) {
            /* Access details */
            final GordianCoreSignatureFactory mySigns = (GordianCoreSignatureFactory) theFactory.getAsyncFactory().getSignatureFactory();
            final GordianSignatureSpec mySignSpec = theState.getSignSpec();
            final GordianKeyPair mySignerPair = mySignerCert.getKeyPair();
            final AlgorithmIdentifier myAlgId = mySigns.getIdentifierForSpecAndKeyPair(mySignSpec, mySignerPair);

            /* Build the signature */
            final GordianSignature mySigner = mySigns.createSigner(mySignSpec);
            mySigner.initForSigning(GordianSignParams.keyPair(mySignerPair));
            mySigner.update(myClient.getEphemeralKeySpec().getEncoded());
            mySigner.update(myClient.getInitVector());
            mySigner.update(myServer.getEphemeralKeySpec().getEncoded());
            mySigner.update(myServer.getInitVector());
            myMsg.setSignerCertificate(mySignerCert)
                    .setSignature(myAlgId, mySigner.sign());
        }

        /* Return the message */
        return myMsg;
    }

    /**
     * Build the clientConfirm.
     *
     * @return the clientConfirm
     * @throws GordianException on error
     */
    GordianCoreAgreementMessageASN1 newClientConfirm() throws GordianException {
        /* Access details */
        final GordianCoreAgreementMessageASN1 myMsg = GordianCoreAgreementMessageASN1.newClientConfirm();
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final GordianCoreAgreementParticipant myServer = theState.getServer();

        /* Set standard details */
        myMsg.setAgreementId(theSupplier.getIdentifierForSpec(theState.getSpec()))
                .setServerId(myServer.getId());

        /* Handle error result */
        if (theState.getResultType() instanceof String myType) {
            myMsg.setResultId(theSupplier.getIdentifierForResultType(myType));
            return myMsg;
        }

        /* Set confirmation */
        myMsg.setConfirmation(myClient.getConfirm());

        /* Return the message */
        return myMsg;
    }

    /**
     * Parse the clientHello.
     *
     * @param pClientHello the message
     * @throws GordianException on error
     */
    void parseClientHello(final GordianCoreAgreementMessageASN1 pClientHello) throws GordianException {
        /* Access details */
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final GordianCoreAgreementParticipant myServer = theState.getServer();

        /* Set standard details */
        theState.setResultType(theSupplier.getResultTypeForIdentifier(pClientHello.getResultId()));
        parseEncapsulated(pClientHello.getEncapsulated());

        /* Store client details */
        myClient.setId(pClientHello.getClientId())
                .setInitVector(pClientHello.getInitVector());
        final X509EncodedKeySpec myEphemeral = pClientHello.getEphemeral();
        if (myEphemeral != null) {
            myClient.setEphemeralKeySpec(myEphemeral)
                    .setEphemeralKeyPair(theKeyPairGenerator.derivePublicOnlyKeyPair(myEphemeral));
        }

        /* Store certificates */
        myClient.setCertificate(pClientHello.getClientCertificate(theFactory));
        myServer.setCertificate(pClientHello.getServerCertificate(theFactory));
    }

    /**
     * Parse the serverHello.
     *
     * @param pServerHello the message
     * @return noError true/false
     * @throws GordianException on error
     */
    boolean parseServerHello(final GordianCoreAgreementMessageASN1 pServerHello) throws GordianException {
        /* Access details */
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final GordianCoreAgreementParticipant myServer = theState.getServer();

        /* Handle error result */
        final AlgorithmIdentifier myResId = pServerHello.getResultId();
        if (myResId != null) {
            setError((String) theSupplier.getResultTypeForIdentifier(myResId));
            return false;
        }

        /* Store server details */
        myServer.setId(pServerHello.getServerId())
                .setInitVector(pServerHello.getInitVector())
                .setConfirm(pServerHello.getConfirmation());
        final X509EncodedKeySpec myEphemeral = pServerHello.getEphemeral();
        if (myEphemeral != null) {
            myServer.setEphemeralKeySpec(myEphemeral)
                    .setEphemeralKeyPair(theKeyPairGenerator.derivePublicOnlyKeyPair(myEphemeral));
        }

        /* Store signing details */
        final GordianCertificate mySignerCert = pServerHello.getSignerCertificate(theFactory);
        if (mySignerCert != null) {
            /* Access details */
            final GordianCoreSignatureFactory mySigns = (GordianCoreSignatureFactory) theFactory.getAsyncFactory().getSignatureFactory();
            final GordianSignatureSpec mySignSpec = mySigns.getSpecForIdentifier(pServerHello.getSignatureId());
            final GordianKeyPair mySignerPair = mySignerCert.getKeyPair();
            theState.setSignerCertificate(mySignerCert)
                    .setSignSpec(mySignSpec);
            final GordianSignature mySigner = mySigns.createSigner(mySignSpec);

            /* Build the signature */
            mySigner.initForVerify(GordianSignParams.keyPair(mySignerPair));
            mySigner.update(myClient.getEphemeralKeySpec().getEncoded());
            mySigner.update(myClient.getInitVector());
            mySigner.update(myServer.getEphemeralKeySpec().getEncoded());
            mySigner.update(myServer.getInitVector());
            if (failSignature || !mySigner.verify(pServerHello.getSignature())) {
                setError("Signature Failed");
                return false;
            }
        }

        /* Success */
        return true;
    }

    /**
     * Parse the clientConfirm.
     *
     * @param pClientConfirm the clientConfirm
     * @return noError true/false
     * @throws GordianException on error
     */
    boolean parseClientConfirm(final GordianCoreAgreementMessageASN1 pClientConfirm) throws GordianException {
        /* Handle error result */
        final AlgorithmIdentifier myResId = pClientConfirm.getResultId();
        if (myResId != null) {
            setError((String) theSupplier.getResultTypeForIdentifier(myResId));
            return false;
        }

        /* Store client details */
        return setClientConfirm(pClientConfirm.getConfirmation());
    }

    /**
     * Parse the clientHello.
     *
     * @param pEncapsulated the encapsulated
     * @throws GordianException on error
     */
    void parseEncapsulated(final byte[] pEncapsulated) throws GordianException {
        if (pEncapsulated != null
                && GordianKeyPairType.NEWHOPE.equals(theState.getSpec().getKeyPairSpec().getKeyPairType())) {
            final GordianKeyPair myKeyPair
                    = theKeyPairGenerator.derivePublicOnlyKeyPair(new X509EncodedKeySpec(pEncapsulated));
            theState.getClient().setEphemeralKeyPair(myKeyPair);
        } else {
            theState.setEncapsulated(pEncapsulated);
        }
    }

    /**
     * Calculate the confirmation tags.
     *
     * @param pSecret the secret
     * @return noError true/false
     * @throws GordianException on error
     */
    private boolean calculateConfirmationTags(final byte[] pSecret) throws GordianException {
        /* Access details */
        final GordianCoreAgreementParticipant myClient = theState.getClient();
        final GordianCoreAgreementParticipant myServer = theState.getServer();

        /* Derive the key */
        final byte[] myKey = theResultCalc.calculateDerivedSecret(pSecret, GordianDerivationId.TAGS, GordianLength.LEN_512.getByteLength());

        /* Create the hMac and initialize with the key */
        final GordianMacFactory myMacs = theFactory.getMacFactory();
        final GordianMacSpec mySpec = GordianMacSpecBuilder.hMac(GordianDerivationId.TAGS.getDigestType());
        final GordianMac myMac = myMacs.createMac(mySpec);
        myMac.initKeyBytes(myKey);

        /* Access the public encodings */
        final byte[] myClientSpec = theKeyPairGenerator.getX509Encoding(myClient.getKeyPair()).getEncoded();
        final byte[] myClientEphemeral = myClient.getEphemeralKeySpec().getEncoded();
        final byte[] myServerSpec = theKeyPairGenerator.getX509Encoding(myServer.getKeyPair()).getEncoded();
        final byte[] myServerEphemeral = myServer.getEphemeralKeySpec().getEncoded();

        /* Build Server Confirmation tag */
        myMac.update(myServerSpec);
        myMac.update(myClientSpec);
        myMac.update(myServerEphemeral);
        myMac.update(myClientEphemeral);
        boolean bSuccess = setServerConfirm(myMac.finish());

        /* If we are OK */
        if (bSuccess) {
            /* Build Client Confirmation tag */
            myMac.update(myClientSpec);
            myMac.update(myServerSpec);
            myMac.update(myClientEphemeral);
            myMac.update(myServerEphemeral);
            bSuccess = setClientConfirm(myMac.finish());
        }

        /* Return success */
        return bSuccess;
    }
}