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.
		
		
		
		
		
			
		
			
				
					291 lines
				
				7.3 KiB
			
		
		
			
		
	
	
					291 lines
				
				7.3 KiB
			| 
											3 years ago
										 | // Copyright 2018 Joyent, Inc.
 | ||
|  | 
 | ||
|  | module.exports = { | ||
|  | 	read: read, | ||
|  | 	write: write | ||
|  | }; | ||
|  | 
 | ||
|  | var assert = require('assert-plus'); | ||
|  | var asn1 = require('asn1'); | ||
|  | var crypto = require('crypto'); | ||
|  | var Buffer = require('safer-buffer').Buffer; | ||
|  | var algs = require('../algs'); | ||
|  | var utils = require('../utils'); | ||
|  | var Key = require('../key'); | ||
|  | var PrivateKey = require('../private-key'); | ||
|  | 
 | ||
|  | var pkcs1 = require('./pkcs1'); | ||
|  | var pkcs8 = require('./pkcs8'); | ||
|  | var sshpriv = require('./ssh-private'); | ||
|  | var rfc4253 = require('./rfc4253'); | ||
|  | 
 | ||
|  | var errors = require('../errors'); | ||
|  | 
 | ||
|  | var OID_PBES2 = '1.2.840.113549.1.5.13'; | ||
|  | var OID_PBKDF2 = '1.2.840.113549.1.5.12'; | ||
|  | 
 | ||
|  | var OID_TO_CIPHER = { | ||
|  | 	'1.2.840.113549.3.7': '3des-cbc', | ||
|  | 	'2.16.840.1.101.3.4.1.2': 'aes128-cbc', | ||
|  | 	'2.16.840.1.101.3.4.1.42': 'aes256-cbc' | ||
|  | }; | ||
|  | var CIPHER_TO_OID = {}; | ||
|  | Object.keys(OID_TO_CIPHER).forEach(function (k) { | ||
|  | 	CIPHER_TO_OID[OID_TO_CIPHER[k]] = k; | ||
|  | }); | ||
|  | 
 | ||
|  | var OID_TO_HASH = { | ||
|  | 	'1.2.840.113549.2.7': 'sha1', | ||
|  | 	'1.2.840.113549.2.9': 'sha256', | ||
|  | 	'1.2.840.113549.2.11': 'sha512' | ||
|  | }; | ||
|  | var HASH_TO_OID = {}; | ||
|  | Object.keys(OID_TO_HASH).forEach(function (k) { | ||
|  | 	HASH_TO_OID[OID_TO_HASH[k]] = k; | ||
|  | }); | ||
|  | 
 | ||
