@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/client.js +137 -8
- package/lib/index.js +1 -0
- package/lib/keygen.js +582 -0
- package/lib/protocol/Protocol.js +65 -13
- package/lib/protocol/SFTP.js +189 -1
- package/lib/protocol/constants.js +5 -2
- package/lib/protocol/handlers.misc.js +76 -5
- package/lib/protocol/kex.js +56 -18
- package/lib/protocol/keyParser.js +6 -5
- package/lib/server.js +9 -0
- package/package.json +4 -4
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
|
+
};
|