package org.bouncycastle.jcajce.provider.asymmetric.dh;

import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.interfaces.DHPrivateKey;
import javax.crypto.interfaces.DHPublicKey;
import javax.crypto.spec.DHParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.crypto.BasicAgreement;
import org.bouncycastle.crypto.DerivationFunction;
import org.bouncycastle.crypto.agreement.DHUnifiedAgreement;
import org.bouncycastle.crypto.agreement.MQVBasicAgreement;
import org.bouncycastle.crypto.agreement.kdf.ConcatenationKDFGenerator;
import org.bouncycastle.crypto.agreement.kdf.DHKEKGenerator;
import org.bouncycastle.crypto.generators.KDF2BytesGenerator;
import org.bouncycastle.crypto.params.DHMQVPrivateParameters;
import org.bouncycastle.crypto.params.DHMQVPublicParameters;
import org.bouncycastle.crypto.params.DHParameters;
import org.bouncycastle.crypto.params.DHPrivateKeyParameters;
import org.bouncycastle.crypto.params.DHPublicKeyParameters;
import org.bouncycastle.crypto.params.DHUPrivateParameters;
import org.bouncycastle.crypto.params.DHUPublicParameters;
import org.bouncycastle.crypto.util.DigestFactory;
import org.bouncycastle.jcajce.provider.asymmetric.util.BaseAgreementSpi;
import org.bouncycastle.jcajce.spec.DHDomainParameterSpec;
import org.bouncycastle.jcajce.spec.DHUParameterSpec;
import org.bouncycastle.jcajce.spec.MQVParameterSpec;
import org.bouncycastle.jcajce.spec.UserKeyingMaterialSpec;
import org.bouncycastle.util.BigIntegers;

/**
 * Diffie-Hellman key agreement. There's actually a better way of doing this
 * if you are using long term public keys, see the light-weight version for
 * details.
 */
