// Copyright 2015 Joyent, Inc.

module.exports = DiffieHellman;

var assert = require('assert-plus'); var crypto = require('crypto'); var algs = require('./algs'); var utils = require('./utils'); var ed;

var Key = require('./key'); var PrivateKey = require('./private-key');

var CRYPTO_HAVE_ECDH = (crypto.createECDH !== undefined);

var ecdh, ec, jsbn;

function DiffieHellman(key) {

utils.assertCompatible(key, Key, [1, 4], 'key');
this._isPriv = PrivateKey.isPrivateKey(key, [1, 3]);
this._algo = key.type;
this._curve = key.curve;
this._key = key;
if (key.type === 'dsa') {
        if (!CRYPTO_HAVE_ECDH) {
                throw (new Error('Due to bugs in the node 0.10 ' +
                    'crypto API, node 0.12.x or later is required ' +
                    'to use DH'));
        }
        this._dh = crypto.createDiffieHellman(
            key.part.p.data, undefined,
            key.part.g.data, undefined);
        this._p = key.part.p;
        this._g = key.part.g;
        if (this._isPriv)
                this._dh.setPrivateKey(key.part.x.data);
        this._dh.setPublicKey(key.part.y.data);

} else if (key.type === 'ecdsa') {
        if (!CRYPTO_HAVE_ECDH) {
                if (ecdh === undefined)
                        ecdh = require('ecc-jsbn');
                if (ec === undefined)
                        ec = require('ecc-jsbn/lib/ec');
                if (jsbn === undefined)
                        jsbn = require('jsbn').BigInteger;

                this._ecParams = new X9ECParameters(this._curve);

                if (this._isPriv) {
                        this._priv = new ECPrivate(
                            this._ecParams, key.part.d.data);
                }
                return;
        }

        var curve = {
                'nistp256': 'prime256v1',
                'nistp384': 'secp384r1',
                'nistp521': 'secp521r1'
        }[key.curve];
        this._dh = crypto.createECDH(curve);
        if (typeof (this._dh) !== 'object' ||
            typeof (this._dh.setPrivateKey) !== 'function') {
                CRYPTO_HAVE_ECDH = false;
                DiffieHellman.call(this, key);
                return;
        }
        if (this._isPriv)
                this._dh.setPrivateKey(key.part.d.data);
        this._dh.setPublicKey(key.part.Q.data);

} else if (key.type === 'curve25519') {
        if (ed === undefined)
                ed = require('jodid25519');

        if (this._isPriv) {
                this._priv = key.part.r.data;
                if (this._priv[0] === 0x00)
                        this._priv = this._priv.slice(1);
                this._priv = this._priv.slice(0, 32);
        }

} else {
        throw (new Error('DH not supported for ' + key.type + ' keys'));
}

}

DiffieHellman.prototype.getPublicKey = function () {

if (this._isPriv)
        return (this._key.toPublic());
return (this._key);

};

DiffieHellman.prototype.getPrivateKey = function () {

if (this._isPriv)
        return (this._key);
else
        return (undefined);

}; DiffieHellman.prototype.getKey = DiffieHellman.prototype.getPrivateKey;

DiffieHellman.prototype._keyCheck = function (pk, isPub) {

assert.object(pk, 'key');
if (!isPub)
        utils.assertCompatible(pk, PrivateKey, [1, 3], 'key');
utils.assertCompatible(pk, Key, [1, 4], 'key');

if (pk.type !== this._algo) {
        throw (new Error('A ' + pk.type + ' key cannot be used in ' +
            this._algo + ' Diffie-Hellman'));
}

if (pk.curve !== this._curve) {
        throw (new Error('A key from the ' + pk.curve + ' curve ' +
            'cannot be used with a ' + this._curve +
            ' Diffie-Hellman'));
}

if (pk.type === 'dsa') {
        assert.deepEqual(pk.part.p, this._p,
            'DSA key prime does not match');
        assert.deepEqual(pk.part.g, this._g,
            'DSA key generator does not match');
}

};

DiffieHellman.prototype.setKey = function (pk) {

this._keyCheck(pk);

if (pk.type === 'dsa') {
        this._dh.setPrivateKey(pk.part.x.data);
        this._dh.setPublicKey(pk.part.y.data);

} else if (pk.type === 'ecdsa') {
        if (CRYPTO_HAVE_ECDH) {
                this._dh.setPrivateKey(pk.part.d.data);
                this._dh.setPublicKey(pk.part.Q.data);
        } else {
                this._priv = new ECPrivate(
                    this._ecParams, pk.part.d.data);
        }

} else if (pk.type === 'curve25519') {
        this._priv = pk.part.r.data;
        if (this._priv[0] === 0x00)
                this._priv = this._priv.slice(1);
        this._priv = this._priv.slice(0, 32);
}
this._key = pk;
this._isPriv = true;

}; DiffieHellman.prototype.setPrivateKey = DiffieHellman.prototype.setKey;

