GordianCoreAgreement.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.GordianAgreement;
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.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.cert.GordianCertificate;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianStreamCipher;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianStreamCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymCipher;
import io.github.tonywasher.joceanus.gordianknot.api.cipher.GordianSymCipherSpec;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactory;
import io.github.tonywasher.joceanus.gordianknot.api.factory.GordianFactoryType;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySet;
import io.github.tonywasher.joceanus.gordianknot.api.keyset.GordianKeySetSpec;
import io.github.tonywasher.joceanus.gordianknot.api.sign.GordianSignatureSpec;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianLogicException;

import java.util.Objects;

/**
 * Key Agreement.
 */
public class GordianCoreAgreement
        implements GordianAgreement {
    /**
     * The Engine.
     */
    private final GordianCoreAgreementEngine theEngine;

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

    /**
     * The Builder.
     */
    private final GordianCoreAgreementBuilder theBuilder;

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

    /**
     * The Agreement Spec.
     */
    private final GordianAgreementSpec theSpec;

    /**
     * The Parameters.
     */
    private GordianCoreAgreementParams theParams;

    /**
     * The Next Message.
     */
    private byte[] theNextMsg;

    /**
     * Constructor.
     *
     * @param pEngine the engine
     * @throws GordianException on error
     */
    public GordianCoreAgreement(final GordianCoreAgreementEngine pEngine) throws GordianException {
        /* Store details */
        theEngine = pEngine;
        theSupplier = theEngine.getSupplier();
        theBuilder = theEngine.getBuilder();
        theState = theBuilder.getState();
        theSpec = theState.getSpec();
    }

    @Override
    public GordianAgreementParams getAgreementParams() {
        return new GordianCoreAgreementParams(theParams);
    }

    /**
     * Obtain the spec.
     *
     * @return the spec
     */
    GordianAgreementSpec getAgreementSpec() {
        return theSpec;
    }

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

    @Override
    public GordianAgreementStatus getStatus() {
        return theState.getStatus();
    }

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

    @Override
    public Object getResult() throws GordianException {
        checkStatus(GordianAgreementStatus.RESULT_AVAILABLE);
        return theState.getResult();
    }

    @Override
    public GordianFactory getFactoryResult() {
        return GordianAgreementStatus.RESULT_AVAILABLE.equals(theState.getStatus())
                && theState.getResultType() instanceof GordianFactoryType
                ? (GordianFactory) theState.getResult() : null;
    }

    @Override
    public GordianKeySet getKeySetResult() {
        return GordianAgreementStatus.RESULT_AVAILABLE.equals(theState.getStatus())
                && theState.getResultType() instanceof GordianKeySetSpec
                ? (GordianKeySet) theState.getResult() : null;
    }

    @Override
    public GordianSymCipher[] getSymCipherPairResult() {
        return GordianAgreementStatus.RESULT_AVAILABLE.equals(theState.getStatus())
                && theState.getResultType() instanceof GordianSymCipherSpec
                ? (GordianSymCipher[]) theState.getResult() : null;
    }

    @Override
    public GordianStreamCipher[] getStreamCipherPairResult() {
        return GordianAgreementStatus.RESULT_AVAILABLE.equals(theState.getStatus())
                && theState.getResultType() instanceof GordianStreamCipherSpec
                ? (GordianStreamCipher[]) theState.getResult() : null;
    }

    @Override
    public byte[] getByteArrayResult() {
        return GordianAgreementStatus.RESULT_AVAILABLE.equals(theState.getStatus())
                && theState.getResultType() instanceof Integer
                ? (byte[]) theState.getResult() : null;
    }

    @Override
    public GordianException getRejectionResult() {
        return GordianAgreementStatus.RESULT_AVAILABLE.equals(theState.getStatus())
                && theState.getResultType() instanceof String
                ? (GordianException) theState.getResult() : null;
    }

    /**
     * Check status.
     *
     * @param pStatus the required status
     * @throws GordianException on error
     */
    protected void checkStatus(final GordianAgreementStatus pStatus) throws GordianException {
        /* If we are in the wrong state */
        final GordianAgreementStatus myStatus = theState.getStatus();
        if (myStatus != pStatus) {
            throw new GordianLogicException("Invalid State: " + myStatus);
        }
    }

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

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

    /**
     * Set the client certificate.
     *
     * @param pClient the client certificate
     * @throws GordianException on error
     */
    void setClientCertificate(final GordianCertificate pClient) throws GordianException {
        /* Handle null client certificate */
        if (pClient == null) {
            final GordianAgreementType myType = theSpec.getAgreementType();
            if (!myType.isAnonymous() && !myType.isSigned()) {
                throw new GordianDataException("Client Certificate must be provided");
            }
            return;
        }

        /* Store the certificate */
        theBuilder.setClientCertificate(pClient);
    }

    /**
     * Set the server certificate.
     *
     * @param pServer the server certificate
     * @throws GordianException on error
     */
    void setServerCertificate(final GordianCertificate pServer) throws GordianException {
        /* Check that we have a certificate */
        if (pServer == null) {
            final GordianAgreementType myType = theSpec.getAgreementType();
            if (!myType.isSigned()) {
                throw new GordianDataException("Server Certificate must be provided");
            }
        }

        /* Store the certificate */
        theBuilder.setServerCertificate(pServer);
    }

    /**
     * Set the signer details.
     *
     * @param pSignSpec the signature spec
     * @param pSigner   the signer certificate
     * @throws GordianException on error
     */
    void setSignerCertificate(final GordianSignatureSpec pSignSpec,
                              final GordianCertificate pSigner) throws GordianException {
        theBuilder.setSignSpec(pSignSpec)
                .setSignerCertificate(pSigner);
    }

    @Override
    public void updateParams(final GordianAgreementParams pParams) throws GordianException {
        /* Must be looking for serverPrivate */
        checkStatus(GordianAgreementStatus.AWAITING_SERVERPRIVATE);

        /* Ensure that we are updating from correct parameters */
        if (!Objects.equals(theParams.getId(), ((GordianCoreAgreementParams) pParams).getId())) {
            throw new GordianDataException("Invalid parameters provided");
        }

        /* Determine agreement type */
        final GordianAgreementSpec mySpec = theState.getSpec();
        final boolean isSigned = mySpec.getAgreementType().isSigned();

        /* If this is a signed agreement */
        if (isSigned) {
            /* Handle no signer certificate */
            final GordianCertificate mySignerCert = pParams.getSignerCertificate();
            if (mySignerCert == null) {
                throw new GordianLogicException("No signer declared for Signed agreement");
            }

            /* Declare the signer */
            setSignerCertificate(pParams.getSignatureSpec(), mySignerCert);

        } else {
            /* Ensure that the server has a private key */
            final GordianCertificate myServerCert = pParams.getServerCertificate();
            if (myServerCert.getKeyPair().isPublicOnly()) {
                throw new GordianDataException("Server Certificate is Public Only");
            }

            /* Update the server certificate */
            setServerCertificate(myServerCert);
        }

        /* Store additional data */
        theState.setAdditionalData(pParams.getAdditionalData());

        /* Update the parameters */
        theParams = new GordianCoreAgreementParams((GordianCoreAgreementParams) pParams);

        /* Process the augmented clientHello and return the agreement */
        processClientHello();
    }

    @Override
    public void setError(final String pError) throws GordianException {
        /* Only allowed while awaiting ServerPrivate */
        checkStatus(GordianAgreementStatus.AWAITING_SERVERPRIVATE);
        theBuilder.setError(pError);

        /* If we are not anonymous */
        if (!theSpec.getAgreementType().isAnonymous()) {
            /* Create the rejection serverHello */
            setNextMessage(theBuilder.newServerHello());
        }

        /* Set result available */
        theBuilder.setStatus(GordianAgreementStatus.RESULT_AVAILABLE);
    }

    @Override
    public boolean isRejected() {
        return theBuilder.isRejected();
    }

    /**
     * Set the next message (or null).
     *
     * @param pMessage the next message
     * @throws GordianException on error
     */
    void setNextMessage(final GordianCoreAgreementMessageASN1 pMessage) throws GordianException {
        theNextMsg = pMessage == null ? null : pMessage.getEncodedBytes();
    }

    @Override
    public byte[] nextMessage() {
        return theNextMsg;
    }

    /**
     * Set additionalData.
     *
     * @param pData the additional data
     */
    void setAdditionalData(final byte[] pData) {
        theState.setAdditionalData(pData);
    }

    /**
     * Build the clientHello.
     *
     * @throws GordianException on error
     */
    void buildClientHello() throws GordianException {
        /* Take a snapshot of the parameters */
        theParams = new GordianCoreAgreementParams(theBuilder);

        /* Create ClientId and InitVector */
        if (!theSpec.getAgreementType().isAnonymous()) {
            theBuilder.newClientId();
        }
        theBuilder.newClientIV();

        /* Create clientEphemeral if needed */
        if (needClientEphemeral()) {
            theBuilder.newClientEphemeral();
        }

        /* Create the new ClientHello */
        theEngine.buildClientHello();
        final GordianCoreAgreementMessageASN1 myMsg = theBuilder.newClientHello();

        /* Set next message and status */
        setNextMessage(myMsg);
        theBuilder.setStatus(theSpec.getAgreementType().isAnonymous()
                ? GordianAgreementStatus.RESULT_AVAILABLE
                : GordianAgreementStatus.AWAITING_SERVERHELLO);

        /* Store into cache if required */
        if (!theSpec.getAgreementType().isAnonymous()) {
            theSupplier.storeAgreement(myMsg.getClientId(), this);
        }
    }

    /**
     * Process the clientHello.
     *
     * @param pClientHello the clientHello
     * @throws GordianException on error
     */
    void parseClientHello(final GordianCoreAgreementMessageASN1 pClientHello) throws GordianException {
        /* Parse the clientHello */
        theBuilder.parseClientHello(pClientHello);
        theParams = new GordianCoreAgreementParams(theBuilder);
        theBuilder.setStatus(GordianAgreementStatus.AWAITING_SERVERPRIVATE);
    }

    /**
     * Process the clientHello.
     *
     * @throws GordianException on error
     */
    void processClientHello() throws GordianException {
        /* Create ServerId and InitVector */
        if (!theSpec.getAgreementType().isAnonymous()) {
            theBuilder.newServerId();
            theBuilder.newServerIV();
        }

        /* Create serverEphemeral if needed */
        if (needServerEphemeral()) {
            theBuilder.newServerEphemeral();
        }

        /* Copy ephemerals to keyPairs for signed */
        if (theSpec.getAgreementType().isSigned()) {
            theBuilder.copyEphemerals();
        }

        /* Process the clientHello */
        theEngine.processClientHello();

        /* If we are anonymous */
        if (theSpec.getAgreementType().isAnonymous()) {
            /* Set that the result is available */
            theBuilder.setStatus(GordianAgreementStatus.RESULT_AVAILABLE);

            /* Else we need to build a serverHello */
        } else {
            /* Build the new serverHello */
            final GordianCoreAgreementMessageASN1 myMsg = theBuilder.newServerHello();
            setNextMessage(myMsg);
            theBuilder.setStatus(Boolean.TRUE.equals(theSpec.withConfirm())
                    ? GordianAgreementStatus.AWAITING_CLIENTCONFIRM
                    : GordianAgreementStatus.RESULT_AVAILABLE);

            /* Store into cache if required */
            if (Boolean.TRUE.equals(theSpec.withConfirm())) {
                theSupplier.storeAgreement(myMsg.getServerId(), this);
            }
        }
    }

    /**
     * Process the serverHello.
     *
     * @param pServerHello the serverHello
     * @throws GordianException on error
     */
    public void processServerHello(final GordianCoreAgreementMessageASN1 pServerHello) throws GordianException {
        /* Check that we are expecting a serverHello */
        checkStatus(GordianAgreementStatus.AWAITING_SERVERHELLO);

        /* Parse the serverHello */
        final boolean bSuccess = theBuilder.parseServerHello(pServerHello);
        if (bSuccess) {
            /* Copy ephemerals to keyPairs for signed */
            if (theSpec.getAgreementType().isSigned()) {
                theBuilder.copyEphemerals();
            }

            /* Process the serverHello */
            theEngine.processServerHello();
        }

        /* If we need to send confirm */
        if (bSuccess && Boolean.TRUE.equals(theSpec.withConfirm())) {
            /* Build the new clientConfirm */
            setNextMessage(theBuilder.newClientConfirm());
        } else {
            setNextMessage(null);
        }
        theBuilder.setStatus(GordianAgreementStatus.RESULT_AVAILABLE);

        /* remove from cache */
        theSupplier.removeAgreement(pServerHello.getClientId());
    }

    /**
     * Process the clientConfirm.
     *
     * @param pClientConfirm the clientConfirm
     * @throws GordianException on error
     */
    public void processClientConfirm(final GordianCoreAgreementMessageASN1 pClientConfirm) throws GordianException {
        /* Check that we are expecting a confirmation */
        checkStatus(GordianAgreementStatus.AWAITING_CLIENTCONFIRM);

        /* Parse the clientConfirm */
        if (theBuilder.parseClientConfirm(pClientConfirm)) {
            /* Process if we have no error */
            theEngine.processClientConfirm();
        }

        /* Update status */
        setNextMessage(null);
        theBuilder.setStatus(GordianAgreementStatus.RESULT_AVAILABLE);

        /* remove from cache */
        theSupplier.removeAgreement(pClientConfirm.getServerId());
    }

    /**
     * Do we need a client ephemeral?
     *
     * @return true/false
     */
    private boolean needClientEphemeral() {
        switch (theSpec.getAgreementType()) {
            case ANON:
            case SIGNED:
            case SM2:
            case MQV:
            case UNIFIED:
                return true;
            case KEM:
            case BASIC:
            default:
                return false;
        }
    }

    /**
     * Do we need a server ephemeral?
     *
     * @return true/false
     */
    private boolean needServerEphemeral() {
        switch (theSpec.getAgreementType()) {
            case SIGNED:
            case SM2:
            case MQV:
            case UNIFIED:
                return true;
            case ANON:
            case KEM:
            case BASIC:
            default:
                return false;
        }
    }
}