Skip to content
File

Blob: src/workerd/api/node/tests/crypto_cipher-test.js

javascript622 lines
1// Copyright (c) 2017-2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import {
6 createCipheriv,
7 createDecipheriv,
8 randomBytes,
9 createSecretKey,
10 publicDecrypt,
11 publicEncrypt,
12 privateDecrypt,
13 privateEncrypt,
14 createPublicKey,
15 createPrivateKey,
16 getCiphers,
17 getCipherInfo,
18 createCipher,
19 createDecipher,
20 Cipher,
21 Decipher,
22} from 'node:crypto';
23 
24import { strictEqual, deepStrictEqual, throws, ok } from 'node:assert';
25 
26const tests = [
27 { name: 'aes-128-cbc', size: 16, iv: 16 },
28 { name: 'aes-192-cbc', size: 24, iv: 16 },
29 { name: 'aes-256-cbc', size: 32, iv: 16 },
30 { name: 'aes-128-ctr', size: 16, iv: 16 },
31 { name: 'aes-192-ctr', size: 24, iv: 16 },
32 { name: 'aes-256-ctr', size: 32, iv: 16 },
33 { name: 'aes-128-ecb', size: 16, iv: 0 },
34 { name: 'aes-192-ecb', size: 24, iv: 0 },
35 { name: 'aes-256-ecb', size: 32, iv: 0 },
36 { name: 'aes-128-ofb', size: 16, iv: 16 },
37 { name: 'aes-192-ofb', size: 24, iv: 16 },
38 { name: 'aes-256-ofb', size: 32, iv: 16 },
39];
40 
41const authTagTests = [
42 { name: 'aes-128-gcm', size: 16, iv: 16 },
43 { name: 'aes-192-gcm', size: 24, iv: 16 },
44 { name: 'aes-256-gcm', size: 32, iv: 16 },
45 { name: 'chacha20-poly1305', size: 32, iv: 12 },
46];
47 
48export const cipheriv = {
49 async test() {
50 tests.forEach((test) => {
51 const key = createSecretKey(Buffer.alloc(test.size));
52 const iv = Buffer.alloc(test.iv);
53 
54 const cipher = createCipheriv(test.name, key, iv);
55 const decipher = createDecipheriv(test.name, key, iv);
56 
57 let data = '';
58 data += decipher.update(cipher.update('Hello World', 'utf8'));
59 data += decipher.update(cipher.final());
60 data += decipher.final();
61 strictEqual(data, 'Hello World');
62 });
63 
64 // Test that the streams API works also
65 await Promise.all(
66 tests.map(async (test) => {
67 const { promise, resolve, reject } = Promise.withResolvers();
68 
69 const key = createSecretKey(Buffer.alloc(test.size));
70 const iv = Buffer.alloc(test.iv);
71 
72 const cipher = createCipheriv(test.name, key, iv);
73 const decipher = createDecipheriv(test.name, key, iv);
74 decipher.setEncoding('utf8');
75 
76 cipher.end('Hello World');
77 cipher.on('data', (chunk) => {
78 decipher.write(chunk);
79 });
80 cipher.on('end', () => {
81 decipher.end();
82 });
83 
84 cipher.on('error', reject);
85 decipher.on('error', reject);
86 
87 let res = '';
88 decipher.on('data', (chunk) => {
89 res += chunk;
90 });
91 decipher.on('end', resolve);
92 
93 await promise;
94 strictEqual(res, 'Hello World', test.name);
95 })
96 );
97 
98 authTagTests.forEach((test) => {
99 const key = createSecretKey(Buffer.alloc(test.size));
100 const iv = Buffer.alloc(test.iv);
101 
102 const cipher = createCipheriv(test.name, key, iv);
103 
104 let data = '';
105 cipher.setAAD(Buffer.from('hello'));
106 data += cipher.update('Hello World', 'utf8', 'hex');
107 data += cipher.final('hex');
108 
109 const tag = cipher.getAuthTag();
110 
111 //tag[1] = 0xbb;
112 
113 const decipher = createDecipheriv(test.name, key, iv);
114 decipher.setAuthTag(tag);
115 decipher.setAAD(Buffer.from('hello'));
116 let res = '';
117 res += decipher.update(data, 'hex');
118 res += decipher.final();
119 strictEqual(res, 'Hello World');
120 });
121 },
122};
123 
124export const largeData = {
125 async test() {
126 const { promise, resolve, reject } = Promise.withResolvers();
127 
128 const key = createSecretKey(Buffer.alloc(16));
129 const iv = Buffer.alloc(16);
130 const cipher = createCipheriv('aes-128-cbc', key, iv);
131 const chunks = [
132 randomBytes(1024),
133 randomBytes(2048),
134 randomBytes(4096),
135 randomBytes(8192),
136 randomBytes(16304),
137 ];
138 chunks.forEach((chunk) => cipher.write(chunk));
139 cipher.end();
140 
141 const decipher = createDecipheriv('aes-128-cbc', key, iv);
142 cipher.pipe(decipher);
143 
144 const output = [];
145 decipher.on('data', (chunk) => output.push(chunk));
146 
147 decipher.on('end', () => {
148 const inputCombined = Buffer.concat(chunks);
149 const outputCombined = Buffer.concat(output);
150 deepStrictEqual(inputCombined, outputCombined);
151 resolve();
152 });
153 
154 decipher.on('error', reject);
155 
156 await promise;
157 },
158};
159 
160export const publicEncryptPrivateDecrypt = {
161 test(_, env) {
162 const pub = createPublicKey(env['rsa_public.pem']);
163 const pvt = createPrivateKey(env['rsa_private.pem']);
164 
165 pub.oaepLabel = 'test';
166 pub.oaepHash = 'sha256';
167 pub.padding = 4;
168 pub.encoding = 'utf8';
169 
170 pvt.oaepLabel = 'test';
171 pvt.oaepHash = 'sha256';
172 pvt.padding = 4;
173 pvt.encoding = 'utf8';
174 
175 strictEqual(
176 privateDecrypt(pvt, publicEncrypt(pub, 'hello')).toString(),
177 'hello'
178 );
179 },
180};
181 
182export const publicEncryptPrivateDecryptDer = {
183 test(_, env) {
184 const pub = createPublicKey(env['rsa_public.pem']);
185 const pvt = createPrivateKey(env['rsa_private.pem']);
186 
187 const pubDer = {
188 key: pub.export({ type: 'pkcs1', format: 'der' }),
189 format: 'der',
190 type: 'pkcs1',
191 };
192 
193 const pvtDer = {
194 key: pvt.export({ type: 'pkcs8', format: 'der' }),
195 format: 'der',
196 type: 'pkcs8',
197 };
198 
199 strictEqual(
200 privateDecrypt(pvtDer, publicEncrypt(pubDer, 'hello')).toString(),
201 'hello'
202 );
203 },
204};
205 
206export const publicEncryptPrivateDecryptPem = {
207 test(_, env) {
208 const pub = createPublicKey(env['rsa_public.pem']);
209 const pvt = createPrivateKey(env['rsa_private.pem']);
210 
211 const pubPem = {
212 key: pub.export({ type: 'pkcs1', format: 'pem' }),
213 format: 'pem',
214 type: 'pkcs1',
215 };
216 
217 const pvtPem = {
218 key: pvt.export({ type: 'pkcs8', format: 'pem' }),
219 format: 'pem',
220 type: 'pkcs8',
221 };
222 
223 strictEqual(
224 privateDecrypt(pvtPem, publicEncrypt(pubPem, 'hello')).toString(),
225 'hello'
226 );
227 },
228};
229 
230export const publicEncryptPrivateDecryptPem2 = {
231 test(_, env) {
232 const pub = createPublicKey(env['rsa_public.pem']);
233 const pvt = createPrivateKey(env['rsa_private.pem']);
234 
235 const pubPem = pub.export({ type: 'pkcs1', format: 'pem' });
236 const pvtPem = pvt.export({ type: 'pkcs8', format: 'pem' });
237 
238 strictEqual(
239 privateDecrypt(pvtPem, publicEncrypt(pubPem, 'hello')).toString(),
240 'hello'
241 );
242 },
243};
244 
245export const publicEncryptPrivateDecryptPem3 = {
246 test(_, env) {
247 const pub = createPublicKey(env['rsa_public.pem']);
248 const pvt = createPrivateKey(env['rsa_private.pem']);
249 
250 const pubPem = pub.export({ type: 'spki', format: 'pem' });
251 const pvtPem = pvt.export({ type: 'pkcs8', format: 'pem' });
252 
253 strictEqual(
254 privateDecrypt(pvtPem, publicEncrypt(pubPem, 'hello')).toString(),
255 'hello'
256 );
257 },
258};
259 
260export const privateEncryptPublicDecrypt = {
261 test(_, env) {
262 const pub = createPublicKey(env['rsa_public.pem']);
263 const pvt = createPrivateKey(env['rsa_private.pem']);
264 
265 pub.oaepLabel = 'test';
266 pub.oaepHash = 'sha256';
267 pub.padding = 3;
268 pub.encoding = 'utf8';
269 
270 pvt.oaepLabel = 'test';
271 pvt.oaepHash = 'sha256';
272 pvt.padding = 3;
273 pvt.encoding = 'utf8';
274 
275 const input = 'a'.repeat(256);
276 strictEqual(
277 publicDecrypt(pub, privateEncrypt(pvt, input)).toString(),
278 input
279 );
280 },
281};
282 
283export const missingArgChecks = {
284 test() {
285 throws(() => publicEncrypt(), {
286 code: 'ERR_MISSING_ARGS',
287 });
288 throws(() => publicDecrypt(), {
289 code: 'ERR_MISSING_ARGS',
290 });
291 throws(() => privateEncrypt(), {
292 code: 'ERR_MISSING_ARGS',
293 });
294 throws(() => privateDecrypt(), {
295 code: 'ERR_MISSING_ARGS',
296 });
297 throws(() => publicEncrypt('key'), {
298 code: 'ERR_MISSING_ARGS',
299 });
300 throws(() => publicDecrypt('key'), {
301 code: 'ERR_MISSING_ARGS',
302 });
303 throws(() => privateEncrypt('key'), {
304 code: 'ERR_MISSING_ARGS',
305 });
306 throws(() => privateDecrypt('key'), {
307 code: 'ERR_MISSING_ARGS',
308 });
309 },
310};
311 
312export const known_ciphers_info = {
313 test() {
314 const ciphers = getCiphers();
315 ciphers.forEach((i) => {
316 const info = getCipherInfo(i);
317 ok(info);
318 strictEqual(typeof info, 'object');
319 strictEqual(typeof info.name, 'string');
320 strictEqual(typeof info.nid, 'number');
321 strictEqual(typeof info.blockSize, 'number');
322 strictEqual(typeof info.ivLength, 'number');
323 strictEqual(typeof info.keyLength, 'number');
324 strictEqual(typeof info.mode, 'string');
325 });
326 },
327};
328 
329export const test_cipher_info = {
330 test() {
331 const info = getCipherInfo('aes-128-cbc', {
332 ivLength: 16,
333 keyLength: 16,
334 });
335 ok(info);
336 strictEqual(typeof info, 'object');
337 strictEqual(typeof info.name, 'string');
338 strictEqual(typeof info.nid, 'number');
339 strictEqual(typeof info.blockSize, 'number');
340 strictEqual(typeof info.ivLength, 'number');
341 strictEqual(typeof info.keyLength, 'number');
342 strictEqual(typeof info.mode, 'string');
343 },
344};
345 
346export const test_cipher_info2 = {
347 test() {
348 const info = getCipherInfo('aes-128-cbc', {
349 ivLength: 17,
350 keyLength: 15,
351 });
352 strictEqual(info, undefined);
353 },
354};
355 
356export const test_cipher_info3 = {
357 test() {
358 const info = getCipherInfo('aes-128-cbc', {
359 ivLength: -1,
360 keyLength: 16,
361 });
362 strictEqual(info, undefined);
363 },
364};
365 
366export const test_cipher_info4 = {
367 test() {
368 const info = getCipherInfo('aes-128-cbc', {
369 ivLength: 16,
370 keyLength: -1,
371 });
372 strictEqual(info, undefined);
373 },
374};
375 
376// Verify that modifying a buffer after passing it to a cipher API does not
377// affect the cipher output. Buffer data (IV, AAD, auth tag) is copied into
378// C++-owned memory at the API boundary, matching Node.js behavior. These tests
379// compare output against a reference encryption performed with unmodified buffers.
380 
381// Helper: encrypt with chacha20-poly1305 (AEAD path) and return {ciphertext, tag}.
382function chachaEncrypt(key, iv, plaintext, aad) {
383 const cipher = createCipheriv('chacha20-poly1305', key, iv);
384 if (aad) cipher.setAAD(aad);
385 const ct = cipher.update(plaintext);
386 cipher.final();
387 return { ciphertext: ct, tag: cipher.getAuthTag() };
388}
389 
390// Helper: encrypt with aes-256-gcm (CipherHandle path) and return {ciphertext, tag}.
391function gcmEncrypt(key, iv, plaintext) {
392 const cipher = createCipheriv('aes-256-gcm', key, iv);
393 const ct = cipher.update(plaintext);
394 cipher.final();
395 return { ciphertext: ct, tag: cipher.getAuthTag() };
396}
397 
398export const modifiedIvChaCha20 = {
399 test() {
400 // ChaCha20-Poly1305 uses the AEAD (EVP_AEAD) path.
401 const plainKey = Buffer.alloc(32, 0x01);
402 const plainIv = Buffer.alloc(12, 0x42);
403 const key = createSecretKey(plainKey);
404 const plaintext = Buffer.from('hello world');
405 
406 // Reference encryption with plain buffers.
407 const ref = chachaEncrypt(key, plainIv, plaintext);
408 
409 // Modify IV buffer after cipher creation -- must not affect output.
410 const ivBuffer = new ArrayBuffer(12, { maxByteLength: 16 });
411 new Uint8Array(ivBuffer).fill(0x42);
412 const cipher = createCipheriv(
413 'chacha20-poly1305',
414 key,
415 new Uint8Array(ivBuffer)
416 );
417 ivBuffer.resize(0);
418 const ct = cipher.update(plaintext);
419 cipher.final();
420 
421 deepStrictEqual(ct, ref.ciphertext);
422 deepStrictEqual(cipher.getAuthTag(), ref.tag);
423 },
424};
425 
426export const modifiedIvAesGcm = {
427 test() {
428 // AES-256-GCM uses the CipherHandle (EVP_CIPHER) path.
429 const plainKey = Buffer.alloc(32, 0x01);
430 const plainIv = Buffer.alloc(16, 0x42);
431 const key = createSecretKey(plainKey);
432 const plaintext = Buffer.from('hello world');
433 
434 const ref = gcmEncrypt(key, plainIv, plaintext);
435 
436 const ivBuffer = new ArrayBuffer(16, { maxByteLength: 32 });
437 new Uint8Array(ivBuffer).fill(0x42);
438 const cipher = createCipheriv('aes-256-gcm', key, new Uint8Array(ivBuffer));
439 ivBuffer.resize(0);
440 const ct = cipher.update(plaintext);
441 cipher.final();
442 
443 deepStrictEqual(ct, ref.ciphertext);
444 deepStrictEqual(cipher.getAuthTag(), ref.tag);
445 },
446};
447 
448export const modifiedIvGrowChaCha20 = {
449 test() {
450 // Growing the IV buffer after cipher creation must not change the output.
451 const plainKey = Buffer.alloc(32, 0x01);
452 const plainIv = Buffer.alloc(12, 0x42);
453 const key = createSecretKey(plainKey);
454 const plaintext = Buffer.from('hello world');
455 
456 const ref = chachaEncrypt(key, plainIv, plaintext);
457 
458 const ivBuffer = new ArrayBuffer(12, { maxByteLength: 32 });
459 new Uint8Array(ivBuffer).fill(0x42);
460 const cipher = createCipheriv(
461 'chacha20-poly1305',
462 key,
463 new Uint8Array(ivBuffer)
464 );
465 ivBuffer.resize(32); // grow past original IV length
466 const ct = cipher.update(plaintext);
467 cipher.final();
468 
469 deepStrictEqual(ct, ref.ciphertext);
470 deepStrictEqual(cipher.getAuthTag(), ref.tag);
471 },
472};
473 
474export const modifiedAadChaCha20 = {
475 test() {
476 // Modifying AAD buffer after setAAD must not change the cipher output.
477 const plainKey = Buffer.alloc(32, 0x01);
478 const plainIv = Buffer.alloc(12, 0x42);
479 const plainAad = Buffer.alloc(16, 0xaa);
480 const key = createSecretKey(plainKey);
481 const plaintext = Buffer.from('hello world');
482 
483 const ref = chachaEncrypt(key, plainIv, plaintext, plainAad);
484 
485 const aadBuffer = new ArrayBuffer(16, { maxByteLength: 32 });
486 new Uint8Array(aadBuffer).fill(0xaa);
487 const cipher = createCipheriv('chacha20-poly1305', key, plainIv);
488 cipher.setAAD(new Uint8Array(aadBuffer));
489 aadBuffer.resize(0);
490 const ct = cipher.update(plaintext);
491 cipher.final();
492 
493 deepStrictEqual(ct, ref.ciphertext);
494 deepStrictEqual(cipher.getAuthTag(), ref.tag);
495 },
496};
497 
498export const modifiedAuthTagDecrypt = {
499 test() {
500 // Modifying auth tag buffer after setAuthTag must still allow correct decryption.
501 const plainKey = Buffer.alloc(32, 0x01);
502 const plainIv = Buffer.alloc(12, 0x42);
503 const key = createSecretKey(plainKey);
504 const plaintext = Buffer.from('hello world');
505 
506 const { ciphertext, tag } = chachaEncrypt(key, plainIv, plaintext);
507 
508 // Copy tag into a buffer, then modify it after setAuthTag.
509 const tagBuffer = new ArrayBuffer(tag.length, { maxByteLength: 32 });
510 new Uint8Array(tagBuffer).set(tag);
511 
512 const decipher = createDecipheriv('chacha20-poly1305', key, plainIv);
513 decipher.setAuthTag(new Uint8Array(tagBuffer));
514 tagBuffer.resize(0);
515 
516 const pt = decipher.update(ciphertext);
517 decipher.final();
518 deepStrictEqual(pt, plaintext);
519 },
520};
521 
522export const transferredIvChaCha20 = {
523 test() {
524 // Transferring IV buffer after cipher creation must not affect output.
525 const plainKey = Buffer.alloc(32, 0x01);
526 const plainIv = Buffer.alloc(12, 0x42);
527 const key = createSecretKey(plainKey);
528 const plaintext = Buffer.from('hello world');
529 
530 const ref = chachaEncrypt(key, plainIv, plaintext);
531 
532 const ivBuffer = new ArrayBuffer(12);
533 new Uint8Array(ivBuffer).fill(0x42);
534 const cipher = createCipheriv(
535 'chacha20-poly1305',
536 key,
537 new Uint8Array(ivBuffer)
538 );
539 structuredClone(ivBuffer, { transfer: [ivBuffer] });
540 const ct = cipher.update(plaintext);
541 cipher.final();
542 
543 deepStrictEqual(ct, ref.ciphertext);
544 deepStrictEqual(cipher.getAuthTag(), ref.tag);
545 },
546};
547 
548export const transferredIvAesGcm = {
549 test() {
550 const plainKey = Buffer.alloc(32, 0x01);
551 const plainIv = Buffer.alloc(16, 0x42);
552 const key = createSecretKey(plainKey);
553 const plaintext = Buffer.from('hello world');
554 
555 const ref = gcmEncrypt(key, plainIv, plaintext);
556 
557 const ivBuffer = new ArrayBuffer(16);
558 new Uint8Array(ivBuffer).fill(0x42);
559 const cipher = createCipheriv('aes-256-gcm', key, new Uint8Array(ivBuffer));
560 structuredClone(ivBuffer, { transfer: [ivBuffer] });
561 const ct = cipher.update(plaintext);
562 cipher.final();
563 
564 deepStrictEqual(ct, ref.ciphertext);
565 deepStrictEqual(cipher.getAuthTag(), ref.tag);
566 },
567};
568 
569export const transferredAadChaCha20 = {
570 test() {
571 const plainKey = Buffer.alloc(32, 0x01);
572 const plainIv = Buffer.alloc(12, 0x42);
573 const plainAad = Buffer.alloc(16, 0xaa);
574 const key = createSecretKey(plainKey);
575 const plaintext = Buffer.from('hello world');
576 
577 const ref = chachaEncrypt(key, plainIv, plaintext, plainAad);
578 
579 const aadBuffer = new ArrayBuffer(16);
580 new Uint8Array(aadBuffer).fill(0xaa);
581 const cipher = createCipheriv('chacha20-poly1305', key, plainIv);
582 cipher.setAAD(new Uint8Array(aadBuffer));
583 structuredClone(aadBuffer, { transfer: [aadBuffer] });
584 const ct = cipher.update(plaintext);
585 cipher.final();
586 
587 deepStrictEqual(ct, ref.ciphertext);
588 deepStrictEqual(cipher.getAuthTag(), ref.tag);
589 },
590};
591 
592export const transferredAuthTagDecrypt = {
593 test() {
594 const plainKey = Buffer.alloc(32, 0x01);
595 const plainIv = Buffer.alloc(12, 0x42);
596 const key = createSecretKey(plainKey);
597 const plaintext = Buffer.from('hello world');
598 
599 const { ciphertext, tag } = chachaEncrypt(key, plainIv, plaintext);
600 
601 const tagBuffer = new ArrayBuffer(tag.length);
602 new Uint8Array(tagBuffer).set(tag);
603 
604 const decipher = createDecipheriv('chacha20-poly1305', key, plainIv);
605 decipher.setAuthTag(new Uint8Array(tagBuffer));
606 structuredClone(tagBuffer, { transfer: [tagBuffer] });
607 
608 const pt = decipher.update(ciphertext);
609 decipher.final();
610 deepStrictEqual(pt, plaintext);
611 },
612};
613 
614export const testUnimplemented = {
615 async test() {
616 strictEqual(typeof Cipher, 'function');
617 strictEqual(typeof Decipher, 'function');
618 strictEqual(typeof createCipher, 'function');
619 strictEqual(typeof createDecipher, 'function');
620 },
621};