You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					221 lines
				
				5.4 KiB
			
		
		
			
		
	
	
					221 lines
				
				5.4 KiB
			| 
											3 years ago
										 | // Copyright 2018 Joyent, Inc.
 | ||
|  | 
 | ||
|  | module.exports = Fingerprint; | ||
|  | 
 | ||
|  | var assert = require('assert-plus'); | ||
|  | var Buffer = require('safer-buffer').Buffer; | ||
|  | var algs = require('./algs'); | ||
|  | var crypto = require('crypto'); | ||
|  | var errs = require('./errors'); | ||
|  | var Key = require('./key'); | ||
|  | var PrivateKey = require('./private-key'); | ||
|  | var Certificate = require('./certificate'); | ||
|  | var utils = require('./utils'); | ||
|  | 
 | ||
|  | var FingerprintFormatError = errs.FingerprintFormatError; | ||
|  | var InvalidAlgorithmError = errs.InvalidAlgorithmError; | ||
|  | 
 | ||
|  | function Fingerprint(opts) { | ||
|  | 	assert.object(opts, 'options'); | ||
|  | 	assert.string(opts.type, 'options.type'); | ||
|  | 	assert.buffer(opts.hash, 'options.hash'); | ||
|  | 	assert.string(opts.algorithm, 'options.algorithm'); | ||
|  | 
 | ||
|  | 	this.algorithm = opts.algorithm.toLowerCase(); | ||
|  | 	if (algs.hashAlgs[this.algorithm] !== true) | ||
|  | 		throw (new InvalidAlgorithmError(this.algorithm)); | ||
|  | 
 | ||
|  | 	this.hash = opts.hash; | ||
|  | 	this.type = opts.type; | ||
|  | 	this.hashType = opts.hashType; | ||
|  | } | ||
|  | 
 | ||
|  | Fingerprint.prototype.toString = function (format) { | ||
|  | 	if (format === undefined) { | ||
|  | 		if (this.algorithm === 'md5' || this.hashType === 'spki') | ||
|  | 			format = 'hex'; | ||
|  | 		else | ||
|  | 			format = 'base64'; | ||
|  | 	} | ||
|  | 	assert.string(format); | ||
|  | 
 | ||
|  | 	switch (format) { | ||
|  | 	case 'hex': | ||
|  | 		if (this.hashType === 'spki') | ||
|  | 			return (this.hash.toString('hex')); | ||
|  | 		return (addColons(this.hash.toString('hex'))); | ||
|  | 	case 'base64': | ||
|  | 		if (this.hashType === 'spki') | ||
|  | 			return (this.hash.toString('base64')); | ||
|  | 		return (sshBase64Format(this.algorithm, | ||
|  | 		    this.hash.toString('base64'))); | ||
|  | 	default: | ||
|  | 		throw (new FingerprintFormatError(undefined, format)); | ||
|  | 	} | ||
|  | }; | ||
|  | 
 | ||
