1 | 'use strict';
|
2 |
|
3 | const assert = require('assert');
|
4 | const { randomBytes } = require('crypto');
|
5 |
|
6 | const {
|
7 | CIPHER_INFO,
|
8 | MAC_INFO,
|
9 | bindingAvailable,
|
10 | NullCipher,
|
11 | createCipher,
|
12 | NullDecipher,
|
13 | createDecipher,
|
14 | init: cryptoInit,
|
15 | } = require('../lib/protocol/crypto.js');
|
16 |
|
17 | (async () => {
|
18 | await cryptoInit;
|
19 |
|
20 | console.log(`Crypto binding ${bindingAvailable ? '' : 'not '}available`);
|
21 | {
|
22 | const PAIRS = [
|
23 |
|
24 | ['native', 'native'],
|
25 | ['binding', 'native'],
|
26 | ['native', 'binding'],
|
27 | ['binding', 'binding'],
|
28 | ].slice(0, bindingAvailable ? 4 : 1);
|
29 |
|
30 | [
|
31 | { cipher: null },
|
32 | { cipher: 'chacha20-poly1305@openssh.com' },
|
33 | { cipher: 'aes128-gcm@openssh.com' },
|
34 | { cipher: 'aes128-cbc', mac: 'hmac-sha1-etm@openssh.com' },
|
35 | { cipher: 'aes128-ctr', mac: 'hmac-sha1' },
|
36 | { cipher: 'arcfour', mac: 'hmac-sha2-256-96' },
|
37 | ].forEach((testConfig) => {
|
38 | for (const pair of PAIRS) {
|
39 | function onCipherData(data) {
|
40 | ciphered = Buffer.concat([ciphered, data]);
|
41 | }
|
42 |
|
43 | function onDecipherPayload(payload) {
|
44 | deciphered.push(payload);
|
45 | }
|
46 |
|
47 | function reset() {
|
48 | ciphered = Buffer.alloc(0);
|
49 | deciphered = [];
|
50 | }
|
51 |
|
52 | function reinit() {
|
53 | if (testConfig.cipher === null) {
|
54 | cipher = new NullCipher(1, onCipherData);
|
55 | decipher = new NullDecipher(1, onDecipherPayload);
|
56 | } else {
|
57 | cipher = createCipher(config);
|
58 | decipher = createDecipher(config);
|
59 | }
|
60 | }
|
61 |
|
62 | let ciphered;
|
63 | let deciphered;
|
64 | let cipher;
|
65 | let decipher;
|
66 | let macSize;
|
67 | let packet;
|
68 | let payload;
|
69 | let cipherInfo;
|
70 | let config;
|
71 |
|
72 | console.log('Testing cipher: %s, mac: %s (%s encrypt, %s decrypt) ...',
|
73 | testConfig.cipher,
|
74 | testConfig.mac
|
75 | || (testConfig.cipher === null ? '<none>' : '<implicit>'),
|
76 | pair[0],
|
77 | pair[1]);
|
78 |
|
79 | if (testConfig.cipher === null) {
|
80 | cipher = new NullCipher(1, onCipherData);
|
81 | decipher = new NullDecipher(1, onDecipherPayload);
|
82 | macSize = 0;
|
83 | } else {
|
84 | cipherInfo = CIPHER_INFO[testConfig.cipher];
|
85 | let macInfo;
|
86 | let macKey;
|
87 | if (testConfig.mac) {
|
88 | macInfo = MAC_INFO[testConfig.mac];
|
89 | macKey = randomBytes(macInfo.len);
|
90 | macSize = macInfo.actualLen;
|
91 | } else if (cipherInfo.authLen) {
|
92 | macSize = cipherInfo.authLen;
|
93 | } else {
|
94 | throw new Error('Missing MAC for cipher');
|
95 | }
|
96 | const key = randomBytes(cipherInfo.keyLen);
|
97 | const iv = (cipherInfo.ivLen
|
98 | ? randomBytes(cipherInfo.ivLen)
|
99 | : Buffer.alloc(0));
|
100 | config = {
|
101 | outbound: {
|
102 | onWrite: onCipherData,
|
103 | cipherInfo,
|
104 | cipherKey: Buffer.from(key),
|
105 | cipherIV: Buffer.from(iv),
|
106 | seqno: 1,
|
107 | macInfo,
|
108 | macKey: (macKey && Buffer.from(macKey)),
|
109 | forceNative: (pair[0] === 'native'),
|
110 | },
|
111 | inbound: {
|
112 | onPayload: onDecipherPayload,
|
113 | decipherInfo: cipherInfo,
|
114 | decipherKey: Buffer.from(key),
|
115 | decipherIV: Buffer.from(iv),
|
116 | seqno: 1,
|
117 | macInfo,
|
118 | macKey: (macKey && Buffer.from(macKey)),
|
119 | forceNative: (pair[1] === 'native'),
|
120 | },
|
121 | };
|
122 | cipher = createCipher(config);
|
123 | decipher = createDecipher(config);
|
124 |
|
125 | if (pair[0] === 'binding')
|
126 | assert(/binding/i.test(cipher.constructor.name));
|
127 | else
|
128 | assert(/native/i.test(cipher.constructor.name));
|
129 | if (pair[1] === 'binding')
|
130 | assert(/binding/i.test(decipher.constructor.name));
|
131 | else
|
132 | assert(/native/i.test(decipher.constructor.name));
|
133 | }
|
134 |
|
135 | let expectedSeqno;
|
136 |
|
137 | payload = Buffer.alloc(0);
|
138 | expectedSeqno = 2;
|
139 |
|
140 | reset();
|
141 | packet = cipher.allocPacket(payload.length);
|
142 | payload.copy(packet, 5);
|
143 | cipher.encrypt(packet);
|
144 | assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length),
|
145 | undefined);
|
146 |
|
147 | assert.strictEqual(cipher.outSeqno, expectedSeqno);
|
148 | assert(ciphered.length >= 9 + macSize);
|
149 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
150 | assert.strictEqual(deciphered.length, 1);
|
151 | assert.deepStrictEqual(deciphered[0], payload);
|
152 |
|
153 |
|
154 | payload = Buffer.from([ 0xEF ]);
|
155 | expectedSeqno = 3;
|
156 |
|
157 | reset();
|
158 | packet = cipher.allocPacket(payload.length);
|
159 | payload.copy(packet, 5);
|
160 | cipher.encrypt(packet);
|
161 | assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length),
|
162 | undefined);
|
163 |
|
164 | assert.strictEqual(cipher.outSeqno, 3);
|
165 | assert(ciphered.length >= 9 + macSize);
|
166 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
167 | assert.strictEqual(deciphered.length, 1);
|
168 | assert.deepStrictEqual(deciphered[0], payload);
|
169 |
|
170 |
|
171 | payload = randomBytes(32 * 1024);
|
172 | expectedSeqno = 4;
|
173 |
|
174 | reset();
|
175 | packet = cipher.allocPacket(payload.length);
|
176 | payload.copy(packet, 5);
|
177 | cipher.encrypt(packet);
|
178 | assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length),
|
179 | undefined);
|
180 |
|
181 | assert.strictEqual(cipher.outSeqno, expectedSeqno);
|
182 | assert(ciphered.length >= 9 + macSize);
|
183 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
184 | assert.strictEqual(deciphered.length, 1);
|
185 | assert.deepStrictEqual(deciphered[0], payload);
|
186 |
|
187 |
|
188 | payload = randomBytes(4);
|
189 | expectedSeqno = 0;
|
190 | cipher.outSeqno = decipher.inSeqno = (2 ** 32) - 1;
|
191 |
|
192 | reset();
|
193 | packet = cipher.allocPacket(payload.length);
|
194 | payload.copy(packet, 5);
|
195 | cipher.encrypt(packet);
|
196 | assert.strictEqual(decipher.decrypt(ciphered, 0, ciphered.length),
|
197 | undefined);
|
198 |
|
199 | assert.strictEqual(cipher.outSeqno, expectedSeqno);
|
200 | assert(ciphered.length >= 9 + macSize);
|
201 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
202 | assert.strictEqual(deciphered.length, 1);
|
203 | assert.deepStrictEqual(deciphered[0], payload);
|
204 |
|
205 |
|
206 | payload = randomBytes(32 * 768);
|
207 | expectedSeqno = 1;
|
208 |
|
209 | reset();
|
210 | packet = cipher.allocPacket(payload.length);
|
211 | payload.copy(packet, 5);
|
212 | cipher.encrypt(packet);
|
213 | assert.strictEqual(decipher.decrypt(ciphered, 0, 2), undefined);
|
214 | assert.strictEqual(decipher.decrypt(ciphered, 2, ciphered.length),
|
215 | undefined);
|
216 |
|
217 | assert.strictEqual(cipher.outSeqno, expectedSeqno);
|
218 | assert(ciphered.length >= 9 + macSize);
|
219 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
220 | assert.strictEqual(deciphered.length, 1);
|
221 | assert.deepStrictEqual(deciphered[0], payload);
|
222 |
|
223 |
|
224 | payload = randomBytes(32 * 768);
|
225 | expectedSeqno = 2;
|
226 |
|
227 | reset();
|
228 | packet = cipher.allocPacket(payload.length);
|
229 | payload.copy(packet, 5);
|
230 | cipher.encrypt(packet);
|
231 | assert.strictEqual(decipher.decrypt(ciphered, 0, 4), undefined);
|
232 | assert.strictEqual(decipher.decrypt(ciphered, 4, ciphered.length),
|
233 | undefined);
|
234 |
|
235 | assert.strictEqual(cipher.outSeqno, expectedSeqno);
|
236 | assert(ciphered.length >= 9 + macSize);
|
237 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
238 | assert.strictEqual(deciphered.length, 1);
|
239 | assert.deepStrictEqual(deciphered[0], payload);
|
240 |
|
241 |
|
242 | payload = randomBytes(32 * 768);
|
243 | expectedSeqno = 3;
|
244 |
|
245 | reset();
|
246 | packet = cipher.allocPacket(payload.length);
|
247 | payload.copy(packet, 5);
|
248 | cipher.encrypt(packet);
|
249 | assert.strictEqual(
|
250 | decipher.decrypt(ciphered, 0, ciphered.length - macSize),
|
251 | undefined
|
252 | );
|
253 | assert.strictEqual(
|
254 | decipher.decrypt(ciphered,
|
255 | ciphered.length - macSize,
|
256 | ciphered.length),
|
257 | undefined
|
258 | );
|
259 |
|
260 | assert.strictEqual(cipher.outSeqno, expectedSeqno);
|
261 | assert(ciphered.length >= 9 + macSize);
|
262 | assert.strictEqual(decipher.inSeqno, cipher.outSeqno);
|
263 | assert.strictEqual(deciphered.length, 1);
|
264 | assert.deepStrictEqual(deciphered[0], payload);
|
265 |
|
266 |
|
267 | [0, 2 ** 32 - 1].forEach((n) => {
|
268 | reset();
|
269 | packet = cipher.allocPacket(0);
|
270 | packet.writeUInt32BE(n, 0);
|
271 | cipher.encrypt(packet);
|
272 | let threw = false;
|
273 | try {
|
274 | decipher.decrypt(ciphered, 0, ciphered.length);
|
275 | } catch (ex) {
|
276 | threw = true;
|
277 | assert(ex instanceof Error);
|
278 | assert(/packet length/i.test(ex.message));
|
279 | }
|
280 | if (!threw)
|
281 | throw new Error('Expected error');
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | reinit();
|
287 | });
|
288 |
|
289 |
|
290 | if (testConfig.cipher !== null) {
|
291 | let payloadLen;
|
292 | const blockLen = cipherInfo.blockLen;
|
293 | if (/chacha|gcm/i.test(testConfig.cipher)
|
294 | || /etm/i.test(testConfig.mac)) {
|
295 | payloadLen = blockLen - 2;
|
296 | } else {
|
297 | payloadLen = blockLen - 6;
|
298 | }
|
299 | const minLen = 4 + 1 + payloadLen + (blockLen + 1);
|
300 |
|
301 |
|
302 | assert(cipher.allocPacket(payloadLen).length >= minLen);
|
303 | }
|
304 |
|
305 |
|
306 | cipher.free();
|
307 | decipher.free();
|
308 | if (testConfig.cipher === null)
|
309 | break;
|
310 | }
|
311 | });
|
312 | }
|
313 |
|
314 |
|
315 | {
|
316 | [
|
317 | [
|
318 | [true, null],
|
319 | /invalid config/i
|
320 | ],
|
321 | [
|
322 | [{}],
|
323 | [/invalid outbound/i, /invalid inbound/i]
|
324 | ],
|
325 | [
|
326 | [{ outbound: {}, inbound: {} }],
|
327 | [/invalid outbound\.onWrite/i, /invalid inbound\.onPayload/i]
|
328 | ],
|
329 | [
|
330 | [
|
331 | { outbound: {
|
332 | onWrite: () => {},
|
333 | cipherInfo: true
|
334 | },
|
335 | inbound: {
|
336 | onPayload: () => {},
|
337 | decipherInfo: true
|
338 | },
|
339 | },
|
340 | { outbound: {
|
341 | onWrite: () => {},
|
342 | cipherInfo: null
|
343 | },
|
344 | inbound: {
|
345 | onPayload: () => {},
|
346 | decipherInfo: null
|
347 | },
|
348 | },
|
349 | ],
|
350 | [/invalid outbound\.cipherInfo/i, /invalid inbound\.decipherInfo/i]
|
351 | ],
|
352 | [
|
353 | [
|
354 | { outbound: {
|
355 | onWrite: () => {},
|
356 | cipherInfo: {},
|
357 | cipherKey: {},
|
358 | },
|
359 | inbound: {
|
360 | onPayload: () => {},
|
361 | decipherInfo: {},
|
362 | decipherKey: {},
|
363 | },
|
364 | },
|
365 | { outbound: {
|
366 | onWrite: () => {},
|
367 | cipherInfo: { keyLen: 32 },
|
368 | cipherKey: Buffer.alloc(8),
|
369 | },
|
370 | inbound: {
|
371 | onPayload: () => {},
|
372 | decipherInfo: { keyLen: 32 },
|
373 | decipherKey: Buffer.alloc(8),
|
374 | },
|
375 | },
|
376 | ],
|
377 | [/invalid outbound\.cipherKey/i, /invalid inbound\.decipherKey/i]
|
378 | ],
|
379 | [
|
380 | [
|
381 | { outbound: {
|
382 | onWrite: () => {},
|
383 | cipherInfo: { keyLen: 1, ivLen: 12 },
|
384 | cipherKey: Buffer.alloc(1),
|
385 | cipherIV: true
|
386 | },
|
387 | inbound: {
|
388 | onPayload: () => {},
|
389 | decipherInfo: { keyLen: 1, ivLen: 12 },
|
390 | decipherKey: Buffer.alloc(1),
|
391 | cipherIV: true
|
392 | },
|
393 | },
|
394 | { outbound: {
|
395 | onWrite: () => {},
|
396 | cipherInfo: { keyLen: 1, ivLen: 12 },
|
397 | cipherKey: Buffer.alloc(1),
|
398 | cipherIV: null
|
399 | },
|
400 | inbound: {
|
401 | onPayload: () => {},
|
402 | decipherInfo: { keyLen: 1, ivLen: 12 },
|
403 | decipherKey: Buffer.alloc(1),
|
404 | cipherIV: null
|
405 | },
|
406 | },
|
407 | { outbound: {
|
408 | onWrite: () => {},
|
409 | cipherInfo: { keyLen: 1, ivLen: 12 },
|
410 | cipherKey: Buffer.alloc(1),
|
411 | cipherIV: {}
|
412 | },
|
413 | inbound: {
|
414 | onPayload: () => {},
|
415 | decipherInfo: { keyLen: 1, ivLen: 12 },
|
416 | decipherKey: Buffer.alloc(1),
|
417 | cipherIV: {}
|
418 | },
|
419 | },
|
420 | { outbound: {
|
421 | onWrite: () => {},
|
422 | cipherInfo: { keyLen: 1, ivLen: 12 },
|
423 | cipherKey: Buffer.alloc(1),
|
424 | cipherIV: Buffer.alloc(1)
|
425 | },
|
426 | inbound: {
|
427 | onPayload: () => {},
|
428 | decipherInfo: { keyLen: 1, ivLen: 12 },
|
429 | decipherKey: Buffer.alloc(1),
|
430 | cipherIV: Buffer.alloc(1)
|
431 | },
|
432 | },
|
433 | ],
|
434 | [/invalid outbound\.cipherIV/i, /invalid inbound\.decipherIV/i]
|
435 | ],
|
436 | [
|
437 | [
|
438 | { outbound: {
|
439 | onWrite: () => {},
|
440 | cipherInfo: { keyLen: 1, ivLen: 0 },
|
441 | cipherKey: Buffer.alloc(1),
|
442 | seqno: true
|
443 | },
|
444 | inbound: {
|
445 | onPayload: () => {},
|
446 | decipherInfo: { keyLen: 1, ivLen: 0 },
|
447 | decipherKey: Buffer.alloc(1),
|
448 | seqno: true
|
449 | },
|
450 | },
|
451 | { outbound: {
|
452 | onWrite: () => {},
|
453 | cipherInfo: { keyLen: 1, ivLen: 0 },
|
454 | cipherKey: Buffer.alloc(1),
|
455 | seqno: -1
|
456 | },
|
457 | inbound: {
|
458 | onPayload: () => {},
|
459 | decipherInfo: { keyLen: 1, ivLen: 0 },
|
460 | decipherKey: Buffer.alloc(1),
|
461 | seqno: -1
|
462 | },
|
463 | },
|
464 | { outbound: {
|
465 | onWrite: () => {},
|
466 | cipherInfo: { keyLen: 1, ivLen: 0 },
|
467 | cipherKey: Buffer.alloc(1),
|
468 | seqno: 2 ** 32
|
469 | },
|
470 | inbound: {
|
471 | onPayload: () => {},
|
472 | decipherInfo: { keyLen: 1, ivLen: 0 },
|
473 | decipherKey: Buffer.alloc(1),
|
474 | seqno: 2 ** 32
|
475 | },
|
476 | },
|
477 | ],
|
478 | [/invalid outbound\.seqno/i, /invalid inbound\.seqno/i]
|
479 | ],
|
480 | [
|
481 | [
|
482 | { outbound: {
|
483 | onWrite: () => {},
|
484 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
485 | cipherKey: Buffer.alloc(1),
|
486 | seqno: 0
|
487 | },
|
488 | inbound: {
|
489 | onPayload: () => {},
|
490 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
491 | decipherKey: Buffer.alloc(1),
|
492 | seqno: 0
|
493 | },
|
494 | },
|
495 | { outbound: {
|
496 | onWrite: () => {},
|
497 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
498 | cipherKey: Buffer.alloc(1),
|
499 | seqno: 0,
|
500 | macInfo: true
|
501 | },
|
502 | inbound: {
|
503 | onPayload: () => {},
|
504 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
505 | decipherKey: Buffer.alloc(1),
|
506 | seqno: 0,
|
507 | macInfo: true
|
508 | },
|
509 | },
|
510 | { outbound: {
|
511 | onWrite: () => {},
|
512 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
513 | cipherKey: Buffer.alloc(1),
|
514 | seqno: 0,
|
515 | macInfo: null
|
516 | },
|
517 | inbound: {
|
518 | onPayload: () => {},
|
519 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
520 | decipherKey: Buffer.alloc(1),
|
521 | seqno: 0,
|
522 | macInfo: null
|
523 | },
|
524 | },
|
525 | ],
|
526 | [/invalid outbound\.macInfo/i, /invalid inbound\.macInfo/i]
|
527 | ],
|
528 | [
|
529 | [
|
530 | { outbound: {
|
531 | onWrite: () => {},
|
532 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
533 | cipherKey: Buffer.alloc(1),
|
534 | seqno: 0,
|
535 | macInfo: { keyLen: 16 }
|
536 | },
|
537 | inbound: {
|
538 | onPayload: () => {},
|
539 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
540 | decipherKey: Buffer.alloc(1),
|
541 | seqno: 0,
|
542 | macInfo: { keyLen: 16 }
|
543 | },
|
544 | },
|
545 | { outbound: {
|
546 | onWrite: () => {},
|
547 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
548 | cipherKey: Buffer.alloc(1),
|
549 | seqno: 0,
|
550 | macInfo: { keyLen: 16 },
|
551 | macKey: true
|
552 | },
|
553 | inbound: {
|
554 | onPayload: () => {},
|
555 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
556 | decipherKey: Buffer.alloc(1),
|
557 | seqno: 0,
|
558 | macInfo: { keyLen: 16 },
|
559 | macKey: true
|
560 | },
|
561 | },
|
562 | { outbound: {
|
563 | onWrite: () => {},
|
564 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
565 | cipherKey: Buffer.alloc(1),
|
566 | seqno: 0,
|
567 | macInfo: { keyLen: 16 },
|
568 | macKey: null
|
569 | },
|
570 | inbound: {
|
571 | onPayload: () => {},
|
572 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
573 | decipherKey: Buffer.alloc(1),
|
574 | seqno: 0,
|
575 | macInfo: { keyLen: 16 },
|
576 | macKey: null
|
577 | },
|
578 | },
|
579 | { outbound: {
|
580 | onWrite: () => {},
|
581 | cipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
582 | cipherKey: Buffer.alloc(1),
|
583 | seqno: 0,
|
584 | macInfo: { keyLen: 16 },
|
585 | macKey: Buffer.alloc(1)
|
586 | },
|
587 | inbound: {
|
588 | onPayload: () => {},
|
589 | decipherInfo: { keyLen: 1, ivLen: 0, sslName: 'foo' },
|
590 | decipherKey: Buffer.alloc(1),
|
591 | seqno: 0,
|
592 | macInfo: { keyLen: 16 },
|
593 | macKey: Buffer.alloc(1)
|
594 | },
|
595 | },
|
596 | ],
|
597 | [/invalid outbound\.macKey/i, /invalid inbound\.macKey/i]
|
598 | ],
|
599 | ].forEach((testCase) => {
|
600 | let errorChecks = testCase[1];
|
601 | if (!Array.isArray(errorChecks))
|
602 | errorChecks = [errorChecks[0], errorChecks[0]];
|
603 | for (const input of testCase[0]) {
|
604 | assert.throws(() => createCipher(input), errorChecks[0]);
|
605 | assert.throws(() => createDecipher(input), errorChecks[1]);
|
606 | }
|
607 | });
|
608 | }
|
609 | })();
|