DiffieHellman.prototype.computeSecret = function (otherpk) {

this._keyCheck(otherpk, true);
if (!this._isPriv)
        throw (new Error('DH exchange has not been initialized with ' +
            'a private key yet'));

var pub;
if (this._algo === 'dsa') {
        return (this._dh.computeSecret(
            otherpk.part.y.data));

} else if (this._algo === 'ecdsa') {
        if (CRYPTO_HAVE_ECDH) {
                return (this._dh.computeSecret(
                    otherpk.part.Q.data));
        } else {
                pub = new ECPublic(
                    this._ecParams, otherpk.part.Q.data);
                return (this._priv.deriveSharedSecret(pub));
        }

} else if (this._algo === 'curve25519') {
        pub = otherpk.part.R.data;
        if (pub[0] === 0x00)
                pub = pub.slice(1);

        var secret = ed.dh.computeKey(
            this._priv.toString('binary'),
            pub.toString('binary'));

        return (new Buffer(secret, 'binary'));
}

throw (new Error('Invalid algorithm: ' + this._algo));

};

DiffieHellman.prototype.generateKey = function () {

var parts = [];
var priv, pub;
if (this._algo === 'dsa') {
        this._dh.generateKeys();

        parts.push({name: 'p', data: this._p.data});
        parts.push({name: 'q', data: this._key.part.q.data});
        parts.push({name: 'g', data: this._g.data});
        parts.push({name: 'y', data: this._dh.getPublicKey()});
        parts.push({name: 'x', data: this._dh.getPrivateKey()});
        this._key = new PrivateKey({
                type: 'dsa',
                parts: parts
        });
        this._isPriv = true;
        return (this._key);

} else if (this._algo === 'ecdsa') {
        if (CRYPTO_HAVE_ECDH) {
                this._dh.generateKeys();

                parts.push({name: 'curve',
                    data: new Buffer(this._curve)});
                parts.push({name: 'Q', data: this._dh.getPublicKey()});
                parts.push({name: 'd', data: this._dh.getPrivateKey()});
                this._key = new PrivateKey({
                        type: 'ecdsa',
                        curve: this._curve,
                        parts: parts
                });
                this._isPriv = true;
                return (this._key);

        } else {
                var n = this._ecParams.getN();
                var r = new jsbn(crypto.randomBytes(n.bitLength()));
                var n1 = n.subtract(jsbn.ONE);
                priv = r.mod(n1).add(jsbn.ONE);
                pub = this._ecParams.getG().multiply(priv);

                priv = new Buffer(priv.toByteArray());
                pub = new Buffer(this._ecParams.getCurve().
                    encodePointHex(pub), 'hex');

                this._priv = new ECPrivate(this._ecParams, priv);

                parts.push({name: 'curve',
                    data: new Buffer(this._curve)});
                parts.push({name: 'Q', data: pub});
                parts.push({name: 'd', data: priv});

                this._key = new PrivateKey({
                        type: 'ecdsa',
                        curve: this._curve,
                        parts: parts
                });
                this._isPriv = true;
                return (this._key);
        }

} else if (this._algo === 'curve25519') {
        priv = ed.dh.generateKey();
        pub = ed.dh.publicKey(priv);
        this._priv = priv = new Buffer(priv, 'binary');
        pub = new Buffer(pub, 'binary');

        parts.push({name: 'R', data: pub});
        parts.push({name: 'r', data: Buffer.concat([priv, pub])});
        this._key = new PrivateKey({
                type: 'curve25519',
                parts: parts
        });
        this._isPriv = true;
        return (this._key);
}

throw (new Error('Invalid algorithm: ' + this._algo));

}; DiffieHellman.prototype.generateKeys = DiffieHellman.prototype.generateKey;

/* These are helpers for using ecc-jsbn (for node 0.10 compatibility). */

function X9ECParameters(name) {

var params = algs.curves[name];
assert.object(params);

var p = new jsbn(params.p);
var a = new jsbn(params.a);
var b = new jsbn(params.b);
var n = new jsbn(params.n);
var h = jsbn.ONE;
var curve = new ec.ECCurveFp(p, a, b);
var G = curve.decodePointHex(params.G.toString('hex'));

this.curve = curve;
this.g = G;
this.n = n;
this.h = h;

} X9ECParameters.prototype.getCurve = function () { return (this.curve); }; X9ECParameters.prototype.getG = function () { return (this.g); }; X9ECParameters.prototype.getN = function () { return (this.n); }; X9ECParameters.prototype.getH = function () { return (this.h); };

function ECPublic(params, buffer) {

this._params = params;
if (buffer[0] === 0x00)
        buffer = buffer.slice(1);
this._pub = params.getCurve().decodePointHex(buffer.toString('hex'));

}

function ECPrivate(params, buffer) {

this._params = params;
this._priv = new jsbn(utils.mpNormalize(buffer));

} ECPrivate.prototype.deriveSharedSecret = function (pubKey) {

assert.ok(pubKey instanceof ECPublic);
var S = pubKey._pub.multiply(this._priv);
return (new Buffer(S.getX().toBigInteger().toByteArray()));

};