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.
		
		
		
		
		
			
		
			
				
					
					
						
							262 lines
						
					
					
						
							6.8 KiB
						
					
					
				
			
		
		
	
	
							262 lines
						
					
					
						
							6.8 KiB
						
					
					
				// 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)); | 
						|
}
 | 
						|
 |