263 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			263 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | // Copyright 2015 Joyent, Inc.
 | ||
|  | 
 | ||
|  | module.exports = { | ||
|  | 	read: read, | ||
|  | 	readSSHPrivate: readSSHPrivate, | ||
|  | 	write: write | ||
|  | }; | ||
|  | 
 | ||
|  | var assert = require('assert-plus'); | ||
|  | var asn1 = require('asn1'); | ||
|  | var Buffer = require('safer-buffer').Buffer; | ||
|  | var algs = require('../algs'); | ||
|  | var utils = require('../utils'); | ||
|  | var crypto = require('crypto'); | ||
|  | 
 | ||
|  | var Key = require('../key'); | ||
|  | var PrivateKey = require('../private-key'); | ||
|  | var pem = require('./pem'); | ||
|  | var rfc4253 = require('./rfc4253'); | ||
|  | var SSHBuffer = require('../ssh-buffer'); | ||
|  | var errors = require('../errors'); | ||
|  | 
 | ||
|  | var bcrypt; | ||
|  | 
 | ||
|  | function read(buf, options) { | ||
|  | 	return (pem.read(buf, options)); | ||
|  | } | ||
|  | 
 | ||
|  | var MAGIC = 'openssh-key-v1'; | ||
|  | 
 | ||