|  | /* | ||
|  |  * For reading we support both PKCS#1 and PKCS#8. If we find a private key, | ||
|  |  * we just take the public component of it and use that. | ||
|  |  */ | ||
|  | function read(buf, options, forceType) { | ||
|  | 	var input = buf; | ||
|  | 	if (typeof (buf) !== 'string') { | ||
|  | 		assert.buffer(buf, 'buf'); | ||
|  | 		buf = buf.toString('ascii'); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var lines = buf.trim().split(/[\r\n]+/g); | ||
|  | 
 | ||
|  | 	var m; | ||
|  | 	var si = -1; | ||
|  | 	while (!m && si < lines.length) { | ||
|  | 		m = lines[++si].match(/*JSSTYLED*/ | ||
|  | 		    /[-]+[ ]*BEGIN ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); | ||
|  | 	} | ||
|  | 	assert.ok(m, 'invalid PEM header'); | ||
|  | 
 | ||
|  | 	var m2; | ||
|  | 	var ei = lines.length; | ||
|  | 	while (!m2 && ei > 0) { | ||
|  | 		m2 = lines[--ei].match(/*JSSTYLED*/ | ||
|  | 		    /[-]+[ ]*END ([A-Z0-9][A-Za-z0-9]+ )?(PUBLIC|PRIVATE) KEY[ ]*[-]+/); | ||
|  | 	} | ||
|  | 	assert.ok(m2, 'invalid PEM footer'); | ||
|  | 
 | ||
|  | 	/* Begin and end banners must match key type */ | ||
|  | 	assert.equal(m[2], m2[2]); | ||
|  | 	var type = m[2].toLowerCase(); | ||
|  | 
 | ||
|  | 	var alg; | ||
|  | 	if (m[1]) { | ||
|  | 		/* They also must match algorithms, if given */ | ||
|  | 		assert.equal(m[1], m2[1], 'PEM header and footer mismatch'); | ||
|  | 		alg = m[1].trim(); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	lines = lines.slice(si, ei + 1); | ||
|  | 
 | ||
|  | 	var headers = {}; | ||
|  | 	while (true) { | ||
|  | 		lines = lines.slice(1); | ||
|  | 		m = lines[0].match(/*JSSTYLED*/ | ||
|  | 		    /^([A-Za-z0-9-]+): (.+)$/); | ||
|  | 		if (!m) | ||
|  | 			break; | ||
|  | 		headers[m[1].toLowerCase()] = m[2]; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/* Chop off the first and last lines */ | ||
|  | 	lines = lines.slice(0, -1).join(''); | ||
|  | 	buf = Buffer.from(lines, 'base64'); | ||
|  | 
 | ||
|  | 	var cipher, key, iv; | ||
|  | 	if (headers['proc-type']) { | ||
|  | 		var parts = headers['proc-type'].split(','); | ||
|  | 		if (parts[0] === '4' && parts[1] === 'ENCRYPTED') { | ||
|  | 			if (typeof (options.passphrase) === 'string') { | ||
|  | 				options.passphrase = Buffer.from( | ||
|  | 				    options.passphrase, 'utf-8'); | ||
|  | 			} | ||
|  | 			if (!Buffer.isBuffer(options.passphrase)) { | ||
|  | 				throw (new errors.KeyEncryptedError( | ||
|  | 				    options.filename, 'PEM')); | ||
|  | 			} else { | ||
|  | 				parts = headers['dek-info'].split(','); | ||
|  | 				assert.ok(parts.length === 2); | ||
|  | 				cipher = parts[0].toLowerCase(); | ||
|  | 				iv = Buffer.from(parts[1], 'hex'); | ||
|  | 				key = utils.opensslKeyDeriv(cipher, iv, | ||
|  | 				    options.passphrase, 1).key; | ||
|  | 			} | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	if (alg && alg.toLowerCase() === 'encrypted') { | ||
|  | 		var eder = new asn1.BerReader(buf); | ||
|  | 		var pbesEnd; | ||
|  | 		eder.readSequence(); | ||
|  | 
 | ||
|  | 		eder.readSequence(); | ||
|  | 		pbesEnd = eder.offset + eder.length; | ||
|  | 
 | ||
|  | 		var method = eder.readOID(); | ||
|  | 		if (method !== OID_PBES2) { | ||
|  | 			throw (new Error('Unsupported PEM/PKCS8 encryption ' + | ||
|  | 			    'scheme: ' + method)); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		eder.readSequence();	/* PBES2-params */ | ||
|  | 
 | ||
|  | 		eder.readSequence();	/* keyDerivationFunc */ | ||
|  | 		var kdfEnd = eder.offset + eder.length; | ||
|  | 		var kdfOid = eder.readOID(); | ||
|  | 		if (kdfOid !== OID_PBKDF2) | ||
|  | 			throw (new Error('Unsupported PBES2 KDF: ' + kdfOid)); | ||
|  | 		eder.readSequence(); | ||
|  | 		var salt = eder.readString(asn1.Ber.OctetString, true); | ||
|  | 		var iterations = eder.readInt(); | ||
|  | 		var hashAlg = 'sha1'; | ||
|  | 		if (eder.offset < kdfEnd) { | ||
|  | 			eder.readSequence(); | ||
|  | 			var hashAlgOid = eder.readOID(); | ||
|  | 			hashAlg = OID_TO_HASH[hashAlgOid]; | ||
|  | 			if (hashAlg === undefined) { | ||
|  | 				throw (new Error('Unsupported PBKDF2 hash: ' + | ||
|  | 				    hashAlgOid)); | ||
|  | 			} | ||
|  | 		} | ||
|  | 		eder._offset = kdfEnd; | ||
|  | 
 | ||
|  | 		eder.readSequence();	/* encryptionScheme */ | ||
|  | 		var cipherOid = eder.readOID(); | ||
|  | 		cipher = OID_TO_CIPHER[cipherOid]; | ||
|  | 		if (cipher === undefined) { | ||
|  | 			throw (new Error('Unsupported PBES2 cipher: ' + | ||
|  | 			    cipherOid)); | ||
|  | 		} | ||
|  | 		iv = eder.readString(asn1.Ber.OctetString, true); | ||
|  | 
 | ||
|  | 		eder._offset = pbesEnd; | ||
|  | 		buf = eder.readString(asn1.Ber.OctetString, true); | ||
|  | 
 | ||
|  | 		if (typeof (options.passphrase) === 'string') { | ||
|  | 			options.passphrase = Buffer.from( | ||
|  | 			    options.passphrase, 'utf-8'); | ||
|  | 		} | ||
|  | 		if (!Buffer.isBuffer(options.passphrase)) { | ||
|  | 			throw (new errors.KeyEncryptedError( | ||
|  | 			    options.filename, 'PEM')); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		var cinfo = utils.opensshCipherInfo(cipher); | ||
|  | 
 | ||
|  | 		cipher = cinfo.opensslName; | ||
|  | 		key = utils.pbkdf2(hashAlg, salt, iterations, cinfo.keySize, | ||
|  | 		    options.passphrase); | ||
|  | 		alg = undefined; | ||
|  | 	} | ||
|  | 
 | ||
|  | 	if (cipher && key && iv) { | ||
|  | 		var cipherStream = crypto.createDecipheriv(cipher, key, iv); | ||
|  | 		var chunk, chunks = []; | ||
|  | 		cipherStream.once('error', function (e) { | ||
|  | 			if (e.toString().indexOf('bad decrypt') !== -1) { | ||
|  | 				throw (new Error('Incorrect passphrase ' + | ||
|  | 				    'supplied, could not decrypt key')); | ||
|  | 			} | ||
|  | 			throw (e); | ||
|  | 		}); | ||
|  | 		cipherStream.write(buf); | ||
|  | 		cipherStream.end(); | ||
|  | 		while ((chunk = cipherStream.read()) !== null) | ||
|  | 			chunks.push(chunk); | ||
|  | 		buf = Buffer.concat(chunks); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	/* The new OpenSSH internal format abuses PEM headers */ | ||
|  | 	if (alg && alg.toLowerCase() === 'openssh') | ||
|  | 		return (sshpriv.readSSHPrivate(type, buf, options)); | ||
|  | 	if (alg && alg.toLowerCase() === 'ssh2') | ||
|  | 		return (rfc4253.readType(type, buf, options)); | ||
|  | 
 | ||
|  | 	var der = new asn1.BerReader(buf); | ||
|  | 	der.originalInput = input; | ||
|  | 
 | ||
|  | 	/* | ||
|  | 	 * All of the PEM file types start with a sequence tag, so chop it | ||
|  | 	 * off here | ||
|  | 	 */ | ||
|  | 	der.readSequence(); | ||
|  | 
 | ||
|  | 	/* PKCS#1 type keys name an algorithm in the banner explicitly */ | ||
|  | 	if (alg) { | ||
|  | 		if (forceType) | ||
|  | 			assert.strictEqual(forceType, 'pkcs1'); | ||
|  | 		return (pkcs1.readPkcs1(alg, type, der)); | ||
|  | 	} else { | ||
|  | 		if (forceType) | ||
|  | 			assert.strictEqual(forceType, 'pkcs8'); | ||
|  | 		return (pkcs8.readPkcs8(alg, type, der)); | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | function write(key, options, type) { | ||
|  | 	assert.object(key); | ||
|  | 
 | ||
|  | 	var alg = { | ||
|  | 	    'ecdsa': 'EC', | ||
|  | 	    'rsa': 'RSA', | ||
|  | 	    'dsa': 'DSA', | ||
|  | 	    'ed25519': 'EdDSA' | ||
|  | 	}[key.type]; | ||
|  | 	var header; | ||
|  | 
 | ||
|  | 	var der = new asn1.BerWriter(); | ||
|  | 
 | ||
|  | 	if (PrivateKey.isPrivateKey(key)) { | ||
|  | 		if (type && type === 'pkcs8') { | ||
|  | 			header = 'PRIVATE KEY'; | ||
|  | 			pkcs8.writePkcs8(der, key); | ||
|  | 		} else { | ||
|  | 			if (type) | ||
|  | 				assert.strictEqual(type, 'pkcs1'); | ||
|  | 			header = alg + ' PRIVATE KEY'; | ||
|  | 			pkcs1.writePkcs1(der, key); | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} else if (Key.isKey(key)) { | ||
|  | 		if (type && type === 'pkcs1') { | ||
|  | 			header = alg + ' PUBLIC KEY'; | ||
|  | 			pkcs1.writePkcs1(der, key); | ||
|  | 		} else { | ||
|  | 			if (type) | ||
|  | 				assert.strictEqual(type, 'pkcs8'); | ||
|  | 			header = 'PUBLIC KEY'; | ||
|  | 			pkcs8.writePkcs8(der, key); | ||
|  | 		} | ||
|  | 
 | ||
|  | 	} else { | ||
|  | 		throw (new Error('key is not a Key or PrivateKey')); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var tmp = der.buffer.toString('base64'); | ||
|  | 	var len = tmp.length + (tmp.length / 64) + | ||
|  | 	    18 + 16 + header.length*2 + 10; | ||
|  | 	var buf = Buffer.alloc(len); | ||
|  | 	var o = 0; | ||
|  | 	o += buf.write('-----BEGIN ' + header + '-----\n', o); | ||
|  | 	for (var i = 0; i < tmp.length; ) { | ||
|  | 		var limit = i + 64; | ||
|  | 		if (limit > tmp.length) | ||
|  | 			limit = tmp.length; | ||
|  | 		o += buf.write(tmp.slice(i, limit), o); | ||
|  | 		buf[o++] = 10; | ||
|  | 		i = limit; | ||
|  | 	} | ||
|  | 	o += buf.write('-----END ' + header + '-----\n', o); | ||
|  | 
 | ||
|  | 	return (buf.slice(0, o)); | ||
|  | } |