/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.browserid;

import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;

import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;

public class RSACryptoImplementation {
  public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";

  /**
   * Parameters are serialized as decimal strings. Hex-versus-decimal was
   * reverse-engineered from what the Persona public verifier accepted. We
   * expect to follow the JOSE/JWT spec as it solidifies, and that will probably
   * mean unifying this base.
   */
  protected static final int SERIALIZATION_BASE = 10;

  protected static class RSAVerifyingPublicKey implements VerifyingPublicKey {
    protected final RSAPublicKey publicKey;

    public RSAVerifyingPublicKey(RSAPublicKey publicKey) {
      this.publicKey = publicKey;
    }

    /**
     * Serialize to a JSON object.
     * <p>
     * Parameters are serialized as decimal strings. Hex-versus-decimal was
     * reverse-engineered from what the Persona public verifier accepted.
     */
    @Override
    public ExtendedJSONObject toJSONObject() {
      ExtendedJSONObject o = new ExtendedJSONObject();
      o.put("algorithm", "RS");
      o.put("n", publicKey.getModulus().toString(SERIALIZATION_BASE));
      o.put("e", publicKey.getPublicExponent().toString(SERIALIZATION_BASE));
      return o;
    }

    @Override
    public boolean verifyMessage(byte[] bytes, byte[] signature)
        throws GeneralSecurityException {
      final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
      signer.initVerify(publicKey);
      signer.update(bytes);
      return signer.verify(signature);
    }
  }

  protected static class RSASigningPrivateKey implements SigningPrivateKey {
    protected final RSAPrivateKey privateKey;

    public RSASigningPrivateKey(RSAPrivateKey privateKey) {
      this.privateKey = privateKey;
    }

    @Override
    public String getAlgorithm() {
      return "RS" + (privateKey.getModulus().bitLength() + 7)/8;
    }

    /**
     * Serialize to a JSON object.
     * <p>
     * Parameters are serialized as decimal strings. Hex-versus-decimal was
     * reverse-engineered from what the Persona public verifier accepted.
     */
    @Override
    public ExtendedJSONObject toJSONObject() {
      ExtendedJSONObject o = new ExtendedJSONObject();
      o.put("algorithm", "RS");
      o.put("n", privateKey.getModulus().toString(SERIALIZATION_BASE));
      o.put("d", privateKey.getPrivateExponent().toString(SERIALIZATION_BASE));
      return o;
    }

    @Override
    public byte[] signMessage(byte[] bytes)
        throws GeneralSecurityException {
      final Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
      signer.initSign(privateKey);
      signer.update(bytes);
      return signer.sign();
    }
  }

  public static BrowserIDKeyPair generateKeyPair(final int keysize) throws NoSuchAlgorithmException {
    final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
    keyPairGenerator.initialize(keysize);
    final KeyPair keyPair = keyPairGenerator.generateKeyPair();
    RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
    RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
    return new BrowserIDKeyPair(new RSASigningPrivateKey(privateKey), new RSAVerifyingPublicKey(publicKey));
  }

  public static SigningPrivateKey createPrivateKey(BigInteger n, BigInteger d) throws NoSuchAlgorithmException, InvalidKeySpecException {
    if (n == null) {
      throw new IllegalArgumentException("n must not be null");
    }
    if (d == null) {
      throw new IllegalArgumentException("d must not be null");
    }
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    KeySpec keySpec = new RSAPrivateKeySpec(n, d);
    RSAPrivateKey privateKey = (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
    return new RSASigningPrivateKey(privateKey);
  }

  public static VerifyingPublicKey createPublicKey(BigInteger n, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException {
    if (n == null) {
      throw new IllegalArgumentException("n must not be null");
    }
    if (e == null) {
      throw new IllegalArgumentException("e must not be null");
    }
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    KeySpec keySpec = new RSAPublicKeySpec(n, e);
    RSAPublicKey publicKey = (RSAPublicKey) keyFactory.generatePublic(keySpec);
    return new RSAVerifyingPublicKey(publicKey);
  }

  public static SigningPrivateKey createPrivateKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
    String algorithm = o.getString("algorithm");
    if (!"RS".equals(algorithm)) {
      throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
    }
    try {
      BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
      BigInteger d = new BigInteger(o.getString("d"), SERIALIZATION_BASE);
      return createPrivateKey(n, d);
    } catch (NullPointerException | NumberFormatException e) {
      throw new InvalidKeySpecException("n and d must be integers encoded as strings, base " + SERIALIZATION_BASE);
    }
  }

  public static VerifyingPublicKey createPublicKey(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
    String algorithm = o.getString("algorithm");
    if (!"RS".equals(algorithm)) {
      throw new InvalidKeySpecException("algorithm must equal RS, was " + algorithm);
    }
    try {
      BigInteger n = new BigInteger(o.getString("n"), SERIALIZATION_BASE);
      BigInteger e = new BigInteger(o.getString("e"), SERIALIZATION_BASE);
      return createPublicKey(n, e);
    } catch (NullPointerException | NumberFormatException e) {
      throw new InvalidKeySpecException("n and e must be integers encoded as strings, base " + SERIALIZATION_BASE);
    }
  }

  public static BrowserIDKeyPair fromJSONObject(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
    try {
      ExtendedJSONObject privateKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PRIVATEKEY);
      ExtendedJSONObject publicKey = o.getObject(BrowserIDKeyPair.JSON_KEY_PUBLICKEY);
      if (privateKey == null) {
        throw new InvalidKeySpecException("privateKey must not be null");
      }
      if (publicKey == null) {
        throw new InvalidKeySpecException("publicKey must not be null");
      }
      return new BrowserIDKeyPair(createPrivateKey(privateKey), createPublicKey(publicKey));
    } catch (NonObjectJSONException e) {
      throw new InvalidKeySpecException("privateKey and publicKey must be JSON objects");
    }
  }
}
