@electerm/ssh2 1.11.2 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/keygen.js ADDED
@@ -0,0 +1,582 @@
1
+ 'use strict';
2
+
3
+ const {
4
+ createCipheriv,
5
+ generateKeyPair: generateKeyPair_,
6
+ generateKeyPairSync: generateKeyPairSync_,
7
+ getCurves,
8
+ randomBytes,
9
+ } = require('crypto');
10
+
11
+ const { Ber } = require('asn1');
12
+ const bcrypt_pbkdf = require('bcrypt-pbkdf').pbkdf;
13
+
14
+ const { CIPHER_INFO } = require('./protocol/crypto.js');
15
+
16
+ const SALT_LEN = 16;
17
+ const DEFAULT_ROUNDS = 16;
18
+
19
+ const curves = getCurves();
20
+ const ciphers = new Map(Object.entries(CIPHER_INFO));
21
+
22
+ function makeArgs(type, opts) {
23
+ if (typeof type !== 'string')
24
+ throw new TypeError('Key type must be a string');
25
+
26
+ const publicKeyEncoding = { type: 'spki', format: 'der' };
27
+ const privateKeyEncoding = { type: 'pkcs8', format: 'der' };
28
+
29
+ switch (type.toLowerCase()) {
30
+ case 'rsa': {
31
+ if (typeof opts !== 'object' || opts === null)
32
+ throw new TypeError('Missing options object for RSA key');
33
+ const modulusLength = opts.bits;
34
+ if (!Number.isInteger(modulusLength))
35
+ throw new TypeError('RSA bits must be an integer');
36
+ if (modulusLength <= 0 || modulusLength > 16384)
37
+ throw new RangeError('RSA bits must be non-zero and <= 16384');
38
+ return ['rsa', { modulusLength, publicKeyEncoding, privateKeyEncoding }];
39
+ }
40
+ case 'ecdsa': {
41
+ if (typeof opts !== 'object' || opts === null)
42
+ throw new TypeError('Missing options object for ECDSA key');
43
+ if (!Number.isInteger(opts.bits))
44
+ throw new TypeError('ECDSA bits must be an integer');
45
+ let namedCurve;
46
+ switch (opts.bits) {
47
+ case 256:
48
+ namedCurve = 'prime256v1';
49
+ break;
50
+ case 384:
51
+ namedCurve = 'secp384r1';
52
+ break;
53
+ case 521:
54
+ namedCurve = 'secp521r1';
55
+ break;
56
+ default:
57
+ throw new Error('ECDSA bits must be 256, 384, or 521');
58
+ }
59
+ if (!curves.includes(namedCurve))
60
+ throw new Error('Unsupported ECDSA bits value');
61
+ return ['ec', { namedCurve, publicKeyEncoding, privateKeyEncoding }];
62
+ }
63
+ case 'ed25519':
64
+ return ['ed25519', { publicKeyEncoding, privateKeyEncoding }];
65
+ default:
66
+ throw new Error(`Unsupported key type: ${type}`);
67
+ }
68
+ }
69
+
70
+ function parseDERs(keyType, pub, priv) {
71
+ switch (keyType) {
72
+ case 'rsa': {
73
+ // Note: we don't need to parse the public key since the PKCS8 private key
74
+ // already includes the public key parameters
75
+
76
+ // Parse private key
77
+ let reader = new Ber.Reader(priv);
78
+ reader.readSequence();
79
+
80
+ // - Version
81
+ if (reader.readInt() !== 0)
82
+ throw new Error('Unsupported version in RSA private key');
83
+
84
+ // - Algorithm
85
+ reader.readSequence();
86
+ if (reader.readOID() !== '1.2.840.113549.1.1.1')
87
+ throw new Error('Bad RSA private OID');
88
+ // - Algorithm parameters (RSA has none)
89
+ if (reader.readByte() !== Ber.Null)
90
+ throw new Error('Malformed RSA private key (expected null)');
91
+ if (reader.readByte() !== 0x00) {
92
+ throw new Error(
93
+ 'Malformed RSA private key (expected zero-length null)'
94
+ );
95
+ }
96
+
97
+ reader = new Ber.Reader(reader.readString(Ber.OctetString, true));
98
+ reader.readSequence();
99
+ if (reader.readInt() !== 0)
100
+ throw new Error('Unsupported version in RSA private key');
101
+ const n = reader.readString(Ber.Integer, true);
102
+ const e = reader.readString(Ber.Integer, true);
103
+ const d = reader.readString(Ber.Integer, true);
104
+ const p = reader.readString(Ber.Integer, true);
105
+ const q = reader.readString(Ber.Integer, true);
106
+ reader.readString(Ber.Integer, true); // dmp1
107
+ reader.readString(Ber.Integer, true); // dmq1
108
+ const iqmp = reader.readString(Ber.Integer, true);
109
+
110
+ /*
111
+ OpenSSH RSA private key:
112
+ string "ssh-rsa"
113
+ string n -- public
114
+ string e -- public
115
+ string d -- private
116
+ string iqmp -- private
117
+ string p -- private
118
+ string q -- private
119
+ */
120
+ const keyName = Buffer.from('ssh-rsa');
121
+ const privBuf = Buffer.allocUnsafe(
122
+ 4 + keyName.length
123
+ + 4 + n.length
124
+ + 4 + e.length
125
+ + 4 + d.length
126
+ + 4 + iqmp.length
127
+ + 4 + p.length
128
+ + 4 + q.length
129
+ );
130
+ let pos = 0;
131
+
132
+ privBuf.writeUInt32BE(keyName.length, pos += 0);
133
+ privBuf.set(keyName, pos += 4);
134
+ privBuf.writeUInt32BE(n.length, pos += keyName.length);
135
+ privBuf.set(n, pos += 4);
136
+ privBuf.writeUInt32BE(e.length, pos += n.length);
137
+ privBuf.set(e, pos += 4);
138
+ privBuf.writeUInt32BE(d.length, pos += e.length);
139
+ privBuf.set(d, pos += 4);
140
+ privBuf.writeUInt32BE(iqmp.length, pos += d.length);
141
+ privBuf.set(iqmp, pos += 4);
142
+ privBuf.writeUInt32BE(p.length, pos += iqmp.length);
143
+ privBuf.set(p, pos += 4);
144
+ privBuf.writeUInt32BE(q.length, pos += p.length);
145
+ privBuf.set(q, pos += 4);
146
+
147
+ /*
148
+ OpenSSH RSA public key:
149
+ string "ssh-rsa"
150
+ string e -- public
151
+ string n -- public
152
+ */
153
+ const pubBuf = Buffer.allocUnsafe(
154
+ 4 + keyName.length
155
+ + 4 + e.length
156
+ + 4 + n.length
157
+ );
158
+ pos = 0;
159
+
160
+ pubBuf.writeUInt32BE(keyName.length, pos += 0);
161
+ pubBuf.set(keyName, pos += 4);
162
+ pubBuf.writeUInt32BE(e.length, pos += keyName.length);
163
+ pubBuf.set(e, pos += 4);
164
+ pubBuf.writeUInt32BE(n.length, pos += e.length);
165
+ pubBuf.set(n, pos += 4);
166
+
167
+ return { sshName: keyName.toString(), priv: privBuf, pub: pubBuf };
168
+ }
169
+ case 'ec': {
170
+ // Parse public key
171
+ let reader = new Ber.Reader(pub);
172
+ reader.readSequence();
173
+
174
+ reader.readSequence();
175
+ if (reader.readOID() !== '1.2.840.10045.2.1')
176
+ throw new Error('Bad ECDSA public OID');
177
+ // Skip curve OID, we'll get it from the private key
178
+ reader.readOID();
179
+ let pubBin = reader.readString(Ber.BitString, true);
180
+ {
181
+ // Remove leading zero bytes
182
+ let i = 0;
183
+ for (; i < pubBin.length && pubBin[i] === 0x00; ++i);
184
+ if (i > 0)
185
+ pubBin = pubBin.slice(i);
186
+ }
187
+
188
+ // Parse private key
189
+ reader = new Ber.Reader(priv);
190
+ reader.readSequence();
191
+
192
+ // - Version
193
+ if (reader.readInt() !== 0)
194
+ throw new Error('Unsupported version in ECDSA private key');
195
+
196
+ reader.readSequence();
197
+ if (reader.readOID() !== '1.2.840.10045.2.1')
198
+ throw new Error('Bad ECDSA private OID');
199
+ const curveOID = reader.readOID();
200
+ let sshCurveName;
201
+ switch (curveOID) {
202
+ case '1.2.840.10045.3.1.7':
203
+ // prime256v1/secp256r1
204
+ sshCurveName = 'nistp256';
205
+ break;
206
+ case '1.3.132.0.34':
207
+ // secp384r1
208
+ sshCurveName = 'nistp384';
209
+ break;
210
+ case '1.3.132.0.35':
211
+ // secp521r1
212
+ sshCurveName = 'nistp521';
213
+ break;
214
+ default:
215
+ throw new Error('Unsupported curve in ECDSA private key');
216
+ }
217
+
218
+ reader = new Ber.Reader(reader.readString(Ber.OctetString, true));
219
+ reader.readSequence();
220
+
221
+ // - Version
222
+ if (reader.readInt() !== 1)
223
+ throw new Error('Unsupported version in ECDSA private key');
224
+
225
+ // Add leading zero byte to prevent negative bignum in private key
226
+ const privBin = Buffer.concat([
227
+ Buffer.from([0x00]),
228
+ reader.readString(Ber.OctetString, true)
229
+ ]);
230
+
231
+ /*
232
+ OpenSSH ECDSA private key:
233
+ string "ecdsa-sha2-<sshCurveName>"
234
+ string curve name
235
+ string Q -- public
236
+ string d -- private
237
+ */
238
+ const keyName = Buffer.from(`ecdsa-sha2-${sshCurveName}`);
239
+ sshCurveName = Buffer.from(sshCurveName);
240
+ const privBuf = Buffer.allocUnsafe(
241
+ 4 + keyName.length
242
+ + 4 + sshCurveName.length
243
+ + 4 + pubBin.length
244
+ + 4 + privBin.length
245
+ );
246
+ let pos = 0;
247
+
248
+ privBuf.writeUInt32BE(keyName.length, pos += 0);
249
+ privBuf.set(keyName, pos += 4);
250
+ privBuf.writeUInt32BE(sshCurveName.length, pos += keyName.length);
251
+ privBuf.set(sshCurveName, pos += 4);
252
+ privBuf.writeUInt32BE(pubBin.length, pos += sshCurveName.length);
253
+ privBuf.set(pubBin, pos += 4);
254
+ privBuf.writeUInt32BE(privBin.length, pos += pubBin.length);
255
+ privBuf.set(privBin, pos += 4);
256
+
257
+ /*
258
+ OpenSSH ECDSA public key:
259
+ string "ecdsa-sha2-<sshCurveName>"
260
+ string curve name
261
+ string Q -- public
262
+ */
263
+ const pubBuf = Buffer.allocUnsafe(
264
+ 4 + keyName.length
265
+ + 4 + sshCurveName.length
266
+ + 4 + pubBin.length
267
+ );
268
+ pos = 0;
269
+
270
+ pubBuf.writeUInt32BE(keyName.length, pos += 0);
271
+ pubBuf.set(keyName, pos += 4);
272
+ pubBuf.writeUInt32BE(sshCurveName.length, pos += keyName.length);
273
+ pubBuf.set(sshCurveName, pos += 4);
274
+ pubBuf.writeUInt32BE(pubBin.length, pos += sshCurveName.length);
275
+ pubBuf.set(pubBin, pos += 4);
276
+
277
+ return { sshName: keyName.toString(), priv: privBuf, pub: pubBuf };
278
+ }
279
+ case 'ed25519': {
280
+ // Parse public key
281
+ let reader = new Ber.Reader(pub);
282
+ reader.readSequence();
283
+
284
+ // - Algorithm
285
+ reader.readSequence();
286
+ if (reader.readOID() !== '1.3.101.112')
287
+ throw new Error('Bad ED25519 public OID');
288
+ // - Attributes (absent for ED25519)
289
+
290
+ let pubBin = reader.readString(Ber.BitString, true);
291
+ {
292
+ // Remove leading zero bytes
293
+ let i = 0;
294
+ for (; i < pubBin.length && pubBin[i] === 0x00; ++i);
295
+ if (i > 0)
296
+ pubBin = pubBin.slice(i);
297
+ }
298
+
299
+ // Parse private key
300
+ reader = new Ber.Reader(priv);
301
+ reader.readSequence();
302
+
303
+ // - Version
304
+ if (reader.readInt() !== 0)
305
+ throw new Error('Unsupported version in ED25519 private key');
306
+
307
+ // - Algorithm
308
+ reader.readSequence();
309
+ if (reader.readOID() !== '1.3.101.112')
310
+ throw new Error('Bad ED25519 private OID');
311
+ // - Attributes (absent)
312
+
313
+ reader = new Ber.Reader(reader.readString(Ber.OctetString, true));
314
+ const privBin = reader.readString(Ber.OctetString, true);
315
+
316
+ /*
317
+ OpenSSH ed25519 private key:
318
+ string "ssh-ed25519"
319
+ string public key
320
+ string private key + public key
321
+ */
322
+ const keyName = Buffer.from('ssh-ed25519');
323
+ const privBuf = Buffer.allocUnsafe(
324
+ 4 + keyName.length
325
+ + 4 + pubBin.length
326
+ + 4 + (privBin.length + pubBin.length)
327
+ );
328
+ let pos = 0;
329
+
330
+ privBuf.writeUInt32BE(keyName.length, pos += 0);
331
+ privBuf.set(keyName, pos += 4);
332
+ privBuf.writeUInt32BE(pubBin.length, pos += keyName.length);
333
+ privBuf.set(pubBin, pos += 4);
334
+ privBuf.writeUInt32BE(
335
+ privBin.length + pubBin.length,
336
+ pos += pubBin.length
337
+ );
338
+ privBuf.set(privBin, pos += 4);
339
+ privBuf.set(pubBin, pos += privBin.length);
340
+
341
+ /*
342
+ OpenSSH ed25519 public key:
343
+ string "ssh-ed25519"
344
+ string public key
345
+ */
346
+ const pubBuf = Buffer.allocUnsafe(
347
+ 4 + keyName.length
348
+ + 4 + pubBin.length
349
+ );
350
+ pos = 0;
351
+
352
+ pubBuf.writeUInt32BE(keyName.length, pos += 0);
353
+ pubBuf.set(keyName, pos += 4);
354
+ pubBuf.writeUInt32BE(pubBin.length, pos += keyName.length);
355
+ pubBuf.set(pubBin, pos += 4);
356
+
357
+ return { sshName: keyName.toString(), priv: privBuf, pub: pubBuf };
358
+ }
359
+ }
360
+ }
361
+
362
+ function convertKeys(keyType, pub, priv, opts) {
363
+ let format = 'new';
364
+ let encrypted;
365
+ let comment = '';
366
+ if (typeof opts === 'object' && opts !== null) {
367
+ if (typeof opts.comment === 'string' && opts.comment)
368
+ comment = opts.comment;
369
+ if (typeof opts.format === 'string' && opts.format)
370
+ format = opts.format;
371
+ if (opts.passphrase) {
372
+ let passphrase;
373
+ if (typeof opts.passphrase === 'string')
374
+ passphrase = Buffer.from(opts.passphrase);
375
+ else if (Buffer.isBuffer(opts.passphrase))
376
+ passphrase = opts.passphrase;
377
+ else
378
+ throw new Error('Invalid passphrase');
379
+
380
+ if (opts.cipher === undefined)
381
+ throw new Error('Missing cipher name');
382
+ const cipher = ciphers.get(opts.cipher);
383
+ if (cipher === undefined)
384
+ throw new Error('Invalid cipher name');
385
+
386
+ if (format === 'new') {
387
+ let rounds = DEFAULT_ROUNDS;
388
+ if (opts.rounds !== undefined) {
389
+ if (!Number.isInteger(opts.rounds))
390
+ throw new TypeError('rounds must be an integer');
391
+ if (opts.rounds > 0)
392
+ rounds = opts.rounds;
393
+ }
394
+
395
+ const gen = Buffer.allocUnsafe(cipher.keyLen + cipher.ivLen);
396
+ const salt = randomBytes(SALT_LEN);
397
+ const r = bcrypt_pbkdf(
398
+ passphrase,
399
+ passphrase.length,
400
+ salt,
401
+ salt.length,
402
+ gen,
403
+ gen.length,
404
+ rounds
405
+ );
406
+ if (r !== 0)
407
+ return new Error('Failed to generate information to encrypt key');
408
+
409
+ /*
410
+ string salt
411
+ uint32 rounds
412
+ */
413
+ const kdfOptions = Buffer.allocUnsafe(4 + salt.length + 4);
414
+ {
415
+ let pos = 0;
416
+ kdfOptions.writeUInt32BE(salt.length, pos += 0);
417
+ kdfOptions.set(salt, pos += 4);
418
+ kdfOptions.writeUInt32BE(rounds, pos += salt.length);
419
+ }
420
+
421
+ encrypted = {
422
+ cipher,
423
+ cipherName: opts.cipher,
424
+ kdfName: 'bcrypt',
425
+ kdfOptions,
426
+ key: gen.slice(0, cipher.keyLen),
427
+ iv: gen.slice(cipher.keyLen),
428
+ };
429
+ }
430
+ }
431
+ }
432
+
433
+ switch (format) {
434
+ case 'new': {
435
+ let privateB64 = '-----BEGIN OPENSSH PRIVATE KEY-----\n';
436
+ let publicB64;
437
+ /*
438
+ byte[] "openssh-key-v1\0"
439
+ string ciphername
440
+ string kdfname
441
+ string kdfoptions
442
+ uint32 number of keys N
443
+ string publickey1
444
+ string encrypted, padded list of private keys
445
+ uint32 checkint
446
+ uint32 checkint
447
+ byte[] privatekey1
448
+ string comment1
449
+ byte 1
450
+ byte 2
451
+ byte 3
452
+ ...
453
+ byte padlen % 255
454
+ */
455
+ const cipherName = Buffer.from(encrypted ? encrypted.cipherName : 'none');
456
+ const kdfName = Buffer.from(encrypted ? encrypted.kdfName : 'none');
457
+ const kdfOptions = (encrypted ? encrypted.kdfOptions : Buffer.alloc(0));
458
+ const blockLen = (encrypted ? encrypted.cipher.blockLen : 8);
459
+
460
+ const parsed = parseDERs(keyType, pub, priv);
461
+
462
+ const checkInt = randomBytes(4);
463
+ const commentBin = Buffer.from(comment);
464
+ const privBlobLen = (4 + 4 + parsed.priv.length + 4 + commentBin.length);
465
+ let padding = [];
466
+ for (let i = 1; ((privBlobLen + padding.length) % blockLen); ++i)
467
+ padding.push(i & 0xFF);
468
+ padding = Buffer.from(padding);
469
+
470
+ let privBlob = Buffer.allocUnsafe(privBlobLen + padding.length);
471
+ let extra;
472
+ {
473
+ let pos = 0;
474
+ privBlob.set(checkInt, pos += 0);
475
+ privBlob.set(checkInt, pos += 4);
476
+ privBlob.set(parsed.priv, pos += 4);
477
+ privBlob.writeUInt32BE(commentBin.length, pos += parsed.priv.length);
478
+ privBlob.set(commentBin, pos += 4);
479
+ privBlob.set(padding, pos += commentBin.length);
480
+ }
481
+
482
+ if (encrypted) {
483
+ const options = { authTagLength: encrypted.cipher.authLen };
484
+ const cipher = createCipheriv(
485
+ encrypted.cipher.sslName,
486
+ encrypted.key,
487
+ encrypted.iv,
488
+ options
489
+ );
490
+ cipher.setAutoPadding(false);
491
+ privBlob = Buffer.concat([ cipher.update(privBlob), cipher.final() ]);
492
+ if (encrypted.cipher.authLen > 0)
493
+ extra = cipher.getAuthTag();
494
+ else
495
+ extra = Buffer.alloc(0);
496
+ encrypted.key.fill(0);
497
+ encrypted.iv.fill(0);
498
+ } else {
499
+ extra = Buffer.alloc(0);
500
+ }
501
+
502
+ const magicBytes = Buffer.from('openssh-key-v1\0');
503
+ const privBin = Buffer.allocUnsafe(
504
+ magicBytes.length
505
+ + 4 + cipherName.length
506
+ + 4 + kdfName.length
507
+ + 4 + kdfOptions.length
508
+ + 4
509
+ + 4 + parsed.pub.length
510
+ + 4 + privBlob.length
511
+ + extra.length
512
+ );
513
+ {
514
+ let pos = 0;
515
+ privBin.set(magicBytes, pos += 0);
516
+ privBin.writeUInt32BE(cipherName.length, pos += magicBytes.length);
517
+ privBin.set(cipherName, pos += 4);
518
+ privBin.writeUInt32BE(kdfName.length, pos += cipherName.length);
519
+ privBin.set(kdfName, pos += 4);
520
+ privBin.writeUInt32BE(kdfOptions.length, pos += kdfName.length);
521
+ privBin.set(kdfOptions, pos += 4);
522
+ privBin.writeUInt32BE(1, pos += kdfOptions.length);
523
+ privBin.writeUInt32BE(parsed.pub.length, pos += 4);
524
+ privBin.set(parsed.pub, pos += 4);
525
+ privBin.writeUInt32BE(privBlob.length, pos += parsed.pub.length);
526
+ privBin.set(privBlob, pos += 4);
527
+ privBin.set(extra, pos += privBlob.length);
528
+ }
529
+
530
+ {
531
+ const b64 = privBin.base64Slice(0, privBin.length);
532
+ let formatted = b64.replace(/.{64}/g, '$&\n');
533
+ if (b64.length & 63)
534
+ formatted += '\n';
535
+ privateB64 += formatted;
536
+ }
537
+
538
+ {
539
+ const b64 = parsed.pub.base64Slice(0, parsed.pub.length);
540
+ publicB64 = `${parsed.sshName} ${b64}${comment ? ` ${comment}` : ''}`;
541
+ }
542
+
543
+ privateB64 += '-----END OPENSSH PRIVATE KEY-----\n';
544
+ return {
545
+ private: privateB64,
546
+ public: publicB64,
547
+ };
548
+ }
549
+ default:
550
+ throw new Error('Invalid output key format');
551
+ }
552
+ }
553
+
554
+ function noop() {}
555
+
556
+ module.exports = {
557
+ generateKeyPair: (keyType, opts, cb) => {
558
+ if (typeof opts === 'function') {
559
+ cb = opts;
560
+ opts = undefined;
561
+ }
562
+ if (typeof cb !== 'function')
563
+ cb = noop;
564
+ const args = makeArgs(keyType, opts);
565
+ generateKeyPair_(...args, (err, pub, priv) => {
566
+ if (err)
567
+ return cb(err);
568
+ let ret;
569
+ try {
570
+ ret = convertKeys(args[0], pub, priv, opts);
571
+ } catch (ex) {
572
+ return cb(ex);
573
+ }
574
+ cb(null, ret);
575
+ });
576
+ },
577
+ generateKeyPairSync: (keyType, opts) => {
578
+ const args = makeArgs(keyType, opts);
579
+ const { publicKey: pub, privateKey: priv } = generateKeyPairSync_(...args);
580
+ return convertKeys(args[0], pub, priv, opts);
581
+ }
582
+ };