/*!
 * x509.js - x509 handling for bcoin
 * Copyright (c) 2016, Christopher Jeffrey (MIT License).
 * https://github.com/bcoin-org/bcoin
 */

'use strict';

var bn = require('bn.js');
var asn1 = require('./asn1');
var elliptic = require('elliptic');
var crypto = require('../crypto/crypto');

var nativeCrypto;

try {
  nativeCrypto = require('crypto');
} catch (e) {
  ;
}

var pk = exports;
var rsa = {};
var ecdsa = {};
var native = {};

rsa.prefixes = {
  md5: new Buffer('3020300c06082a864886f70d020505000410', 'hex'),
  sha1: new Buffer('3021300906052b0e03021a05000414', 'hex'),
  sha224: new Buffer('302d300d06096086480165030402040500041c', 'hex'),
  sha256: new Buffer('3031300d060960864801650304020105000420', 'hex'),
  sha384: new Buffer('3041300d060960864801650304020205000430', 'hex'),
  sha512: new Buffer('3051300d060960864801650304020305000440', 'hex'),
  md5sha1: new Buffer(0),
  ripemd160: new Buffer('30203008060628cf060300310414', 'hex')
};

// Ported from:
// https://github.com/golang/go/blob/master/src/crypto/rsa/pkcs1v15.go

rsa.verify = function verify(hashAlg, msg, sig, key) {
  var hash = crypto.hash(hashAlg, msg);
  var prefix = rsa.prefixes[hashAlg];
  var len = prefix.length + hash.length;
  var pub = asn1.parseRSAPublic(key);
  var N = new bn(pub.modulus);
  var e = new bn(pub.publicExponent);
  var k = Math.ceil(N.bitLength() / 8);
  var m, em, ok, i;

  if (k < len + 11)
    throw new Error('Message too long.');

  m = rsa.encrypt(N, e, sig);
  em = leftpad(m, k);

  ok = crypto.ceq(em[0], 0x00);
  ok &= crypto.ceq(em[1], 0x01);
  ok &= crypto.ccmp(em.slice(k - hash.length, k), hash);
  ok &= crypto.ccmp(em.slice(k - len, k - hash.length), prefix);
  ok &= crypto.ceq(em[k - len - 1], 0x00);

  for (i = 2; i < k - len - 1; i++)
    ok &= crypto.ceq(em[i], 0xff);

  return ok === 1;
};

rsa.sign = function sign(hashAlg, msg, key) {
  var hash = crypto.hash(hashAlg, msg);
  var prefix = rsa.prefixes[hashAlg];
  var len = prefix.length + hash.length;
  var priv = asn1.parseRSAPrivate(key);
  var N = new bn(priv.modulus);
  var D = new bn(priv.privateExponent);
  var k = Math.ceil(N.bitLength() / 8);
  var i, em;

  if (k < len + 11)
    throw new Error('Message too long.');

  em = new Buffer(k);
  em.fill(0);

  em[1] = 0x01;
  for (i = 2; i < k - len - 1; i++)
    em[i] = 0xff;

  prefix.copy(em, k - len);
  hash.copy(em, k - hash.length);

  return rsa.decrypt(N, D, em);
};

rsa.decrypt = function decrypt(N, D, m) {
  var c = new bn(m);

  if (c.cmp(N) > 0)
    throw new Error('Cannot decrypt.');

  return c
    .toRed(bn.red(N))
    .redPow(D)
    .fromRed()
    .toArrayLike(Buffer, 'be');
};

rsa.encrypt = function encrypt(N, e, m) {
  return new bn(m)
    .toRed(bn.red(N))
    .redPow(e)
    .fromRed()
    .toArrayLike(Buffer, 'be');
};

ecdsa.verify = function verify(curve, msg, hashAlg, key, sig) {
  var hash = crypto.hash(hashAlg, msg);
  var ec = elliptic.ec(curve);
  return ec.verify(hash, sig, key);
};

ecdsa.sign = function sign(curve, msg, hashAlg, key) {
  var hash = crypto.hash(hashAlg, msg);
  var ec = elliptic.ec(curve);
  return new Buffer(ec.sign(hash, key));
};

native.verify = function verify(alg, hash, msg, sig, key) {
  var algo, verify;

  if (!nativeCrypto)
    return false;

  algo = normalizeAlg(alg, hash);
  verify = nativeCrypto.createVerify(algo);
  verify.update(msg);

  return verify.verify(key, sig);
};

native.sign = function _sign(alg, hash, msg, key) {
  var algo, sig;

  if (!nativeCrypto)
    return false;

  algo = normalizeAlg(alg, hash);
  sig = nativeCrypto.createSign(algo);
  sig.update(msg);
  return sig.sign(key);
};

pk.pemTag = {
  dsa: 'DSA',
  rsa: 'RSA',
  ecdsa: 'EC'
};

pk.toPEM = function toPEM(key, type) {
  var tag = pk.pemTag[key.alg];
  var pem = asn1.toPEM(key.data, tag, type);

  // Key parameters, usually present
  // if selecting an EC curve.
  if (key.params)
    pem += asn1.toPEM(key.params, tag, 'parameters');

  return pem;
};

pk._verify = function verify(hash, msg, sig, key) {
  var pem;
  switch (key.alg) {
    case 'dsa':
      pem = pk.toPEM(key, 'public key');
      return native.verify(key.alg, hash, msg, sig, pem);
    case 'rsa':
      if (nativeCrypto) {
        pem = pk.toPEM(key, 'public key');
        return native.verify(key.alg, hash, msg, sig, pem);
      }
      return rsa.verify(hash, msg, sig, key.data);
    case 'ecdsa':
      if (!key.curve)
        throw new Error('No curve present.');
      return ecdsa.verify(key.curve, hash, msg, sig, key.data);
    default:
      throw new Error('Unsupported algorithm.');
  }
};

pk.verify = function verify(hash, msg, sig, key) {
  try {
    return pk._verify(hash, msg, sig, key);
  } catch (e) {
    return false;
  }
};

pk.sign = function sign(hash, msg, key) {
  var pem;
  switch (key.alg) {
    case 'dsa':
      pem = pk.toPEM(key, 'private key');
      return native.sign(key.alg, hash, msg, pem);
    case 'rsa':
      if (nativeCrypto) {
        pem = pk.toPEM(key, 'private key');
        return native.sign(key.alg, hash, msg, pem);
      }
      return rsa.sign(hash, msg, key.data);
    case 'ecdsa':
      if (!key.curve)
        throw new Error('No curve present.');
      return ecdsa.sign(key.curve, hash, msg, key.data);
    default:
      throw new Error('Unsupported algorithm.');
  }
};

function leftpad(input, size) {
  var n = input.length;
  var out;

  if (n > size)
    n = size;

  out = new Buffer(size);
  out.fill(0);

  input.copy(out, out.length - n);

  return out;
}

function normalizeAlg(alg, hash) {
  var name = alg.toUpperCase() + '-' + hash.toUpperCase();

  switch (name) {
    case 'ECDSA-SHA1':
      name = 'ecdsa-with-SHA1';
      break;
    case 'ECDSA-SHA256':
      name = 'ecdsa-with-SHA256';
      break;
  }

  return name;
}

pk.rsa = rsa;
pk.ecdsa = ecdsa;
pk.native = native;
