// Copyright 2018 Joyent, Inc.

module.exports = Key;

var assert = require('assert-plus'); var algs = require('./algs'); var crypto = require('crypto'); var Fingerprint = require('./fingerprint'); var Signature = require('./signature'); var DiffieHellman = require('./dhe').DiffieHellman; var errs = require('./errors'); var utils = require('./utils'); var PrivateKey = require('./private-key'); var edCompat;

try {

edCompat = require('./ed-compat');

} catch (e) {

/* Just continue through, and bail out if we try to use it. */

}

var InvalidAlgorithmError = errs.InvalidAlgorithmError; var KeyParseError = errs.KeyParseError;

var formats = {}; formats = require('./formats/auto'); formats = require('./formats/pem'); formats = require('./formats/pkcs1'); formats = require('./formats/pkcs8'); formats = require('./formats/rfc4253'); formats = require('./formats/ssh'); formats = require('./formats/ssh-private'); formats = formats; formats = require('./formats/dnssec'); formats = require('./formats/putty'); formats = formats;

function Key(opts) {

assert.object(opts, 'options');
assert.arrayOfObject(opts.parts, 'options.parts');
assert.string(opts.type, 'options.type');
assert.optionalString(opts.comment, 'options.comment');

var algInfo = algs.info[opts.type];
if (typeof (algInfo) !== 'object')
        throw (new InvalidAlgorithmError(opts.type));

var partLookup = {};
for (var i = 0; i < opts.parts.length; ++i) {
        var part = opts.parts[i];
        partLookup[part.name] = part;
}

this.type = opts.type;
this.parts = opts.parts;
this.part = partLookup;
this.comment = undefined;
this.source = opts.source;

/* for speeding up hashing/fingerprint operations */
this._rfc4253Cache = opts._rfc4253Cache;
this._hashCache = {};

var sz;
this.curve = undefined;
if (this.type === 'ecdsa') {
        var curve = this.part.curve.data.toString();
        this.curve = curve;
        sz = algs.curves[curve].size;
} else if (this.type === 'ed25519' || this.type === 'curve25519') {
        sz = 256;
        this.curve = 'curve25519';
} else {
        var szPart = this.part[algInfo.sizePart];
        sz = szPart.data.length;
        sz = sz * 8 - utils.countZeros(szPart.data);
}
this.size = sz;

}

Key.formats = formats;

Key.prototype.toBuffer = function (format, options) {

if (format === undefined)
        format = 'ssh';
assert.string(format, 'format');
assert.object(formats[format], 'formats[format]');
assert.optionalObject(options, 'options');

if (format === 'rfc4253') {
        if (this._rfc4253Cache === undefined)
                this._rfc4253Cache = formats['rfc4253'].write(this);
        return (this._rfc4253Cache);
}

return (formats[format].write(this, options));

};

Key.prototype.toString = function (format, options) {

return (this.toBuffer(format, options).toString());

};

Key.prototype.hash = function (algo, type) {

assert.string(algo, 'algorithm');
assert.optionalString(type, 'type');
if (type === undefined)
        type = 'ssh';
algo = algo.toLowerCase();
if (algs.hashAlgs[algo] === undefined)
        throw (new InvalidAlgorithmError(algo));

var cacheKey = algo + '||' + type;
if (this._hashCache[cacheKey])
        return (this._hashCache[cacheKey]);

var buf;
if (type === 'ssh') {
        buf = this.toBuffer('rfc4253');
} else if (type === 'spki') {
        buf = formats.pkcs8.pkcs8ToBuffer(this);
} else {
        throw (new Error('Hash type ' + type + ' not supported'));
}
var hash = crypto.createHash(algo).update(buf).digest();
this._hashCache[cacheKey] = hash;
return (hash);

};

Key.prototype.fingerprint = function (algo, type) {

if (algo === undefined)
        algo = 'sha256';
if (type === undefined)
        type = 'ssh';
assert.string(algo, 'algorithm');
assert.string(type, 'type');
var opts = {
        type: 'key',
        hash: this.hash(algo, type),
        algorithm: algo,
        hashType: type
};
return (new Fingerprint(opts));

};

Key.prototype.defaultHashAlgorithm = function () {

var hashAlgo = 'sha1';
if (this.type === 'rsa')
        hashAlgo = 'sha256';
if (this.type === 'dsa' && this.size > 1024)
        hashAlgo = 'sha256';
if (this.type === 'ed25519')
        hashAlgo = 'sha512';
if (this.type === 'ecdsa') {
        if (this.size <= 256)
                hashAlgo = 'sha256';
        else if (this.size <= 384)
                hashAlgo = 'sha384';
        else
                hashAlgo = 'sha512';
}
return (hashAlgo);

};

