[6a3a178] | 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 | };
|
---|