|  | function readSSHPrivate(type, buf, options) { | ||
|  | 	buf = new SSHBuffer({buffer: buf}); | ||
|  | 
 | ||
|  | 	var magic = buf.readCString(); | ||
|  | 	assert.strictEqual(magic, MAGIC, 'bad magic string'); | ||
|  | 
 | ||
|  | 	var cipher = buf.readString(); | ||
|  | 	var kdf = buf.readString(); | ||
|  | 	var kdfOpts = buf.readBuffer(); | ||
|  | 
 | ||
|  | 	var nkeys = buf.readInt(); | ||
|  | 	if (nkeys !== 1) { | ||
|  | 		throw (new Error('OpenSSH-format key file contains ' + | ||
|  | 		    'multiple keys: this is unsupported.')); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var pubKey = buf.readBuffer(); | ||
|  | 
 | ||
|  | 	if (type === 'public') { | ||
|  | 		assert.ok(buf.atEnd(), 'excess bytes left after key'); | ||
|  | 		return (rfc4253.read(pubKey)); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var privKeyBlob = buf.readBuffer(); | ||
|  | 	assert.ok(buf.atEnd(), 'excess bytes left after key'); | ||
|  | 
 | ||
|  | 	var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts }); | ||
|  | 	switch (kdf) { | ||
|  | 	case 'none': | ||
|  | 		if (cipher !== 'none') { | ||
|  | 			throw (new Error('OpenSSH-format key uses KDF "none" ' + | ||
|  | 			     'but specifies a cipher other than "none"')); | ||
|  | 		} | ||
|  | 		break; | ||
|  | 	case 'bcrypt': | ||
|  | 		var salt = kdfOptsBuf.readBuffer(); | ||
|  | 		var rounds = kdfOptsBuf.readInt(); | ||
|  | 		var cinf = utils.opensshCipherInfo(cipher); | ||
|  | 		if (bcrypt === undefined) { | ||
|  | 			bcrypt = require('bcrypt-pbkdf'); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		if (typeof (options.passphrase) === 'string') { | ||
|  | 			options.passphrase = Buffer.from(options.passphrase, | ||
|  | 			    'utf-8'); | ||
|  | 		} | ||
|  | 		if (!Buffer.isBuffer(options.passphrase)) { | ||
|  | 			throw (new errors.KeyEncryptedError( | ||
|  | 			    options.filename, 'OpenSSH')); | ||
|  | 		} | ||
|  | 
 | ||
|  | 		var pass = new Uint8Array(options.passphrase); | ||
|  | 		var salti = new Uint8Array(salt); | ||
|  | 		/* Use the pbkdf to derive both the key and the IV. */ | ||
|  | 		var out = new Uint8Array(cinf.keySize + cinf.blockSize); | ||
|  | 		var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, | ||
|  | 		    out, out.length, rounds); | ||
|  | 		if (res !== 0) { | ||
|  | 			throw (new Error('bcrypt_pbkdf function returned ' + | ||
|  | 			    'failure, parameters invalid')); | ||
|  | 		} | ||
|  | 		out = Buffer.from(out); | ||
|  | 		var ckey = out.slice(0, cinf.keySize); | ||
|  | 		var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); | ||
|  | 		var cipherStream = crypto.createDecipheriv(cinf.opensslName, | ||
|  | 		    ckey, iv); | ||
|  | 		cipherStream.setAutoPadding(false); | ||
|  | 		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(privKeyBlob); | ||
|  | 		cipherStream.end(); | ||
|  | 		while ((chunk = cipherStream.read()) !== null) | ||
|  | 			chunks.push(chunk); | ||
|  | 		privKeyBlob = Buffer.concat(chunks); | ||
|  | 		break; | ||
|  | 	default: | ||
|  | 		throw (new Error( | ||
|  | 		    'OpenSSH-format key uses unknown KDF "' + kdf + '"')); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	buf = new SSHBuffer({buffer: privKeyBlob}); | ||
|  | 
 | ||
|  | 	var checkInt1 = buf.readInt(); | ||
|  | 	var checkInt2 = buf.readInt(); | ||
|  | 	if (checkInt1 !== checkInt2) { | ||
|  | 		throw (new Error('Incorrect passphrase supplied, could not ' + | ||
|  | 		    'decrypt key')); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var ret = {}; | ||
|  | 	var key = rfc4253.readInternal(ret, 'private', buf.remainder()); | ||
|  | 
 | ||
|  | 	buf.skip(ret.consumed); | ||
|  | 
 | ||
|  | 	var comment = buf.readString(); | ||
|  | 	key.comment = comment; | ||
|  | 
 | ||
|  | 	return (key); | ||
|  | } | ||
|  | 
 | ||
|  | function write(key, options) { | ||
|  | 	var pubKey; | ||
|  | 	if (PrivateKey.isPrivateKey(key)) | ||
|  | 		pubKey = key.toPublic(); | ||
|  | 	else | ||
|  | 		pubKey = key; | ||
|  | 
 | ||
|  | 	var cipher = 'none'; | ||
|  | 	var kdf = 'none'; | ||
|  | 	var kdfopts = Buffer.alloc(0); | ||
|  | 	var cinf = { blockSize: 8 }; | ||
|  | 	var passphrase; | ||
|  | 	if (options !== undefined) { | ||
|  | 		passphrase = options.passphrase; | ||
|  | 		if (typeof (passphrase) === 'string') | ||
|  | 			passphrase = Buffer.from(passphrase, 'utf-8'); | ||
|  | 		if (passphrase !== undefined) { | ||
|  | 			assert.buffer(passphrase, 'options.passphrase'); | ||
|  | 			assert.optionalString(options.cipher, 'options.cipher'); | ||
|  | 			cipher = options.cipher; | ||
|  | 			if (cipher === undefined) | ||
|  | 				cipher = 'aes128-ctr'; | ||
|  | 			cinf = utils.opensshCipherInfo(cipher); | ||
|  | 			kdf = 'bcrypt'; | ||
|  | 		} | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var privBuf; | ||
|  | 	if (PrivateKey.isPrivateKey(key)) { | ||
|  | 		privBuf = new SSHBuffer({}); | ||
|  | 		var checkInt = crypto.randomBytes(4).readUInt32BE(0); | ||
|  | 		privBuf.writeInt(checkInt); | ||
|  | 		privBuf.writeInt(checkInt); | ||
|  | 		privBuf.write(key.toBuffer('rfc4253')); | ||
|  | 		privBuf.writeString(key.comment || ''); | ||
|  | 
 | ||
|  | 		var n = 1; | ||
|  | 		while (privBuf._offset % cinf.blockSize !== 0) | ||
|  | 			privBuf.writeChar(n++); | ||
|  | 		privBuf = privBuf.toBuffer(); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	switch (kdf) { | ||
|  | 	case 'none': | ||
|  | 		break; | ||
|  | 	case 'bcrypt': | ||
|  | 		var salt = crypto.randomBytes(16); | ||
|  | 		var rounds = 16; | ||
|  | 		var kdfssh = new SSHBuffer({}); | ||
|  | 		kdfssh.writeBuffer(salt); | ||
|  | 		kdfssh.writeInt(rounds); | ||
|  | 		kdfopts = kdfssh.toBuffer(); | ||
|  | 
 | ||
|  | 		if (bcrypt === undefined) { | ||
|  | 			bcrypt = require('bcrypt-pbkdf'); | ||
|  | 		} | ||
|  | 		var pass = new Uint8Array(passphrase); | ||
|  | 		var salti = new Uint8Array(salt); | ||
|  | 		/* Use the pbkdf to derive both the key and the IV. */ | ||
|  | 		var out = new Uint8Array(cinf.keySize + cinf.blockSize); | ||
|  | 		var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, | ||
|  | 		    out, out.length, rounds); | ||
|  | 		if (res !== 0) { | ||
|  | 			throw (new Error('bcrypt_pbkdf function returned ' + | ||
|  | 			    'failure, parameters invalid')); | ||
|  | 		} | ||
|  | 		out = Buffer.from(out); | ||
|  | 		var ckey = out.slice(0, cinf.keySize); | ||
|  | 		var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); | ||
|  | 
 | ||
|  | 		var cipherStream = crypto.createCipheriv(cinf.opensslName, | ||
|  | 		    ckey, iv); | ||
|  | 		cipherStream.setAutoPadding(false); | ||
|  | 		var chunk, chunks = []; | ||
|  | 		cipherStream.once('error', function (e) { | ||
|  | 			throw (e); | ||
|  | 		}); | ||
|  | 		cipherStream.write(privBuf); | ||
|  | 		cipherStream.end(); | ||
|  | 		while ((chunk = cipherStream.read()) !== null) | ||
|  | 			chunks.push(chunk); | ||
|  | 		privBuf = Buffer.concat(chunks); | ||
|  | 		break; | ||
|  | 	default: | ||
|  | 		throw (new Error('Unsupported kdf ' + kdf)); | ||
|  | 	} | ||
|  | 
 | ||
|  | 	var buf = new SSHBuffer({}); | ||
|  | 
 | ||
|  | 	buf.writeCString(MAGIC); | ||
|  | 	buf.writeString(cipher);	/* cipher */ | ||
|  | 	buf.writeString(kdf);		/* kdf */ | ||
|  | 	buf.writeBuffer(kdfopts);	/* kdfoptions */ | ||
|  | 
 | ||
|  | 	buf.writeInt(1);		/* nkeys */ | ||
|  | 	buf.writeBuffer(pubKey.toBuffer('rfc4253')); | ||
|  | 
 | ||
|  | 	if (privBuf) | ||
|  | 		buf.writeBuffer(privBuf); | ||
|  | 
 | ||
|  | 	buf = buf.toBuffer(); | ||
|  | 
 | ||
|  | 	var header; | ||
|  | 	if (PrivateKey.isPrivateKey(key)) | ||
|  | 		header = 'OPENSSH PRIVATE KEY'; | ||
|  | 	else | ||
|  | 		header = 'OPENSSH PUBLIC KEY'; | ||
|  | 
 | ||
|  | 	var tmp = buf.toString('base64'); | ||
|  | 	var len = tmp.length + (tmp.length / 70) + | ||
|  | 	    18 + 16 + header.length*2 + 10; | ||
|  | 	buf = Buffer.alloc(len); | ||
|  | 	var o = 0; | ||
|  | 	o += buf.write('-----BEGIN ' + header + '-----\n', o); | ||
|  | 	for (var i = 0; i < tmp.length; ) { | ||
|  | 		var limit = i + 70; | ||
|  | 		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)); | ||
|  | } |