public class KeyAgreementSpi
    extends BaseAgreementSpi
{
    private static final BigInteger ONE = BigInteger.valueOf(1);
    private static final BigInteger TWO = BigInteger.valueOf(2);

    private final DHUnifiedAgreement unifiedAgreement;
    private final BasicAgreement mqvAgreement;

    private DHUParameterSpec dheParameters;
    private MQVParameterSpec mqvParameters;

    private BigInteger      x;
    private BigInteger      p;
    private BigInteger      g;

    private byte[]          result;

    public KeyAgreementSpi()
    {
        this("Diffie-Hellman", null);
    }

    public KeyAgreementSpi(
        String kaAlgorithm,
        DerivationFunction kdf)
    {
        super(kaAlgorithm, kdf);
        this.unifiedAgreement = null;
        this.mqvAgreement = null;
    }

    public KeyAgreementSpi(
        String kaAlgorithm,
        DHUnifiedAgreement unifiedAgreement,
        DerivationFunction kdf)
    {
        super(kaAlgorithm, kdf);
        this.unifiedAgreement = unifiedAgreement;
        this.mqvAgreement = null;
    }

    public KeyAgreementSpi(
        String kaAlgorithm,
        BasicAgreement mqvAgreement,
        DerivationFunction kdf)
    {
        super(kaAlgorithm, kdf);
        this.unifiedAgreement = null;
        this.mqvAgreement = mqvAgreement;
    }

    protected byte[] bigIntToBytes(
        BigInteger    r)
    {
        //
        // RFC 2631 (2.1.2) specifies that the secret should be padded with leading zeros if necessary
        // must be the same length as p
        //
        int expectedLength = (p.bitLength() + 7) / 8;

        return BigIntegers.asUnsignedByteArray(expectedLength, r);
    }

    protected Key engineDoPhase(
        Key     key,
        boolean lastPhase) 
        throws InvalidKeyException, IllegalStateException
    {
        if (x == null)
        {
            throw new IllegalStateException("Diffie-Hellman not initialised.");
        }

        if (!(key instanceof DHPublicKey))
        {
            throw new InvalidKeyException("DHKeyAgreement doPhase requires DHPublicKey");
        }
        DHPublicKey pubKey = (DHPublicKey)key;

        if (!pubKey.getParams().getG().equals(g) || !pubKey.getParams().getP().equals(p))
        {
            throw new InvalidKeyException("DHPublicKey not for this KeyAgreement!");
        }

        BigInteger peerY = ((DHPublicKey)key).getY();
        if (peerY == null || peerY.compareTo(TWO) < 0
            || peerY.compareTo(p.subtract(ONE)) >= 0)
        {
            throw new InvalidKeyException("Invalid DH PublicKey");
        }

        if (unifiedAgreement != null)
        {
            if (!lastPhase)
            {
                throw new IllegalStateException("unified Diffie-Hellman can use only two key pairs");
            }

            DHPublicKeyParameters staticKey = generatePublicKeyParameter((PublicKey)key);
            DHPublicKeyParameters ephemKey = generatePublicKeyParameter(dheParameters.getOtherPartyEphemeralKey());

            DHUPublicParameters pKey = new DHUPublicParameters(staticKey, ephemKey);

            result = unifiedAgreement.calculateAgreement(pKey);

            return null;
        }
        else if (mqvAgreement != null)
        {
            if (!lastPhase)
            {
                throw new IllegalStateException("MQV Diffie-Hellman can use only two key pairs");
            }

            DHPublicKeyParameters staticKey = generatePublicKeyParameter((PublicKey)key);
            DHPublicKeyParameters ephemKey = generatePublicKeyParameter(mqvParameters.getOtherPartyEphemeralKey());

            DHMQVPublicParameters pKey = new DHMQVPublicParameters(staticKey, ephemKey);

            result = bigIntToBytes(mqvAgreement.calculateAgreement(pKey));

            return null;
        }
        else
        {
            BigInteger res = peerY.modPow(x, p);
            if (res.compareTo(ONE) == 0)
            {
                throw new InvalidKeyException("Shared key can't be 1");
            }

            result = bigIntToBytes(res);

            if (lastPhase)
            {
                return null;
            }

            return new BCDHPublicKey(res, pubKey.getParams());
        }
    }

    protected byte[] engineGenerateSecret() 
        throws IllegalStateException
    {
        if (x == null)
        {
            throw new IllegalStateException("Diffie-Hellman not initialised.");
        }

        return super.engineGenerateSecret();
    }

    protected int engineGenerateSecret(
        byte[]  sharedSecret,
        int     offset) 
        throws IllegalStateException, ShortBufferException
    {
        if (x == null)
        {
            throw new IllegalStateException("Diffie-Hellman not initialised.");
        }

        return super.engineGenerateSecret(sharedSecret, offset);
    }

    protected SecretKey engineGenerateSecret(
        String algorithm)
        throws NoSuchAlgorithmException
    {
        if (x == null)
        {
            throw new IllegalStateException("Diffie-Hellman not initialised.");
        }

        // for JSSE compatibility
        if (algorithm.equals("TlsPremasterSecret"))
        {
            return new SecretKeySpec(trimZeroes(result), algorithm);
        }

        return super.engineGenerateSecret(algorithm);
    }

    protected void doInitFromKey(
        Key                     key,
        AlgorithmParameterSpec  params,
        SecureRandom            random) 
        throws InvalidKeyException, InvalidAlgorithmParameterException
    {
        if (!(key instanceof DHPrivateKey))
        {
            throw new InvalidKeyException("DHKeyAgreement requires DHPrivateKey for initialisation");
        }
        DHPrivateKey    privKey = (DHPrivateKey)key;

        if (params != null)
        {
            if (params instanceof DHParameterSpec)    // p, g override.
            {
                DHParameterSpec p = (DHParameterSpec)params;

                this.p = p.getP();
                this.g = p.getG();
                this.dheParameters = null;
                this.ukmParameters = null;
            }
            else if (params instanceof DHUParameterSpec)
            {
                if (unifiedAgreement == null)
                {
                    throw new InvalidAlgorithmParameterException("agreement algorithm not DHU based");
                }
                this.p = privKey.getParams().getP();
                this.g = privKey.getParams().getG();
                this.dheParameters = (DHUParameterSpec)params;
                this.ukmParameters = ((DHUParameterSpec)params).getUserKeyingMaterial();

                if (dheParameters.getEphemeralPublicKey() != null)
                {
                    unifiedAgreement.init(new DHUPrivateParameters(generatePrivateKeyParameter(privKey),
                        generatePrivateKeyParameter(dheParameters.getEphemeralPrivateKey()),
                        generatePublicKeyParameter(dheParameters.getEphemeralPublicKey())));
                }
                else
                {
                    unifiedAgreement.init(new DHUPrivateParameters(generatePrivateKeyParameter(privKey),
                            generatePrivateKeyParameter(dheParameters.getEphemeralPrivateKey())));
                }
            }
            else if (params instanceof MQVParameterSpec)
            {
                if (mqvAgreement == null)
                {
                    throw new InvalidAlgorithmParameterException("agreement algorithm not MQV based");
                }
                this.p = privKey.getParams().getP();
                this.g = privKey.getParams().getG();
                this.mqvParameters = (MQVParameterSpec)params;
                this.ukmParameters = ((MQVParameterSpec)params).getUserKeyingMaterial();

                if (mqvParameters.getEphemeralPublicKey() != null)
                {
                    mqvAgreement.init(new DHMQVPrivateParameters(generatePrivateKeyParameter(privKey),
                        generatePrivateKeyParameter(mqvParameters.getEphemeralPrivateKey()),
                        generatePublicKeyParameter(mqvParameters.getEphemeralPublicKey())));
                }
                else
                {
                    mqvAgreement.init(new DHMQVPrivateParameters(generatePrivateKeyParameter(privKey),
                            generatePrivateKeyParameter(mqvParameters.getEphemeralPrivateKey())));
                }
            }
            else if (params instanceof UserKeyingMaterialSpec)
            {
                if (kdf == null)
                {
                    throw new InvalidAlgorithmParameterException("no KDF specified for UserKeyingMaterialSpec");
                }
                this.p = privKey.getParams().getP();
                this.g = privKey.getParams().getG();
                this.dheParameters = null;
                this.ukmParameters = ((UserKeyingMaterialSpec)params).getUserKeyingMaterial();
            }
            else
            {
                throw new InvalidAlgorithmParameterException("DHKeyAgreement only accepts DHParameterSpec");
            }
        }
        else
        {
            this.p = privKey.getParams().getP();
            this.g = privKey.getParams().getG();
        }

        this.x = privKey.getX();
        this.result = bigIntToBytes(x);
    }

    protected void engineInit(
        Key             key,
        SecureRandom    random) 
        throws InvalidKeyException
    {
        if (!(key instanceof DHPrivateKey))
        {
            throw new InvalidKeyException("DHKeyAgreement requires DHPrivateKey");
        }

        DHPrivateKey    privKey = (DHPrivateKey)key;

        this.p = privKey.getParams().getP();
        this.g = privKey.getParams().getG();
        this.x = privKey.getX();
        this.result = bigIntToBytes(x);
    }

    protected byte[] doCalcSecret()
    {
        return result;
    }

    private DHPrivateKeyParameters generatePrivateKeyParameter(PrivateKey privKey)
        throws InvalidKeyException
    {
        if (privKey instanceof DHPrivateKey)
        {
            if (privKey instanceof BCDHPrivateKey)
            {
                return ((BCDHPrivateKey)privKey).engineGetKeyParameters();
            }
            else
            {
                DHPrivateKey pub = (DHPrivateKey)privKey;

                DHParameterSpec params = pub.getParams();
                return new DHPrivateKeyParameters(pub.getX(),
                            new DHParameters(params.getP(), params.getG(), null, params.getL()));
            }
        }
        else
        {
            throw new InvalidKeyException("private key not a DHPrivateKey");
        }
    }

    private DHPublicKeyParameters generatePublicKeyParameter(PublicKey pubKey)
        throws InvalidKeyException
    {
        if (pubKey instanceof DHPublicKey)
        {
            if (pubKey instanceof BCDHPublicKey)
            {
                return ((BCDHPublicKey)pubKey).engineGetKeyParameters();
            }
            else
            {
                DHPublicKey pub = (DHPublicKey)pubKey;

                DHParameterSpec params = pub.getParams();

                if (params instanceof DHDomainParameterSpec)
                {
                    return new DHPublicKeyParameters(pub.getY(), ((DHDomainParameterSpec)params).getDomainParameters());
                }
                return new DHPublicKeyParameters(pub.getY(),
                            new DHParameters(params.getP(), params.getG(), null, params.getL()));
            }
        }
        else
        {
            throw new InvalidKeyException("public key not a DHPublicKey");
        }
    }

    public static class DHwithRFC2631KDF
        extends KeyAgreementSpi
    {
        public DHwithRFC2631KDF()
        {
            super("DHwithRFC2631KDF", new DHKEKGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class DHwithSHA1KDF
        extends KeyAgreementSpi
    {
        public DHwithSHA1KDF()
        {
            super("DHwithSHA1CKDF", new KDF2BytesGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class DHwithSHA224KDF
        extends KeyAgreementSpi
    {
        public DHwithSHA224KDF()
        {
            super("DHwithSHA224CKDF", new KDF2BytesGenerator(DigestFactory.createSHA224()));
        }
    }

    public static class DHwithSHA256KDF
        extends KeyAgreementSpi
    {
        public DHwithSHA256KDF()
        {
            super("DHwithSHA256CKDF", new KDF2BytesGenerator(DigestFactory.createSHA256()));
        }
    }

    public static class DHwithSHA384KDF
        extends KeyAgreementSpi
    {
        public DHwithSHA384KDF()
        {
            super("DHwithSHA384KDF", new KDF2BytesGenerator(DigestFactory.createSHA384()));
        }
    }

    public static class DHwithSHA512KDF
        extends KeyAgreementSpi
    {
        public DHwithSHA512KDF()
        {
            super("DHwithSHA512KDF", new KDF2BytesGenerator(DigestFactory.createSHA512()));
        }
    }

    public static class DHwithSHA1CKDF
        extends KeyAgreementSpi
    {
        public DHwithSHA1CKDF()
        {
            super("DHwithSHA1CKDF", new ConcatenationKDFGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class DHwithSHA224CKDF
        extends KeyAgreementSpi
    {
        public DHwithSHA224CKDF()
        {
            super("DHwithSHA224CKDF", new ConcatenationKDFGenerator(DigestFactory.createSHA224()));
        }
    }

    public static class DHwithSHA256CKDF
        extends KeyAgreementSpi
    {
        public DHwithSHA256CKDF()
        {
            super("DHwithSHA256CKDF", new ConcatenationKDFGenerator(DigestFactory.createSHA256()));
        }
    }

    public static class DHwithSHA384CKDF
        extends KeyAgreementSpi
    {
        public DHwithSHA384CKDF()
        {
            super("DHwithSHA384CKDF", new ConcatenationKDFGenerator(DigestFactory.createSHA384()));
        }
    }

    public static class DHwithSHA512CKDF
        extends KeyAgreementSpi
    {
        public DHwithSHA512CKDF()
        {
            super("DHwithSHA512CKDF", new ConcatenationKDFGenerator(DigestFactory.createSHA512()));
        }
    }

    public static class DHUwithSHA1KDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA1KDF()
        {
            super("DHUwithSHA1KDF", new DHUnifiedAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class DHUwithSHA224KDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA224KDF()
        {
            super("DHUwithSHA224KDF", new DHUnifiedAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA224()));
        }
    }

    public static class DHUwithSHA256KDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA256KDF()
        {
            super("DHUwithSHA256KDF", new DHUnifiedAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA256()));
        }
    }

    public static class DHUwithSHA384KDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA384KDF()
        {
            super("DHUwithSHA384KDF", new DHUnifiedAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA384()));
        }
    }

    public static class DHUwithSHA512KDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA512KDF()
        {
            super("DHUwithSHA512KDF", new DHUnifiedAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA512()));
        }
    }

    public static class DHUwithSHA1CKDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA1CKDF()
        {
            super("DHUwithSHA1CKDF", new DHUnifiedAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class DHUwithSHA224CKDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA224CKDF()
        {
            super("DHUwithSHA224CKDF", new DHUnifiedAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA224()));
        }
    }

    public static class DHUwithSHA256CKDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA256CKDF()
        {
            super("DHUwithSHA256CKDF", new DHUnifiedAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA256()));
        }
    }

    public static class DHUwithSHA384CKDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA384CKDF()
        {
            super("DHUwithSHA384CKDF", new DHUnifiedAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA384()));
        }
    }

    public static class DHUwithSHA512CKDF
        extends KeyAgreementSpi
    {
        public DHUwithSHA512CKDF()
        {
            super("DHUwithSHA512CKDF", new DHUnifiedAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA512()));
        }
    }

    public static class MQVwithSHA1KDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA1KDF()
        {
            super("MQVwithSHA1KDF", new MQVBasicAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class MQVwithSHA224KDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA224KDF()
        {
            super("MQVwithSHA224KDF", new MQVBasicAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA224()));
        }
    }

    public static class MQVwithSHA256KDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA256KDF()
        {
            super("MQVwithSHA256KDF", new MQVBasicAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA256()));
        }
    }

    public static class MQVwithSHA384KDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA384KDF()
        {
            super("MQVwithSHA384KDF", new MQVBasicAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA384()));
        }
    }

    public static class MQVwithSHA512KDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA512KDF()
        {
            super("MQVwithSHA512KDF", new MQVBasicAgreement(), new KDF2BytesGenerator(DigestFactory.createSHA512()));
        }
    }

    public static class MQVwithSHA1CKDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA1CKDF()
        {
            super("MQVwithSHA1CKDF", new MQVBasicAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA1()));
        }
    }

    public static class MQVwithSHA224CKDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA224CKDF()
        {
            super("MQVwithSHA224CKDF", new MQVBasicAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA224()));
        }
    }

    public static class MQVwithSHA256CKDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA256CKDF()
        {
            super("MQVwithSHA256CKDF", new MQVBasicAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA256()));
        }
    }

    public static class MQVwithSHA384CKDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA384CKDF()
        {
            super("MQVwithSHA384CKDF", new MQVBasicAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA384()));
        }
    }

    public static class MQVwithSHA512CKDF
        extends KeyAgreementSpi
    {
        public MQVwithSHA512CKDF()
        {
            super("MQVwithSHA512CKDF", new MQVBasicAgreement(), new ConcatenationKDFGenerator(DigestFactory.createSHA512()));
        }
    }
}