Key.prototype.createVerify = function (hashAlgo) {

if (hashAlgo === undefined)
        hashAlgo = this.defaultHashAlgorithm();
assert.string(hashAlgo, 'hash algorithm');

/* ED25519 is not supported by OpenSSL, use a javascript impl. */
if (this.type === 'ed25519' && edCompat !== undefined)
        return (new edCompat.Verifier(this, hashAlgo));
if (this.type === 'curve25519')
        throw (new Error('Curve25519 keys are not suitable for ' +
            'signing or verification'));

var v, nm, err;
try {
        nm = hashAlgo.toUpperCase();
        v = crypto.createVerify(nm);
} catch (e) {
        err = e;
}
if (v === undefined || (err instanceof Error &&
    err.message.match(/Unknown message digest/))) {
        nm = 'RSA-';
        nm += hashAlgo.toUpperCase();
        v = crypto.createVerify(nm);
}
assert.ok(v, 'failed to create verifier');
var oldVerify = v.verify.bind(v);
var key = this.toBuffer('pkcs8');
var curve = this.curve;
var self = this;
v.verify = function (signature, fmt) {
        if (Signature.isSignature(signature, [2, 0])) {
                if (signature.type !== self.type)
                        return (false);
                if (signature.hashAlgorithm &&
                    signature.hashAlgorithm !== hashAlgo)
                        return (false);
                if (signature.curve && self.type === 'ecdsa' &&
                    signature.curve !== curve)
                        return (false);
                return (oldVerify(key, signature.toBuffer('asn1')));

        } else if (typeof (signature) === 'string' ||
            Buffer.isBuffer(signature)) {
                return (oldVerify(key, signature, fmt));

        /*
         * Avoid doing this on valid arguments, walking the prototype
         * chain can be quite slow.
         */
        } else if (Signature.isSignature(signature, [1, 0])) {
                throw (new Error('signature was created by too old ' +
                    'a version of sshpk and cannot be verified'));

        } else {
                throw (new TypeError('signature must be a string, ' +
                    'Buffer, or Signature object'));
        }
};
return (v);

};

Key.prototype.createDiffieHellman = function () {

if (this.type === 'rsa')
        throw (new Error('RSA keys do not support Diffie-Hellman'));

return (new DiffieHellman(this));

}; Key.prototype.createDH = Key.prototype.createDiffieHellman;

Key.parse = function (data, format, options) {

if (typeof (data) !== 'string')
        assert.buffer(data, 'data');
if (format === undefined)
        format = 'auto';
assert.string(format, 'format');
if (typeof (options) === 'string')
        options = { filename: options };
assert.optionalObject(options, 'options');
if (options === undefined)
        options = {};
assert.optionalString(options.filename, 'options.filename');
if (options.filename === undefined)
        options.filename = '(unnamed)';

assert.object(formats[format], 'formats[format]');

try {
        var k = formats[format].read(data, options);
        if (k instanceof PrivateKey)
                k = k.toPublic();
        if (!k.comment)
                k.comment = options.filename;
        return (k);
} catch (e) {
        if (e.name === 'KeyEncryptedError')
                throw (e);
        throw (new KeyParseError(options.filename, format, e));
}

};

Key.isKey = function (obj, ver) {

return (utils.isCompatible(obj, Key, ver));

};

/*

* API versions for Key:
* [1,0] -- initial ver, may take Signature for createVerify or may not
* [1,1] -- added pkcs1, pkcs8 formats
* [1,2] -- added auto, ssh-private, openssh formats
* [1,3] -- added defaultHashAlgorithm
* [1,4] -- added ed support, createDH
* [1,5] -- first explicitly tagged version
* [1,6] -- changed ed25519 part names
* [1,7] -- spki hash types
*/

Key.prototype._sshpkApiVersion = [1, 7];

Key._oldVersionDetect = function (obj) {

assert.func(obj.toBuffer);
assert.func(obj.fingerprint);
if (obj.createDH)
        return ([1, 4]);
if (obj.defaultHashAlgorithm)
        return ([1, 3]);
if (obj.formats['auto'])
        return ([1, 2]);
if (obj.formats['pkcs1'])
        return ([1, 1]);
return ([1, 0]);

};