package org.bouncycastle.crypto.agreement.srp;

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

import org.bouncycastle.crypto.CryptoException;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.params.SRP6GroupParameters;

/**
 * Implements the server side SRP-6a protocol. Note that this class is stateful, and therefore NOT threadsafe.
 * This implementation of SRP is based on the optimized message sequence put forth by Thomas Wu in the paper
 * "SRP-6: Improvements and Refinements to the Secure Remote Password Protocol, 2002"
 */
public class SRP6Server
{
    protected BigInteger N;
    protected BigInteger g;
    protected BigInteger v;

    protected SecureRandom random;
    protected Digest digest;

    protected BigInteger A;

    protected BigInteger b;
    protected BigInteger B;

    protected BigInteger u;
    protected BigInteger S;
    protected BigInteger M1;
	protected BigInteger M2;
	protected BigInteger Key;
	
    public SRP6Server()
    {
    }

    /**
     * Initialises the server to accept a new client authentication attempt
     * @param N The safe prime associated with the client's verifier
     * @param g The group parameter associated with the client's verifier
     * @param v The client's verifier
     * @param digest The digest algorithm associated with the client's verifier
     * @param random For key generation
     */
    public void init(BigInteger N, BigInteger g, BigInteger v, Digest digest, SecureRandom random)
    {
        this.N = N;
        this.g = g;
        this.v = v;

        this.random = random;
        this.digest = digest;
    }

    public void init(SRP6GroupParameters group, BigInteger v, Digest digest, SecureRandom random)
    {
        init(group.getN(), group.getG(), v, digest, random);
    }

    /**
     * Generates the server's credentials that are to be sent to the client.
     * @return The server's public value to the client
     */
    public BigInteger generateServerCredentials()
    {
        BigInteger k = SRP6Util.calculateK(digest, N, g);
        this.b = selectPrivateValue();
        this.B = k.multiply(v).mod(N).add(g.modPow(b, N)).mod(N);

        return B;
    }

    /**
     * Processes the client's credentials. If valid the shared secret is generated and returned.
     * @param clientA The client's credentials
     * @return A shared secret BigInteger
     * @throws CryptoException If client's credentials are invalid
     */
    public BigInteger calculateSecret(BigInteger clientA) throws CryptoException
    {
        this.A = SRP6Util.validatePublicValue(N, clientA);
        this.u = SRP6Util.calculateU(digest, N, A, B);
        this.S = calculateS();

        return S;
    }

    protected BigInteger selectPrivateValue()
    {
        return SRP6Util.generatePrivateValue(digest, N, g, random);        
    }

    private BigInteger calculateS()
    {
        return v.modPow(u, N).multiply(A).mod(N).modPow(b, N);
    }
    
    /** 
	 * Authenticates the received client evidence message M1 and saves it only if correct.
	 * To be called after calculating the secret S.
	 * @param clientM1 the client side generated evidence message
	 * @return A boolean indicating if the client message M1 was the expected one.
	 * @throws CryptoException 
	 */
	public boolean verifyClientEvidenceMessage(BigInteger clientM1) throws CryptoException
	{
		// Verify pre-requirements
		if (this.A == null || this.B == null || this.S == null)
		{
			throw new CryptoException("Impossible to compute and verify M1: " +
					"some data are missing from the previous operations (A,B,S)");
		}

		// Compute the own client evidence message 'M1'
		BigInteger computedM1 = SRP6Util.calculateM1(digest, N, A, B, S);
		if (computedM1.equals(clientM1))
		{
			this.M1 = clientM1;
			return true;
		}
		return false;
	}
	
	/**
	 * Computes the server evidence message M2 using the previously verified values.
	 * To be called after successfully verifying the client evidence message M1.
	 * @return M2: the server side generated evidence message
	 * @throws CryptoException
	 */
	public BigInteger calculateServerEvidenceMessage() throws CryptoException
	{
		// Verify pre-requirements
		if (this.A == null || this.M1 == null || this.S == null)
		{
			throw new CryptoException("Impossible to compute M2: " +
					"some data are missing from the previous operations (A,M1,S)");
		}

		// Compute the server evidence message 'M2'
		this.M2 = SRP6Util.calculateM2(digest, N, A, M1, S);  
		return M2;
	}
	
	/**
	 * Computes the final session key as a result of the SRP successful mutual authentication
	 * To be called after calculating the server evidence message M2.
	 * @return Key: the mutual authenticated symmetric session key
	 * @throws CryptoException
	 */
	public BigInteger calculateSessionKey() throws CryptoException
	{
		// Verify pre-requirements
		if (this.S == null || this.M1 == null || this.M2 == null)
		{
			throw new CryptoException("Impossible to compute Key: " +
					"some data are missing from the previous operations (S,M1,M2)");
		}
		this.Key = SRP6Util.calculateKey(digest, N, S);
		return Key;
	}
}
