| // 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['auto'] = require('./formats/auto'); |
| formats['pem'] = require('./formats/pem'); |
| formats['pkcs1'] = require('./formats/pkcs1'); |
| formats['pkcs8'] = require('./formats/pkcs8'); |
| formats['rfc4253'] = require('./formats/rfc4253'); |
| formats['ssh'] = require('./formats/ssh'); |
| formats['ssh-private'] = require('./formats/ssh-private'); |
| formats['openssh'] = formats['ssh-private']; |
| formats['dnssec'] = require('./formats/dnssec'); |
| formats['putty'] = require('./formats/putty'); |
| formats['ppk'] = formats['putty']; |
| |
| 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]); |
| }; |