• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 Joyent, Inc.
2
3module.exports = {
4	read: read,
5	readSSHPrivate: readSSHPrivate,
6	write: write
7};
8
9var assert = require('assert-plus');
10var asn1 = require('asn1');
11var Buffer = require('safer-buffer').Buffer;
12var algs = require('../algs');
13var utils = require('../utils');
14var crypto = require('crypto');
15
16var Key = require('../key');
17var PrivateKey = require('../private-key');
18var pem = require('./pem');
19var rfc4253 = require('./rfc4253');
20var SSHBuffer = require('../ssh-buffer');
21var errors = require('../errors');
22
23var bcrypt;
24
25function read(buf, options) {
26	return (pem.read(buf, options));
27}
28
29var MAGIC = 'openssh-key-v1';
30
31function readSSHPrivate(type, buf, options) {
32	buf = new SSHBuffer({buffer: buf});
33
34	var magic = buf.readCString();
35	assert.strictEqual(magic, MAGIC, 'bad magic string');
36
37	var cipher = buf.readString();
38	var kdf = buf.readString();
39	var kdfOpts = buf.readBuffer();
40
41	var nkeys = buf.readInt();
42	if (nkeys !== 1) {
43		throw (new Error('OpenSSH-format key file contains ' +
44		    'multiple keys: this is unsupported.'));
45	}
46
47	var pubKey = buf.readBuffer();
48
49	if (type === 'public') {
50		assert.ok(buf.atEnd(), 'excess bytes left after key');
51		return (rfc4253.read(pubKey));
52	}
53
54	var privKeyBlob = buf.readBuffer();
55	assert.ok(buf.atEnd(), 'excess bytes left after key');
56
57	var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts });
58	switch (kdf) {
59	case 'none':
60		if (cipher !== 'none') {
61			throw (new Error('OpenSSH-format key uses KDF "none" ' +
62			     'but specifies a cipher other than "none"'));
63		}
64		break;
65	case 'bcrypt':
66		var salt = kdfOptsBuf.readBuffer();
67		var rounds = kdfOptsBuf.readInt();
68		var cinf = utils.opensshCipherInfo(cipher);
69		if (bcrypt === undefined) {
70			bcrypt = require('bcrypt-pbkdf');
71		}
72
73		if (typeof (options.passphrase) === 'string') {
74			options.passphrase = Buffer.from(options.passphrase,
75			    'utf-8');
76		}
77		if (!Buffer.isBuffer(options.passphrase)) {
78			throw (new errors.KeyEncryptedError(
79			    options.filename, 'OpenSSH'));
80		}
81
82		var pass = new Uint8Array(options.passphrase);
83		var salti = new Uint8Array(salt);
84		/* Use the pbkdf to derive both the key and the IV. */
85		var out = new Uint8Array(cinf.keySize + cinf.blockSize);
86		var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length,
87		    out, out.length, rounds);
88		if (res !== 0) {
89			throw (new Error('bcrypt_pbkdf function returned ' +
90			    'failure, parameters invalid'));
91		}
92		out = Buffer.from(out);
93		var ckey = out.slice(0, cinf.keySize);
94		var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize);
95		var cipherStream = crypto.createDecipheriv(cinf.opensslName,
96		    ckey, iv);
97		cipherStream.setAutoPadding(false);
98		var chunk, chunks = [];
99		cipherStream.once('error', function (e) {
100			if (e.toString().indexOf('bad decrypt') !== -1) {
101				throw (new Error('Incorrect passphrase ' +
102				    'supplied, could not decrypt key'));
103			}
104			throw (e);
105		});
106		cipherStream.write(privKeyBlob);
107		cipherStream.end();
108		while ((chunk = cipherStream.read()) !== null)
109			chunks.push(chunk);
110		privKeyBlob = Buffer.concat(chunks);
111		break;
112	default:
113		throw (new Error(
114		    'OpenSSH-format key uses unknown KDF "' + kdf + '"'));
115	}
116
117	buf = new SSHBuffer({buffer: privKeyBlob});
118
119	var checkInt1 = buf.readInt();
120	var checkInt2 = buf.readInt();
121	if (checkInt1 !== checkInt2) {
122		throw (new Error('Incorrect passphrase supplied, could not ' +
123		    'decrypt key'));
124	}
125
126	var ret = {};
127	var key = rfc4253.readInternal(ret, 'private', buf.remainder());
128
129	buf.skip(ret.consumed);
130
131	var comment = buf.readString();
132	key.comment = comment;
133
134	return (key);
135}
136
137function write(key, options) {
138	var pubKey;
139	if (PrivateKey.isPrivateKey(key))
140		pubKey = key.toPublic();
141	else
142		pubKey = key;
143
144	var cipher = 'none';
145	var kdf = 'none';
146	var kdfopts = Buffer.alloc(0);
147	var cinf = { blockSize: 8 };
148	var passphrase;
149	if (options !== undefined) {
150		passphrase = options.passphrase;
151		if (typeof (passphrase) === 'string')
152			passphrase = Buffer.from(passphrase, 'utf-8');
153		if (passphrase !== undefined) {
154			assert.buffer(passphrase, 'options.passphrase');
155			assert.optionalString(options.cipher, 'options.cipher');
156			cipher = options.cipher;
157			if (cipher === undefined)
158				cipher = 'aes128-ctr';
159			cinf = utils.opensshCipherInfo(cipher);
160			kdf = 'bcrypt';
161		}
162	}
163
164	var privBuf;
165	if (PrivateKey.isPrivateKey(key)) {
166		privBuf = new SSHBuffer({});
167		var checkInt = crypto.randomBytes(4).readUInt32BE(0);
168		privBuf.writeInt(checkInt);
169		privBuf.writeInt(checkInt);
170		privBuf.write(key.toBuffer('rfc4253'));
171		privBuf.writeString(key.comment || '');
172
173		var n = 1;
174		while (privBuf._offset % cinf.blockSize !== 0)
175			privBuf.writeChar(n++);
176		privBuf = privBuf.toBuffer();
177	}
178
179	switch (kdf) {
180	case 'none':
181		break;
182	case 'bcrypt':
183		var salt = crypto.randomBytes(16);
184		var rounds = 16;
185		var kdfssh = new SSHBuffer({});
186		kdfssh.writeBuffer(salt);
187		kdfssh.writeInt(rounds);
188		kdfopts = kdfssh.toBuffer();
189
190		if (bcrypt === undefined) {
191			bcrypt = require('bcrypt-pbkdf');
192		}
193		var pass = new Uint8Array(passphrase);
194		var salti = new Uint8Array(salt);
195		/* Use the pbkdf to derive both the key and the IV. */
196		var out = new Uint8Array(cinf.keySize + cinf.blockSize);
197		var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length,
198		    out, out.length, rounds);
199		if (res !== 0) {
200			throw (new Error('bcrypt_pbkdf function returned ' +
201			    'failure, parameters invalid'));
202		}
203		out = Buffer.from(out);
204		var ckey = out.slice(0, cinf.keySize);
205		var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize);
206
207		var cipherStream = crypto.createCipheriv(cinf.opensslName,
208		    ckey, iv);
209		cipherStream.setAutoPadding(false);
210		var chunk, chunks = [];
211		cipherStream.once('error', function (e) {
212			throw (e);
213		});
214		cipherStream.write(privBuf);
215		cipherStream.end();
216		while ((chunk = cipherStream.read()) !== null)
217			chunks.push(chunk);
218		privBuf = Buffer.concat(chunks);
219		break;
220	default:
221		throw (new Error('Unsupported kdf ' + kdf));
222	}
223
224	var buf = new SSHBuffer({});
225
226	buf.writeCString(MAGIC);
227	buf.writeString(cipher);	/* cipher */
228	buf.writeString(kdf);		/* kdf */
229	buf.writeBuffer(kdfopts);	/* kdfoptions */
230
231	buf.writeInt(1);		/* nkeys */
232	buf.writeBuffer(pubKey.toBuffer('rfc4253'));
233
234	if (privBuf)
235		buf.writeBuffer(privBuf);
236
237	buf = buf.toBuffer();
238
239	var header;
240	if (PrivateKey.isPrivateKey(key))
241		header = 'OPENSSH PRIVATE KEY';
242	else
243		header = 'OPENSSH PUBLIC KEY';
244
245	var tmp = buf.toString('base64');
246	var len = tmp.length + (tmp.length / 70) +
247	    18 + 16 + header.length*2 + 10;
248	buf = Buffer.alloc(len);
249	var o = 0;
250	o += buf.write('-----BEGIN ' + header + '-----\n', o);
251	for (var i = 0; i < tmp.length; ) {
252		var limit = i + 70;
253		if (limit > tmp.length)
254			limit = tmp.length;
255		o += buf.write(tmp.slice(i, limit), o);
256		buf[o++] = 10;
257		i = limit;
258	}
259	o += buf.write('-----END ' + header + '-----\n', o);
260
261	return (buf.slice(0, o));
262}
263