package org.bouncycastle.crypto.signers;

import java.math.BigInteger;
import java.security.SecureRandom;

import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.CryptoServicesRegistrar;
import org.bouncycastle.crypto.DSAExt;
import org.bouncycastle.crypto.DataLengthException;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECKeyParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.math.ec.ECAlgorithms;
import org.bouncycastle.math.ec.ECConstants;
import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.util.BigIntegers;

/**
 * EC-NR as described in IEEE 1363-2000 - a signature algorithm for Elliptic Curve which
 * also offers message recovery.
 */
public class ECNRSigner
    implements DSAExt
{
    private boolean             forSigning;
    private ECKeyParameters     key;
    private SecureRandom        random;

    /**
     * Initialise the signer.
     *
     * @param forSigning true if we are generating a signature, false
     * for verification or if we want to use the signer for message recovery.
     * @param param key parameters for signature generation.
     */
    public void init(boolean forSigning, CipherParameters param) 
    {
        this.forSigning = forSigning;

        if (forSigning)
        {
            SecureRandom providedRandom = null;
            if (param instanceof ParametersWithRandom)
            {
                ParametersWithRandom withRandom = (ParametersWithRandom)param;
                providedRandom = withRandom.getRandom();
                param = withRandom.getParameters();
            }

            this.key = (ECPrivateKeyParameters)param;
            this.random = CryptoServicesRegistrar.getSecureRandom(providedRandom);
        }
        else
        {
            this.key = (ECPublicKeyParameters)param;
            this.random = null;
        }

        CryptoServicesRegistrar.checkConstraints(Utils.getDefaultProperties("ECNR", key, forSigning));
    }

    public BigInteger getOrder()
    {
        return key.getParameters().getN();
    }

    // Section 7.2.5 ECSP-NR, pg 34
    /**
     * generate a signature for the given message using the key we were
     * initialised with.  Generally, the order of the curve should be at 
     * least as long as the hash of the message of interest, and with 
     * ECNR it *must* be at least as long.  
     *
     * @param digest  the digest to be signed.
     * @exception DataLengthException if the digest is longer than the key allows
     */
    public BigInteger[] generateSignature(
        byte[] digest)
    {
        if (!this.forSigning) 
        {
            throw new IllegalStateException("not initialised for signing");
        }
        
        BigInteger n = getOrder();
        
        BigInteger e = new BigInteger(1, digest);

        ECPrivateKeyParameters  privKey = (ECPrivateKeyParameters)key;

        if (e.compareTo(n) >= 0)
        {
            throw new DataLengthException("input too large for ECNR key");
        }

        BigInteger r = null;
        BigInteger s = null;

        AsymmetricCipherKeyPair tempPair;
        do // generate r
        {
            // generate another, but very temporary, key pair using 
            // the same EC parameters
            ECKeyPairGenerator keyGen = new ECKeyPairGenerator();
            
            keyGen.init(new ECKeyGenerationParameters(privKey.getParameters(), this.random));
            
            tempPair = keyGen.generateKeyPair();

            //    BigInteger Vx = tempPair.getPublic().getW().getAffineX();
            ECPublicKeyParameters V = (ECPublicKeyParameters)tempPair.getPublic();        // get temp's public key
            BigInteger Vx = V.getQ().getAffineXCoord().toBigInteger();                    // get the point's x coordinate

            r = Vx.add(e).mod(n);
        }
        while (r.equals(ECConstants.ZERO));

        // generate s
        BigInteger x = privKey.getD();                // private key value
        BigInteger u = ((ECPrivateKeyParameters)tempPair.getPrivate()).getD();    // temp's private key value
        s = u.subtract(r.multiply(x)).mod(n);

        BigInteger[]  res = new BigInteger[2];
        res[0] = r;
        res[1] = s;

        return res;
    }

    // Section 7.2.6 ECVP-NR, pg 35
    /**
     * return true if the value r and s represent a signature for the 
     * message passed in. Generally, the order of the curve should be at 
     * least as long as the hash of the message of interest, and with 
     * ECNR, it *must* be at least as long.  But just in case the signer
     * applied mod(n) to the longer digest, this implementation will
     * apply mod(n) during verification.
     *
     * @param digest  the digest to be verified.
     * @param r       the r value of the signature.
     * @param s       the s value of the signature.
     * @exception DataLengthException if the digest is longer than the key allows
     */
    public boolean verifySignature(
        byte[]      digest,
        BigInteger  r,
        BigInteger  s)
    {
        if (this.forSigning) 
        {
            throw new IllegalStateException("not initialised for verifying");
        }

        ECPublicKeyParameters pubKey = (ECPublicKeyParameters)key;
        BigInteger n = pubKey.getParameters().getN();
        int nBitLength = n.bitLength();
        
        BigInteger e = new BigInteger(1, digest);
        int eBitLength = e.bitLength();
        
        if (eBitLength > nBitLength) 
        {
            throw new DataLengthException("input too large for ECNR key.");
        }

        BigInteger t = extractT(pubKey, r, s);

        return t != null && t.equals(e.mod(n));
    }

    /**
     * Returns the data used for the signature generation, assuming the public key passed
     * to init() is correct.
     *
     * @return null if r and s are not valid.
     */
    public byte[] getRecoveredMessage(BigInteger r, BigInteger s)
    {
        if (this.forSigning)
        {
            throw new IllegalStateException("not initialised for verifying/recovery");
        }

        BigInteger t = extractT((ECPublicKeyParameters)key, r, s);

        if (t != null)
        {
            return BigIntegers.asUnsignedByteArray(t);
        }

        return null;
    }

    private BigInteger extractT(ECPublicKeyParameters pubKey, BigInteger r, BigInteger s)
    {
        BigInteger n = pubKey.getParameters().getN();

        // r in the range [1,n-1]
        if (r.compareTo(ECConstants.ONE) < 0 || r.compareTo(n) >= 0)
        {
            return null;
        }

        // s in the range [0,n-1]           NB: ECNR spec says 0
        if (s.compareTo(ECConstants.ZERO) < 0 || s.compareTo(n) >= 0)
        {
            return null;
        }

        // compute P = sG + rW

        ECPoint G = pubKey.getParameters().getG();
        ECPoint W = pubKey.getQ();
        // calculate P using Bouncy math
        ECPoint P = ECAlgorithms.sumOfTwoMultiplies(G, s, W, r).normalize();

        // components must be bogus.
        if (P.isInfinity())
        {
            return null;
        }

        BigInteger x = P.getAffineXCoord().toBigInteger();

        return r.subtract(x).mod(n);
    }
}