|  | Fingerprint.prototype.matches = function (other) { | ||
|  | 	assert.object(other, 'key or certificate'); | ||
|  | 	if (this.type === 'key' && this.hashType !== 'ssh') { | ||
|  | 		utils.assertCompatible(other, Key, [1, 7], 'key with spki'); | ||
|  | 		if (PrivateKey.isPrivateKey(other)) { | ||
|  | 			utils.assertCompatible(other, PrivateKey, [1, 6], | ||
|  | 			    'privatekey with spki support'); | ||
|  | 		} | ||
|  | 	} else if (this.type === 'key') { | ||
|  | 		utils.assertCompatible(other, Key, [1, 0], 'key'); | ||
|  | 	} else { | ||
|  | 		utils.assertCompatible(other, Certificate, [1, 0], | ||
|  | 		    'certificate'); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var theirHash = other.hash(this.algorithm, this.hashType); | ||
|  | 	var theirHash2 = crypto.createHash(this.algorithm). | ||
|  | 	    update(theirHash).digest('base64'); | ||
|  | 
 | ||
|  | 	if (this.hash2 === undefined) | ||
|  | 		this.hash2 = crypto.createHash(this.algorithm). | ||
|  | 		    update(this.hash).digest('base64'); | ||
|  | 
 | ||
|  | 	return (this.hash2 === theirHash2); | ||
|  | }; | ||
|  | 
 | ||
|  | /*JSSTYLED*/ | ||
|  | var base64RE = /^[A-Za-z0-9+\/=]+$/; | ||
|  | /*JSSTYLED*/ | ||
|  | var hexRE = /^[a-fA-F0-9]+$/; | ||
|  | 
 | ||
|  | Fingerprint.parse = function (fp, options) { | ||
|  | 	assert.string(fp, 'fingerprint'); | ||
|  | 
 | ||
|  | 	var alg, hash, enAlgs; | ||
|  | 	if (Array.isArray(options)) { | ||
|  | 		enAlgs = options; | ||
|  | 		options = {}; | ||
|  | 	} | ||
|  | 	assert.optionalObject(options, 'options'); | ||
|  | 	if (options === undefined) | ||
|  | 		options = {}; | ||
|  | 	if (options.enAlgs !== undefined) | ||
|  | 		enAlgs = options.enAlgs; | ||
|  | 	if (options.algorithms !== undefined) | ||
|  | 		enAlgs = options.algorithms; | ||
|  | 	assert.optionalArrayOfString(enAlgs, 'algorithms'); | ||
|  | 
 | ||
|  | 	var hashType = 'ssh'; | ||
|  | 	if (options.hashType !== undefined) | ||
|  | 		hashType = options.hashType; | ||
|  | 	assert.string(hashType, 'options.hashType'); | ||
|  | 
 | ||
|  | 	var parts = fp.split(':'); | ||
|  | 	if (parts.length == 2) { | ||
|  | 		alg = parts[0].toLowerCase(); | ||
|  | 		if (!base64RE.test(parts[1])) | ||
|  | 			throw (new FingerprintFormatError(fp)); | ||
|  | 		try { | ||
|  | 			hash = Buffer.from(parts[1], 'base64'); | ||
|  | 		} catch (e) { | ||
|  | 			throw (new FingerprintFormatError(fp)); | ||
|  | 		} | ||
|  | 	} else if (parts.length > 2) { | ||
|  | 		alg = 'md5'; | ||
|  | 		if (parts[0].toLowerCase() === 'md5') | ||
|  | 			parts = parts.slice(1); | ||
|  | 		parts = parts.map(function (p) { | ||
|  | 			while (p.length < 2) | ||
|  | 				p = '0' + p; | ||
|  | 			if (p.length > 2) | ||
|  | 				throw (new FingerprintFormatError(fp)); | ||
|  | 			return (p); | ||
|  | 		}); | ||
|  | 		parts = parts.join(''); | ||
|  | 		if (!hexRE.test(parts) || parts.length % 2 !== 0) | ||
|  | 			throw (new FingerprintFormatError(fp)); | ||
|  | 		try { | ||
|  | 			hash = Buffer.from(parts, 'hex'); | ||
|  | 		} catch (e) { | ||
|  | 			throw (new FingerprintFormatError(fp)); | ||
|  | 		} | ||
|  | 	} else { | ||
|  | 		if (hexRE.test(fp)) { | ||
|  | 			hash = Buffer.from(fp, 'hex'); | ||
|  | 		} else if (base64RE.test(fp)) { | ||
|  | 			hash = Buffer.from(fp, 'base64'); | ||
|  | 		} else { | ||
|  | 			throw (new FingerprintFormatError(fp)); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		switch (hash.length) { | ||
|  | 		case 32: | ||
|  | 			alg = 'sha256'; | ||
|  | 			break; | ||
|  | 		case 16: | ||
|  | 			alg = 'md5'; | ||
|  | 			break; | ||
|  | 		case 20: | ||
|  | 			alg = 'sha1'; | ||
|  | 			break; | ||
|  | 		case 64: | ||
|  | 			alg = 'sha512'; | ||
|  | 			break; | ||
|  | 		default: | ||
|  | 			throw (new FingerprintFormatError(fp)); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		/* Plain hex/base64: guess it's probably SPKI unless told. */ | ||
|  | 		if (options.hashType === undefined) | ||
|  | 			hashType = 'spki'; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	if (alg === undefined) | ||
|  | 		throw (new FingerprintFormatError(fp)); | ||
|  | 
 | ||
|  | 	if (algs.hashAlgs[alg] === undefined) | ||
|  | 		throw (new InvalidAlgorithmError(alg)); | ||
|  | 
 | ||
|  | 	if (enAlgs !== undefined) { | ||
|  | 		enAlgs = enAlgs.map(function (a) { return a.toLowerCase(); }); | ||
|  | 		if (enAlgs.indexOf(alg) === -1) | ||
|  | 			throw (new InvalidAlgorithmError(alg)); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	return (new Fingerprint({ | ||
|  | 		algorithm: alg, | ||
|  | 		hash: hash, | ||
|  | 		type: options.type || 'key', | ||
|  | 		hashType: hashType | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | function addColons(s) { | ||
|  | 	/*JSSTYLED*/ | ||
|  | 	return (s.replace(/(.{2})(?=.)/g, '$1:')); | ||
|  | } | ||
|  | 
 | ||
|  | function base64Strip(s) { | ||
|  | 	/*JSSTYLED*/ | ||
|  | 	return (s.replace(/=*$/, '')); | ||
|  | } | ||
|  | 
 | ||
|  | function sshBase64Format(alg, h) { | ||
|  | 	return (alg.toUpperCase() + ':' + base64Strip(h)); | ||
|  | } | ||
|  | 
 | ||
|  | Fingerprint.isFingerprint = function (obj, ver) { | ||
|  | 	return (utils.isCompatible(obj, Fingerprint, ver)); | ||
|  | }; | ||
|  | 
 | ||
|  | /* | ||
|  |  * API versions for Fingerprint: | ||
|  |  * [1,0] -- initial ver | ||
|  |  * [1,1] -- first tagged ver | ||
|  |  * [1,2] -- hashType and spki support | ||
|  |  */ | ||
|  | Fingerprint.prototype._sshpkApiVersion = [1, 2]; | ||
|  | 
 | ||
|  | Fingerprint._oldVersionDetect = function (obj) { | ||
|  | 	assert.func(obj.toString); | ||
|  | 	assert.func(obj.matches); | ||
|  | 	return ([1, 0]); | ||
|  | }; |