fingerprint.js (5530B)
1 // Copyright 2018 Joyent, Inc. 2 3 module.exports = Fingerprint; 4 5 var assert = require('assert-plus'); 6 var Buffer = require('safer-buffer').Buffer; 7 var algs = require('./algs'); 8 var crypto = require('crypto'); 9 var errs = require('./errors'); 10 var Key = require('./key'); 11 var PrivateKey = require('./private-key'); 12 var Certificate = require('./certificate'); 13 var utils = require('./utils'); 14 15 var FingerprintFormatError = errs.FingerprintFormatError; 16 var InvalidAlgorithmError = errs.InvalidAlgorithmError; 17 18 function Fingerprint(opts) { 19 assert.object(opts, 'options'); 20 assert.string(opts.type, 'options.type'); 21 assert.buffer(opts.hash, 'options.hash'); 22 assert.string(opts.algorithm, 'options.algorithm'); 23 24 this.algorithm = opts.algorithm.toLowerCase(); 25 if (algs.hashAlgs[this.algorithm] !== true) 26 throw (new InvalidAlgorithmError(this.algorithm)); 27 28 this.hash = opts.hash; 29 this.type = opts.type; 30 this.hashType = opts.hashType; 31 } 32 33 Fingerprint.prototype.toString = function (format) { 34 if (format === undefined) { 35 if (this.algorithm === 'md5' || this.hashType === 'spki') 36 format = 'hex'; 37 else 38 format = 'base64'; 39 } 40 assert.string(format); 41 42 switch (format) { 43 case 'hex': 44 if (this.hashType === 'spki') 45 return (this.hash.toString('hex')); 46 return (addColons(this.hash.toString('hex'))); 47 case 'base64': 48 if (this.hashType === 'spki') 49 return (this.hash.toString('base64')); 50 return (sshBase64Format(this.algorithm, 51 this.hash.toString('base64'))); 52 default: 53 throw (new FingerprintFormatError(undefined, format)); 54 } 55 }; 56 57 Fingerprint.prototype.matches = function (other) { 58 assert.object(other, 'key or certificate'); 59 if (this.type === 'key' && this.hashType !== 'ssh') { 60 utils.assertCompatible(other, Key, [1, 7], 'key with spki'); 61 if (PrivateKey.isPrivateKey(other)) { 62 utils.assertCompatible(other, PrivateKey, [1, 6], 63 'privatekey with spki support'); 64 } 65 } else if (this.type === 'key') { 66 utils.assertCompatible(other, Key, [1, 0], 'key'); 67 } else { 68 utils.assertCompatible(other, Certificate, [1, 0], 69 'certificate'); 70 } 71 72 var theirHash = other.hash(this.algorithm, this.hashType); 73 var theirHash2 = crypto.createHash(this.algorithm). 74 update(theirHash).digest('base64'); 75 76 if (this.hash2 === undefined) 77 this.hash2 = crypto.createHash(this.algorithm). 78 update(this.hash).digest('base64'); 79 80 return (this.hash2 === theirHash2); 81 }; 82 83 /*JSSTYLED*/ 84 var base64RE = /^[A-Za-z0-9+\/=]+$/; 85 /*JSSTYLED*/ 86 var hexRE = /^[a-fA-F0-9]+$/; 87 88 Fingerprint.parse = function (fp, options) { 89 assert.string(fp, 'fingerprint'); 90 91 var alg, hash, enAlgs; 92 if (Array.isArray(options)) { 93 enAlgs = options; 94 options = {}; 95 } 96 assert.optionalObject(options, 'options'); 97 if (options === undefined) 98 options = {}; 99 if (options.enAlgs !== undefined) 100 enAlgs = options.enAlgs; 101 if (options.algorithms !== undefined) 102 enAlgs = options.algorithms; 103 assert.optionalArrayOfString(enAlgs, 'algorithms'); 104 105 var hashType = 'ssh'; 106 if (options.hashType !== undefined) 107 hashType = options.hashType; 108 assert.string(hashType, 'options.hashType'); 109 110 var parts = fp.split(':'); 111 if (parts.length == 2) { 112 alg = parts[0].toLowerCase(); 113 if (!base64RE.test(parts[1])) 114 throw (new FingerprintFormatError(fp)); 115 try { 116 hash = Buffer.from(parts[1], 'base64'); 117 } catch (e) { 118 throw (new FingerprintFormatError(fp)); 119 } 120 } else if (parts.length > 2) { 121 alg = 'md5'; 122 if (parts[0].toLowerCase() === 'md5') 123 parts = parts.slice(1); 124 parts = parts.map(function (p) { 125 while (p.length < 2) 126 p = '0' + p; 127 if (p.length > 2) 128 throw (new FingerprintFormatError(fp)); 129 return (p); 130 }); 131 parts = parts.join(''); 132 if (!hexRE.test(parts) || parts.length % 2 !== 0) 133 throw (new FingerprintFormatError(fp)); 134 try { 135 hash = Buffer.from(parts, 'hex'); 136 } catch (e) { 137 throw (new FingerprintFormatError(fp)); 138 } 139 } else { 140 if (hexRE.test(fp)) { 141 hash = Buffer.from(fp, 'hex'); 142 } else if (base64RE.test(fp)) { 143 hash = Buffer.from(fp, 'base64'); 144 } else { 145 throw (new FingerprintFormatError(fp)); 146 } 147 148 switch (hash.length) { 149 case 32: 150 alg = 'sha256'; 151 break; 152 case 16: 153 alg = 'md5'; 154 break; 155 case 20: 156 alg = 'sha1'; 157 break; 158 case 64: 159 alg = 'sha512'; 160 break; 161 default: 162 throw (new FingerprintFormatError(fp)); 163 } 164 165 /* Plain hex/base64: guess it's probably SPKI unless told. */ 166 if (options.hashType === undefined) 167 hashType = 'spki'; 168 } 169 170 if (alg === undefined) 171 throw (new FingerprintFormatError(fp)); 172 173 if (algs.hashAlgs[alg] === undefined) 174 throw (new InvalidAlgorithmError(alg)); 175 176 if (enAlgs !== undefined) { 177 enAlgs = enAlgs.map(function (a) { return a.toLowerCase(); }); 178 if (enAlgs.indexOf(alg) === -1) 179 throw (new InvalidAlgorithmError(alg)); 180 } 181 182 return (new Fingerprint({ 183 algorithm: alg, 184 hash: hash, 185 type: options.type || 'key', 186 hashType: hashType 187 })); 188 }; 189 190 function addColons(s) { 191 /*JSSTYLED*/ 192 return (s.replace(/(.{2})(?=.)/g, '$1:')); 193 } 194 195 function base64Strip(s) { 196 /*JSSTYLED*/ 197 return (s.replace(/=*$/, '')); 198 } 199 200 function sshBase64Format(alg, h) { 201 return (alg.toUpperCase() + ':' + base64Strip(h)); 202 } 203 204 Fingerprint.isFingerprint = function (obj, ver) { 205 return (utils.isCompatible(obj, Fingerprint, ver)); 206 }; 207 208 /* 209 * API versions for Fingerprint: 210 * [1,0] -- initial ver 211 * [1,1] -- first tagged ver 212 * [1,2] -- hashType and spki support 213 */ 214 Fingerprint.prototype._sshpkApiVersion = [1, 2]; 215 216 Fingerprint._oldVersionDetect = function (obj) { 217 assert.func(obj.toString); 218 assert.func(obj.matches); 219 return ([1, 0]); 220 };