1// Copyright Joyent, Inc. and other Node contributors. 2// 3// Permission is hereby granted, free of charge, to any person obtaining a 4// copy of this software and associated documentation files (the 5// "Software"), to deal in the Software without restriction, including 6// without limitation the rights to use, copy, modify, merge, publish, 7// distribute, sublicense, and/or sell copies of the Software, and to permit 8// persons to whom the Software is furnished to do so, subject to the 9// following conditions: 10// 11// The above copyright notice and this permission notice shall be included 12// in all copies or substantial portions of the Software. 13// 14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 17// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 20// USE OR OTHER DEALINGS IN THE SOFTWARE. 21// Flags: --no-warnings 22'use strict'; 23const common = require('../common'); 24if (!common.hasCrypto) 25 common.skip('missing crypto'); 26 27const assert = require('assert'); 28const crypto = require('crypto'); 29const { inspect } = require('util'); 30const fixtures = require('../common/fixtures'); 31 32crypto.DEFAULT_ENCODING = 'buffer'; 33 34// 35// Test authenticated encryption modes. 36// 37// !NEVER USE STATIC IVs IN REAL LIFE! 38// 39 40const TEST_CASES = require(fixtures.path('aead-vectors.js')); 41 42const errMessages = { 43 auth: / auth/, 44 state: / state/, 45 FIPS: /not supported in FIPS mode/, 46 length: /Invalid initialization vector/, 47 authTagLength: /Invalid authentication tag length/ 48}; 49 50const ciphers = crypto.getCiphers(); 51 52const expectedWarnings = common.hasFipsCrypto ? 53 [] : [ 54 ['Use Cipheriv for counter mode of aes-192-gcm'], 55 ['Use Cipheriv for counter mode of aes-192-ccm'], 56 ['Use Cipheriv for counter mode of aes-192-ccm'], 57 ['Use Cipheriv for counter mode of aes-128-ccm'], 58 ['Use Cipheriv for counter mode of aes-128-ccm'], 59 ['Use Cipheriv for counter mode of aes-128-ccm'], 60 ['Use Cipheriv for counter mode of aes-256-ccm'], 61 ['Use Cipheriv for counter mode of aes-256-ccm'], 62 ['Use Cipheriv for counter mode of aes-256-ccm'], 63 ['Use Cipheriv for counter mode of aes-256-ccm'], 64 ['Use Cipheriv for counter mode of aes-256-ccm'], 65 ['Use Cipheriv for counter mode of aes-256-ccm'], 66 ['Use Cipheriv for counter mode of aes-256-ccm'], 67 ['Use Cipheriv for counter mode of aes-256-ccm'], 68 ['Use Cipheriv for counter mode of aes-256-ccm'], 69 ['Use Cipheriv for counter mode of aes-256-ccm'], 70 ['Use Cipheriv for counter mode of aes-256-ccm'], 71 ['Use Cipheriv for counter mode of aes-256-ccm'], 72 ['Use Cipheriv for counter mode of aes-256-ccm'], 73 ['Use Cipheriv for counter mode of aes-128-ccm'], 74 ]; 75 76const expectedDeprecationWarnings = [ 77 ['crypto.DEFAULT_ENCODING is deprecated.', 'DEP0091'], 78 ['crypto.createCipher is deprecated.', 'DEP0106'], 79]; 80 81common.expectWarning({ 82 Warning: expectedWarnings, 83 DeprecationWarning: expectedDeprecationWarnings 84}); 85 86for (const test of TEST_CASES) { 87 if (!ciphers.includes(test.algo)) { 88 common.printSkipMessage(`unsupported ${test.algo} test`); 89 continue; 90 } 91 92 if (common.hasFipsCrypto && test.iv.length < 24) { 93 common.printSkipMessage('IV len < 12 bytes unsupported in FIPS mode'); 94 continue; 95 } 96 97 const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo); 98 const isOCB = /^aes-(128|192|256)-ocb$/.test(test.algo); 99 100 let options; 101 if (isCCM || isOCB) 102 options = { authTagLength: test.tag.length / 2 }; 103 104 const inputEncoding = test.plainIsHex ? 'hex' : 'ascii'; 105 106 let aadOptions; 107 if (isCCM) { 108 aadOptions = { 109 plaintextLength: Buffer.from(test.plain, inputEncoding).length 110 }; 111 } 112 113 { 114 const encrypt = crypto.createCipheriv(test.algo, 115 Buffer.from(test.key, 'hex'), 116 Buffer.from(test.iv, 'hex'), 117 options); 118 119 if (test.aad) 120 encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); 121 122 let hex = encrypt.update(test.plain, inputEncoding, 'hex'); 123 hex += encrypt.final('hex'); 124 125 const auth_tag = encrypt.getAuthTag(); 126 // Only test basic encryption run if output is marked as tampered. 127 if (!test.tampered) { 128 assert.strictEqual(hex, test.ct); 129 assert.strictEqual(auth_tag.toString('hex'), test.tag); 130 } 131 } 132 133 { 134 if (isCCM && common.hasFipsCrypto) { 135 assert.throws(() => { 136 crypto.createDecipheriv(test.algo, 137 Buffer.from(test.key, 'hex'), 138 Buffer.from(test.iv, 'hex'), 139 options); 140 }, errMessages.FIPS); 141 } else { 142 const decrypt = crypto.createDecipheriv(test.algo, 143 Buffer.from(test.key, 'hex'), 144 Buffer.from(test.iv, 'hex'), 145 options); 146 decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); 147 if (test.aad) 148 decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); 149 150 const outputEncoding = test.plainIsHex ? 'hex' : 'ascii'; 151 152 let msg = decrypt.update(test.ct, 'hex', outputEncoding); 153 if (!test.tampered) { 154 msg += decrypt.final(outputEncoding); 155 assert.strictEqual(msg, test.plain); 156 } else { 157 // Assert that final throws if input data could not be verified! 158 assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); 159 } 160 } 161 } 162 163 if (test.password) { 164 if (common.hasFipsCrypto) { 165 assert.throws(() => { crypto.createCipher(test.algo, test.password); }, 166 errMessages.FIPS); 167 } else { 168 const encrypt = crypto.createCipher(test.algo, test.password, options); 169 if (test.aad) 170 encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); 171 let hex = encrypt.update(test.plain, 'ascii', 'hex'); 172 hex += encrypt.final('hex'); 173 const auth_tag = encrypt.getAuthTag(); 174 // Only test basic encryption run if output is marked as tampered. 175 if (!test.tampered) { 176 assert.strictEqual(hex, test.ct); 177 assert.strictEqual(auth_tag.toString('hex'), test.tag); 178 } 179 } 180 } 181 182 if (test.password) { 183 if (common.hasFipsCrypto) { 184 assert.throws(() => { crypto.createDecipher(test.algo, test.password); }, 185 errMessages.FIPS); 186 } else { 187 const decrypt = crypto.createDecipher(test.algo, test.password, options); 188 decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); 189 if (test.aad) 190 decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); 191 let msg = decrypt.update(test.ct, 'hex', 'ascii'); 192 if (!test.tampered) { 193 msg += decrypt.final('ascii'); 194 assert.strictEqual(msg, test.plain); 195 } else { 196 // Assert that final throws if input data could not be verified! 197 assert.throws(function() { decrypt.final('ascii'); }, errMessages.auth); 198 } 199 } 200 } 201 202 { 203 // Trying to get tag before inputting all data: 204 const encrypt = crypto.createCipheriv(test.algo, 205 Buffer.from(test.key, 'hex'), 206 Buffer.from(test.iv, 'hex'), 207 options); 208 encrypt.update('blah', 'ascii'); 209 assert.throws(function() { encrypt.getAuthTag(); }, errMessages.state); 210 } 211 212 { 213 // Trying to create cipher with incorrect IV length 214 assert.throws(function() { 215 crypto.createCipheriv( 216 test.algo, 217 Buffer.from(test.key, 'hex'), 218 Buffer.alloc(0) 219 ); 220 }, errMessages.length); 221 } 222} 223 224// Non-authenticating mode: 225{ 226 const encrypt = 227 crypto.createCipheriv('aes-128-cbc', 228 'ipxp9a6i1Mb4USb4', 229 '6fKjEjR3Vl30EUYC'); 230 encrypt.update('blah', 'ascii'); 231 encrypt.final(); 232 assert.throws(() => encrypt.getAuthTag(), errMessages.state); 233 assert.throws(() => encrypt.setAAD(Buffer.from('123', 'ascii')), 234 errMessages.state); 235} 236 237// GCM only supports specific authentication tag lengths, invalid lengths should 238// throw. 239{ 240 for (const length of [0, 1, 2, 6, 9, 10, 11, 17]) { 241 assert.throws(() => { 242 const decrypt = crypto.createDecipheriv('aes-128-gcm', 243 'FxLKsqdmv0E9xrQh', 244 'qkuZpJWCewa6Szih'); 245 decrypt.setAuthTag(Buffer.from('1'.repeat(length))); 246 }, { 247 name: 'TypeError', 248 message: /Invalid authentication tag length/ 249 }); 250 251 assert.throws(() => { 252 crypto.createCipheriv('aes-256-gcm', 253 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 254 'qkuZpJWCewa6Szih', 255 { 256 authTagLength: length 257 }); 258 }, { 259 name: 'TypeError', 260 message: /Invalid authentication tag length/ 261 }); 262 263 assert.throws(() => { 264 crypto.createDecipheriv('aes-256-gcm', 265 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 266 'qkuZpJWCewa6Szih', 267 { 268 authTagLength: length 269 }); 270 }, { 271 name: 'TypeError', 272 message: /Invalid authentication tag length/ 273 }); 274 } 275} 276 277// Test that GCM can produce shorter authentication tags than 16 bytes. 278{ 279 const fullTag = '1debb47b2c91ba2cea16fad021703070'; 280 for (const [authTagLength, e] of [[undefined, 16], [12, 12], [4, 4]]) { 281 const cipher = crypto.createCipheriv('aes-256-gcm', 282 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 283 'qkuZpJWCewa6Szih', { 284 authTagLength 285 }); 286 cipher.setAAD(Buffer.from('abcd')); 287 cipher.update('01234567', 'hex'); 288 cipher.final(); 289 const tag = cipher.getAuthTag(); 290 assert.strictEqual(tag.toString('hex'), fullTag.substr(0, 2 * e)); 291 } 292} 293 294// Test that users can manually restrict the GCM tag length to a single value. 295{ 296 const decipher = crypto.createDecipheriv('aes-256-gcm', 297 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 298 'qkuZpJWCewa6Szih', { 299 authTagLength: 8 300 }); 301 302 assert.throws(() => { 303 // This tag would normally be allowed. 304 decipher.setAuthTag(Buffer.from('1'.repeat(12))); 305 }, { 306 name: 'TypeError', 307 message: /Invalid authentication tag length/ 308 }); 309 310 // The Decipher object should be left intact. 311 decipher.setAuthTag(Buffer.from('445352d3ff85cf94', 'hex')); 312 const text = Buffer.concat([ 313 decipher.update('3a2a3647', 'hex'), 314 decipher.final(), 315 ]); 316 assert.strictEqual(text.toString('utf8'), 'node'); 317} 318 319// Test that create(De|C)ipher(iv)? throws if the mode is CCM and an invalid 320// authentication tag length has been specified. 321{ 322 for (const authTagLength of [-1, true, false, NaN, 5.5]) { 323 assert.throws(() => { 324 crypto.createCipheriv('aes-256-ccm', 325 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 326 'qkuZpJWCewa6S', 327 { 328 authTagLength 329 }); 330 }, { 331 name: 'TypeError', 332 code: 'ERR_INVALID_ARG_VALUE', 333 message: "The property 'options.authTagLength' is invalid. " + 334 `Received ${inspect(authTagLength)}` 335 }); 336 337 assert.throws(() => { 338 crypto.createDecipheriv('aes-256-ccm', 339 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 340 'qkuZpJWCewa6S', 341 { 342 authTagLength 343 }); 344 }, { 345 name: 'TypeError', 346 code: 'ERR_INVALID_ARG_VALUE', 347 message: "The property 'options.authTagLength' is invalid. " + 348 `Received ${inspect(authTagLength)}` 349 }); 350 351 if (!common.hasFipsCrypto) { 352 assert.throws(() => { 353 crypto.createCipher('aes-256-ccm', 'bad password', { authTagLength }); 354 }, { 355 name: 'TypeError', 356 code: 'ERR_INVALID_ARG_VALUE', 357 message: "The property 'options.authTagLength' is invalid. " + 358 `Received ${inspect(authTagLength)}` 359 }); 360 361 assert.throws(() => { 362 crypto.createDecipher('aes-256-ccm', 'bad password', { authTagLength }); 363 }, { 364 name: 'TypeError', 365 code: 'ERR_INVALID_ARG_VALUE', 366 message: "The property 'options.authTagLength' is invalid. " + 367 `Received ${inspect(authTagLength)}` 368 }); 369 } 370 } 371 372 // The following values will not be caught by the JS layer and thus will not 373 // use the default error codes. 374 for (const authTagLength of [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 18]) { 375 assert.throws(() => { 376 crypto.createCipheriv('aes-256-ccm', 377 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 378 'qkuZpJWCewa6S', 379 { 380 authTagLength 381 }); 382 }, errMessages.authTagLength); 383 384 if (!common.hasFipsCrypto) { 385 assert.throws(() => { 386 crypto.createDecipheriv('aes-256-ccm', 387 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 388 'qkuZpJWCewa6S', 389 { 390 authTagLength 391 }); 392 }, errMessages.authTagLength); 393 394 assert.throws(() => { 395 crypto.createCipher('aes-256-ccm', 'bad password', { authTagLength }); 396 }, errMessages.authTagLength); 397 398 assert.throws(() => { 399 crypto.createDecipher('aes-256-ccm', 'bad password', { authTagLength }); 400 }, errMessages.authTagLength); 401 } 402 } 403} 404 405// Test that create(De|C)ipher(iv)? throws if the mode is CCM or OCB and no 406// authentication tag has been specified. 407{ 408 for (const mode of ['ccm', 'ocb']) { 409 assert.throws(() => { 410 crypto.createCipheriv(`aes-256-${mode}`, 411 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 412 'qkuZpJWCewa6S'); 413 }, { 414 message: `authTagLength required for aes-256-${mode}` 415 }); 416 417 // CCM decryption and create(De|C)ipher are unsupported in FIPS mode. 418 if (!common.hasFipsCrypto) { 419 assert.throws(() => { 420 crypto.createDecipheriv(`aes-256-${mode}`, 421 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 422 'qkuZpJWCewa6S'); 423 }, { 424 message: `authTagLength required for aes-256-${mode}` 425 }); 426 427 assert.throws(() => { 428 crypto.createCipher(`aes-256-${mode}`, 'very bad password'); 429 }, { 430 message: `authTagLength required for aes-256-${mode}` 431 }); 432 433 assert.throws(() => { 434 crypto.createDecipher(`aes-256-${mode}`, 'very bad password'); 435 }, { 436 message: `authTagLength required for aes-256-${mode}` 437 }); 438 } 439 } 440} 441 442// Test that setAAD throws if an invalid plaintext length has been specified. 443{ 444 const cipher = crypto.createCipheriv('aes-256-ccm', 445 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 446 'qkuZpJWCewa6S', 447 { 448 authTagLength: 10 449 }); 450 451 for (const plaintextLength of [-1, true, false, NaN, 5.5]) { 452 assert.throws(() => { 453 cipher.setAAD(Buffer.from('0123456789', 'hex'), { plaintextLength }); 454 }, { 455 name: 'TypeError', 456 code: 'ERR_INVALID_ARG_VALUE', 457 message: "The property 'options.plaintextLength' is invalid. " + 458 `Received ${inspect(plaintextLength)}` 459 }); 460 } 461} 462 463// Test that setAAD and update throw if the plaintext is too long. 464{ 465 for (const ivLength of [13, 12]) { 466 const maxMessageSize = (1 << (8 * (15 - ivLength))) - 1; 467 const key = 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8'; 468 const cipher = () => crypto.createCipheriv('aes-256-ccm', key, 469 '0'.repeat(ivLength), 470 { 471 authTagLength: 10 472 }); 473 474 assert.throws(() => { 475 cipher().setAAD(Buffer.alloc(0), { 476 plaintextLength: maxMessageSize + 1 477 }); 478 }, /Invalid message length$/); 479 480 const msg = Buffer.alloc(maxMessageSize + 1); 481 assert.throws(() => { 482 cipher().update(msg); 483 }, /Invalid message length/); 484 485 const c = cipher(); 486 c.setAAD(Buffer.alloc(0), { 487 plaintextLength: maxMessageSize 488 }); 489 c.update(msg.slice(1)); 490 } 491} 492 493// Test that setAAD throws if the mode is CCM and the plaintext length has not 494// been specified. 495{ 496 assert.throws(() => { 497 const cipher = crypto.createCipheriv('aes-256-ccm', 498 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 499 'qkuZpJWCewa6S', 500 { 501 authTagLength: 10 502 }); 503 cipher.setAAD(Buffer.from('0123456789', 'hex')); 504 }, /options\.plaintextLength required for CCM mode with AAD/); 505 506 if (!common.hasFipsCrypto) { 507 assert.throws(() => { 508 const cipher = crypto.createDecipheriv('aes-256-ccm', 509 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', 510 'qkuZpJWCewa6S', 511 { 512 authTagLength: 10 513 }); 514 cipher.setAAD(Buffer.from('0123456789', 'hex')); 515 }, /options\.plaintextLength required for CCM mode with AAD/); 516 } 517} 518 519// Test that final() throws in CCM mode when no authentication tag is provided. 520{ 521 if (!common.hasFipsCrypto) { 522 const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); 523 const iv = Buffer.from('7305220bca40d4c90e1791e9', 'hex'); 524 const ct = Buffer.from('8beba09d4d4d861f957d51c0794f4abf8030848e', 'hex'); 525 const decrypt = crypto.createDecipheriv('aes-128-ccm', key, iv, { 526 authTagLength: 10 527 }); 528 // Normally, we would do this: 529 // decrypt.setAuthTag(Buffer.from('0d9bcd142a94caf3d1dd', 'hex')); 530 assert.throws(() => { 531 decrypt.setAAD(Buffer.from('63616c76696e', 'hex'), { 532 plaintextLength: ct.length 533 }); 534 decrypt.update(ct); 535 decrypt.final(); 536 }, errMessages.state); 537 } 538} 539 540// Test that setAuthTag does not throw in GCM mode when called after setAAD. 541{ 542 const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); 543 const iv = Buffer.from('579d9dfde9cd93d743da1ceaeebb86e4', 'hex'); 544 const decrypt = crypto.createDecipheriv('aes-128-gcm', key, iv); 545 decrypt.setAAD(Buffer.from('0123456789', 'hex')); 546 decrypt.setAuthTag(Buffer.from('1bb9253e250b8069cde97151d7ef32d9', 'hex')); 547 assert.strictEqual(decrypt.update('807022', 'hex', 'hex'), 'abcdef'); 548 assert.strictEqual(decrypt.final('hex'), ''); 549} 550 551// Test that an IV length of 11 does not overflow max_message_size_. 552{ 553 const key = 'x'.repeat(16); 554 const iv = Buffer.from('112233445566778899aabb', 'hex'); 555 const options = { authTagLength: 8 }; 556 const encrypt = crypto.createCipheriv('aes-128-ccm', key, iv, options); 557 encrypt.update('boom'); // Should not throw 'Message exceeds maximum size'. 558 encrypt.final(); 559} 560 561// Test that the authentication tag can be set at any point before calling 562// final() in GCM or OCB mode. 563{ 564 const plain = Buffer.from('Hello world', 'utf8'); 565 const key = Buffer.from('0123456789abcdef', 'utf8'); 566 const iv = Buffer.from('0123456789ab', 'utf8'); 567 568 for (const mode of ['gcm', 'ocb']) { 569 for (const authTagLength of mode === 'gcm' ? [undefined, 8] : [8]) { 570 const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, { 571 authTagLength 572 }); 573 const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); 574 const authTag = cipher.getAuthTag(); 575 576 for (const authTagBeforeUpdate of [true, false]) { 577 const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, { 578 authTagLength 579 }); 580 if (authTagBeforeUpdate) { 581 decipher.setAuthTag(authTag); 582 } 583 const resultUpdate = decipher.update(ciphertext); 584 if (!authTagBeforeUpdate) { 585 decipher.setAuthTag(authTag); 586 } 587 const resultFinal = decipher.final(); 588 const result = Buffer.concat([resultUpdate, resultFinal]); 589 assert(result.equals(plain)); 590 } 591 } 592 } 593} 594 595// Test that setAuthTag can only be called once. 596{ 597 const plain = Buffer.from('Hello world', 'utf8'); 598 const key = Buffer.from('0123456789abcdef', 'utf8'); 599 const iv = Buffer.from('0123456789ab', 'utf8'); 600 const opts = { authTagLength: 8 }; 601 602 for (const mode of ['gcm', 'ccm', 'ocb']) { 603 const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, opts); 604 const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); 605 const tag = cipher.getAuthTag(); 606 607 const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, opts); 608 decipher.setAuthTag(tag); 609 assert.throws(() => { 610 decipher.setAuthTag(tag); 611 }, errMessages.state); 612 // Decryption should still work. 613 const plaintext = Buffer.concat([ 614 decipher.update(ciphertext), 615 decipher.final(), 616 ]); 617 assert(plain.equals(plaintext)); 618 } 619} 620 621 622// Test chacha20-poly1305 rejects invalid IV lengths of 13, 14, 15, and 16 (a 623// length of 17 or greater was already rejected). 624// - https://www.openssl.org/news/secadv/20190306.txt 625{ 626 // Valid extracted from TEST_CASES, check that it detects IV tampering. 627 const valid = { 628 algo: 'chacha20-poly1305', 629 key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', 630 iv: '070000004041424344454647', 631 plain: '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' + 632 '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' + 633 '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' + 634 '637265656e20776f756c642062652069742e', 635 plainIsHex: true, 636 aad: '50515253c0c1c2c3c4c5c6c7', 637 ct: 'd31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5' + 638 'a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e06' + 639 '0b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fa' + 640 'b324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d265' + 641 '86cec64b6116', 642 tag: '1ae10b594f09e26a7e902ecbd0600691', 643 tampered: false, 644 }; 645 646 // Invalid IV lengths should be detected: 647 // - 12 and below are valid. 648 // - 13-16 are not detected as invalid by some OpenSSL versions. 649 check(13); 650 check(14); 651 check(15); 652 check(16); 653 // - 17 and above were always detected as invalid by OpenSSL. 654 check(17); 655 656 function check(ivLength) { 657 const prefix = ivLength - valid.iv.length / 2; 658 assert.throws(() => crypto.createCipheriv( 659 valid.algo, 660 Buffer.from(valid.key, 'hex'), 661 Buffer.from(H(prefix) + valid.iv, 'hex') 662 ), errMessages.length, `iv length ${ivLength} was not rejected`); 663 664 function H(length) { return '00'.repeat(length); } 665 } 666} 667 668{ 669 // CCM cipher without data should not crash, see https://github.com/nodejs/node/issues/38035. 670 const algo = 'aes-128-ccm'; 671 const key = Buffer.alloc(16); 672 const iv = Buffer.alloc(12); 673 const opts = { authTagLength: 10 }; 674 675 for (const cipher of [ 676 crypto.createCipher(algo, 'foo', opts), 677 crypto.createCipheriv(algo, key, iv, opts), 678 ]) { 679 assert.throws(() => { 680 cipher.final(); 681 }, common.hasOpenSSL3 ? { 682 code: 'ERR_OSSL_TAG_NOT_SET' 683 } : { 684 message: /Unsupported state/ 685 }); 686 } 687} 688 689{ 690 const key = Buffer.alloc(32); 691 const iv = Buffer.alloc(12); 692 693 for (const authTagLength of [0, 17]) { 694 assert.throws(() => { 695 crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength }); 696 }, { 697 code: 'ERR_CRYPTO_INVALID_AUTH_TAG', 698 message: errMessages.authTagLength 699 }); 700 } 701} 702 703// ChaCha20-Poly1305 should respect the authTagLength option and should not 704// require the authentication tag before calls to update() during decryption. 705{ 706 const key = Buffer.alloc(32); 707 const iv = Buffer.alloc(12); 708 709 for (let authTagLength = 1; authTagLength <= 16; authTagLength++) { 710 const cipher = 711 crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength }); 712 const ciphertext = Buffer.concat([cipher.update('foo'), cipher.final()]); 713 const authTag = cipher.getAuthTag(); 714 assert.strictEqual(authTag.length, authTagLength); 715 716 // The decipher operation should reject all authentication tags other than 717 // that of the expected length. 718 for (let other = 1; other <= 16; other++) { 719 const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, { 720 authTagLength: other 721 }); 722 // ChaCha20 is a stream cipher so we do not need to call final() to obtain 723 // the full plaintext. 724 const plaintext = decipher.update(ciphertext); 725 assert.strictEqual(plaintext.toString(), 'foo'); 726 if (other === authTagLength) { 727 // The authentication tag length is as expected and the tag itself is 728 // correct, so this should work. 729 decipher.setAuthTag(authTag); 730 decipher.final(); 731 } else { 732 // The authentication tag that we are going to pass to setAuthTag is 733 // either too short or too long. If other < authTagLength, the 734 // authentication tag is still correct, but it should still be rejected 735 // because its security assurance is lower than expected. 736 assert.throws(() => { 737 decipher.setAuthTag(authTag); 738 }, { 739 code: 'ERR_CRYPTO_INVALID_AUTH_TAG', 740 message: `Invalid authentication tag length: ${authTagLength}` 741 }); 742 } 743 } 744 } 745} 746 747// ChaCha20-Poly1305 should default to an authTagLength of 16. When encrypting, 748// this matches the behavior of GCM ciphers. When decrypting, however, it is 749// stricter than GCM in that it only allows authentication tags that are exactly 750// 16 bytes long, whereas, when no authTagLength was specified, GCM would accept 751// shorter tags as long as their length was valid according to NIST SP 800-38D. 752// For ChaCha20-Poly1305, we intentionally deviate from that because there are 753// no recommended or approved authentication tag lengths below 16 bytes. 754{ 755 const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { 756 return algo === 'chacha20-poly1305' && tampered === false; 757 }); 758 assert.strictEqual(rfcTestCases.length, 1); 759 760 const [testCase] = rfcTestCases; 761 const key = Buffer.from(testCase.key, 'hex'); 762 const iv = Buffer.from(testCase.iv, 'hex'); 763 const aad = Buffer.from(testCase.aad, 'hex'); 764 765 for (const opt of [ 766 undefined, 767 { authTagLength: undefined }, 768 { authTagLength: 16 }, 769 ]) { 770 const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, opt); 771 const ciphertext = Buffer.concat([ 772 cipher.setAAD(aad).update(testCase.plain, 'hex'), 773 cipher.final(), 774 ]); 775 const authTag = cipher.getAuthTag(); 776 777 assert.strictEqual(ciphertext.toString('hex'), testCase.ct); 778 assert.strictEqual(authTag.toString('hex'), testCase.tag); 779 780 const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, opt); 781 const plaintext = Buffer.concat([ 782 decipher.setAAD(aad).update(ciphertext), 783 decipher.setAuthTag(authTag).final(), 784 ]); 785 786 assert.strictEqual(plaintext.toString('hex'), testCase.plain); 787 } 788} 789 790// https://github.com/nodejs/node/issues/45874 791{ 792 const rfcTestCases = TEST_CASES.filter(({ algo, tampered }) => { 793 return algo === 'chacha20-poly1305' && tampered === false; 794 }); 795 assert.strictEqual(rfcTestCases.length, 1); 796 797 const [testCase] = rfcTestCases; 798 const key = Buffer.from(testCase.key, 'hex'); 799 const iv = Buffer.from(testCase.iv, 'hex'); 800 const aad = Buffer.from(testCase.aad, 'hex'); 801 const opt = { authTagLength: 16 }; 802 803 const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, opt); 804 const ciphertext = Buffer.concat([ 805 cipher.setAAD(aad).update(testCase.plain, 'hex'), 806 cipher.final(), 807 ]); 808 const authTag = cipher.getAuthTag(); 809 810 assert.strictEqual(ciphertext.toString('hex'), testCase.ct); 811 assert.strictEqual(authTag.toString('hex'), testCase.tag); 812 813 const decipher = crypto.createDecipheriv('chacha20-poly1305', key, iv, opt); 814 decipher.setAAD(aad).update(ciphertext); 815 816 assert.throws(() => { 817 decipher.final(); 818 }, /Unsupported state or unable to authenticate data/); 819} 820