1// Copyright 2017 Joyent, Inc. 2 3module.exports = Key; 4 5var assert = require('assert-plus'); 6var algs = require('./algs'); 7var crypto = require('crypto'); 8var Fingerprint = require('./fingerprint'); 9var Signature = require('./signature'); 10var DiffieHellman = require('./dhe').DiffieHellman; 11var errs = require('./errors'); 12var utils = require('./utils'); 13var PrivateKey = require('./private-key'); 14var edCompat; 15 16try { 17 edCompat = require('./ed-compat'); 18} catch (e) { 19 /* Just continue through, and bail out if we try to use it. */ 20} 21 22var InvalidAlgorithmError = errs.InvalidAlgorithmError; 23var KeyParseError = errs.KeyParseError; 24 25var formats = {}; 26formats['auto'] = require('./formats/auto'); 27formats['pem'] = require('./formats/pem'); 28formats['pkcs1'] = require('./formats/pkcs1'); 29formats['pkcs8'] = require('./formats/pkcs8'); 30formats['rfc4253'] = require('./formats/rfc4253'); 31formats['ssh'] = require('./formats/ssh'); 32formats['ssh-private'] = require('./formats/ssh-private'); 33formats['openssh'] = formats['ssh-private']; 34formats['dnssec'] = require('./formats/dnssec'); 35 36function Key(opts) { 37 assert.object(opts, 'options'); 38 assert.arrayOfObject(opts.parts, 'options.parts'); 39 assert.string(opts.type, 'options.type'); 40 assert.optionalString(opts.comment, 'options.comment'); 41 42 var algInfo = algs.info[opts.type]; 43 if (typeof (algInfo) !== 'object') 44 throw (new InvalidAlgorithmError(opts.type)); 45 46 var partLookup = {}; 47 for (var i = 0; i < opts.parts.length; ++i) { 48 var part = opts.parts[i]; 49 partLookup[part.name] = part; 50 } 51 52 this.type = opts.type; 53 this.parts = opts.parts; 54 this.part = partLookup; 55 this.comment = undefined; 56 this.source = opts.source; 57 58 /* for speeding up hashing/fingerprint operations */ 59 this._rfc4253Cache = opts._rfc4253Cache; 60 this._hashCache = {}; 61 62 var sz; 63 this.curve = undefined; 64 if (this.type === 'ecdsa') { 65 var curve = this.part.curve.data.toString(); 66 this.curve = curve; 67 sz = algs.curves[curve].size; 68 } else if (this.type === 'ed25519' || this.type === 'curve25519') { 69 sz = 256; 70 this.curve = 'curve25519'; 71 } else { 72 var szPart = this.part[algInfo.sizePart]; 73 sz = szPart.data.length; 74 sz = sz * 8 - utils.countZeros(szPart.data); 75 } 76 this.size = sz; 77} 78 79Key.formats = formats; 80 81Key.prototype.toBuffer = function (format, options) { 82 if (format === undefined) 83 format = 'ssh'; 84 assert.string(format, 'format'); 85 assert.object(formats[format], 'formats[format]'); 86 assert.optionalObject(options, 'options'); 87 88 if (format === 'rfc4253') { 89 if (this._rfc4253Cache === undefined) 90 this._rfc4253Cache = formats['rfc4253'].write(this); 91 return (this._rfc4253Cache); 92 } 93 94 return (formats[format].write(this, options)); 95}; 96 97Key.prototype.toString = function (format, options) { 98 return (this.toBuffer(format, options).toString()); 99}; 100 101Key.prototype.hash = function (algo) { 102 assert.string(algo, 'algorithm'); 103 algo = algo.toLowerCase(); 104 if (algs.hashAlgs[algo] === undefined) 105 throw (new InvalidAlgorithmError(algo)); 106 107 if (this._hashCache[algo]) 108 return (this._hashCache[algo]); 109 var hash = crypto.createHash(algo). 110 update(this.toBuffer('rfc4253')).digest(); 111 this._hashCache[algo] = hash; 112 return (hash); 113}; 114 115Key.prototype.fingerprint = function (algo) { 116 if (algo === undefined) 117 algo = 'sha256'; 118 assert.string(algo, 'algorithm'); 119 var opts = { 120 type: 'key', 121 hash: this.hash(algo), 122 algorithm: algo 123 }; 124 return (new Fingerprint(opts)); 125}; 126 127Key.prototype.defaultHashAlgorithm = function () { 128 var hashAlgo = 'sha1'; 129 if (this.type === 'rsa') 130 hashAlgo = 'sha256'; 131 if (this.type === 'dsa' && this.size > 1024) 132 hashAlgo = 'sha256'; 133 if (this.type === 'ed25519') 134 hashAlgo = 'sha512'; 135 if (this.type === 'ecdsa') { 136 if (this.size <= 256) 137 hashAlgo = 'sha256'; 138 else if (this.size <= 384) 139 hashAlgo = 'sha384'; 140 else 141 hashAlgo = 'sha512'; 142 } 143 return (hashAlgo); 144}; 145 146Key.prototype.createVerify = function (hashAlgo) { 147 if (hashAlgo === undefined) 148 hashAlgo = this.defaultHashAlgorithm(); 149 assert.string(hashAlgo, 'hash algorithm'); 150 151 /* ED25519 is not supported by OpenSSL, use a javascript impl. */ 152 if (this.type === 'ed25519' && edCompat !== undefined) 153 return (new edCompat.Verifier(this, hashAlgo)); 154 if (this.type === 'curve25519') 155 throw (new Error('Curve25519 keys are not suitable for ' + 156 'signing or verification')); 157 158 var v, nm, err; 159 try { 160 nm = hashAlgo.toUpperCase(); 161 v = crypto.createVerify(nm); 162 } catch (e) { 163 err = e; 164 } 165 if (v === undefined || (err instanceof Error && 166 err.message.match(/Unknown message digest/))) { 167 nm = 'RSA-'; 168 nm += hashAlgo.toUpperCase(); 169 v = crypto.createVerify(nm); 170 } 171 assert.ok(v, 'failed to create verifier'); 172 var oldVerify = v.verify.bind(v); 173 var key = this.toBuffer('pkcs8'); 174 var curve = this.curve; 175 var self = this; 176 v.verify = function (signature, fmt) { 177 if (Signature.isSignature(signature, [2, 0])) { 178 if (signature.type !== self.type) 179 return (false); 180 if (signature.hashAlgorithm && 181 signature.hashAlgorithm !== hashAlgo) 182 return (false); 183 if (signature.curve && self.type === 'ecdsa' && 184 signature.curve !== curve) 185 return (false); 186 return (oldVerify(key, signature.toBuffer('asn1'))); 187 188 } else if (typeof (signature) === 'string' || 189 Buffer.isBuffer(signature)) { 190 return (oldVerify(key, signature, fmt)); 191 192 /* 193 * Avoid doing this on valid arguments, walking the prototype 194 * chain can be quite slow. 195 */ 196 } else if (Signature.isSignature(signature, [1, 0])) { 197 throw (new Error('signature was created by too old ' + 198 'a version of sshpk and cannot be verified')); 199 200 } else { 201 throw (new TypeError('signature must be a string, ' + 202 'Buffer, or Signature object')); 203 } 204 }; 205 return (v); 206}; 207 208Key.prototype.createDiffieHellman = function () { 209 if (this.type === 'rsa') 210 throw (new Error('RSA keys do not support Diffie-Hellman')); 211 212 return (new DiffieHellman(this)); 213}; 214Key.prototype.createDH = Key.prototype.createDiffieHellman; 215 216Key.parse = function (data, format, options) { 217 if (typeof (data) !== 'string') 218 assert.buffer(data, 'data'); 219 if (format === undefined) 220 format = 'auto'; 221 assert.string(format, 'format'); 222 if (typeof (options) === 'string') 223 options = { filename: options }; 224 assert.optionalObject(options, 'options'); 225 if (options === undefined) 226 options = {}; 227 assert.optionalString(options.filename, 'options.filename'); 228 if (options.filename === undefined) 229 options.filename = '(unnamed)'; 230 231 assert.object(formats[format], 'formats[format]'); 232 233 try { 234 var k = formats[format].read(data, options); 235 if (k instanceof PrivateKey) 236 k = k.toPublic(); 237 if (!k.comment) 238 k.comment = options.filename; 239 return (k); 240 } catch (e) { 241 if (e.name === 'KeyEncryptedError') 242 throw (e); 243 throw (new KeyParseError(options.filename, format, e)); 244 } 245}; 246 247Key.isKey = function (obj, ver) { 248 return (utils.isCompatible(obj, Key, ver)); 249}; 250 251/* 252 * API versions for Key: 253 * [1,0] -- initial ver, may take Signature for createVerify or may not 254 * [1,1] -- added pkcs1, pkcs8 formats 255 * [1,2] -- added auto, ssh-private, openssh formats 256 * [1,3] -- added defaultHashAlgorithm 257 * [1,4] -- added ed support, createDH 258 * [1,5] -- first explicitly tagged version 259 * [1,6] -- changed ed25519 part names 260 */ 261Key.prototype._sshpkApiVersion = [1, 6]; 262 263Key._oldVersionDetect = function (obj) { 264 assert.func(obj.toBuffer); 265 assert.func(obj.fingerprint); 266 if (obj.createDH) 267 return ([1, 4]); 268 if (obj.defaultHashAlgorithm) 269 return ([1, 3]); 270 if (obj.formats['auto']) 271 return ([1, 2]); 272 if (obj.formats['pkcs1']) 273 return ([1, 1]); 274 return ([1, 0]); 275}; 276