diff --git a/deps/ncrypto/ncrypto.cc b/deps/ncrypto/ncrypto.cc index 55395e31de..73af3b8c37 100644 --- a/deps/ncrypto/ncrypto.cc +++ b/deps/ncrypto/ncrypto.cc @@ -3041,6 +3041,9 @@ const Cipher Cipher::AES_256_GCM = Cipher::FromNid(NID_aes_256_gcm); const Cipher Cipher::AES_128_KW = Cipher::FromNid(NID_id_aes128_wrap); const Cipher Cipher::AES_192_KW = Cipher::FromNid(NID_id_aes192_wrap); const Cipher Cipher::AES_256_KW = Cipher::FromNid(NID_id_aes256_wrap); +const Cipher Cipher::AES_128_OCB = Cipher::FromNid(NID_aes_128_ocb); +const Cipher Cipher::AES_192_OCB = Cipher::FromNid(NID_aes_192_ocb); +const Cipher Cipher::AES_256_OCB = Cipher::FromNid(NID_aes_256_ocb); const Cipher Cipher::CHACHA20_POLY1305 = Cipher::FromNid(NID_chacha20_poly1305); bool Cipher::isGcmMode() const { @@ -3243,6 +3246,11 @@ bool CipherCtxPointer::isGcmMode() const { return getMode() == EVP_CIPH_GCM_MODE; } +bool CipherCtxPointer::isOcbMode() const { + if (!ctx_) return false; + return getMode() == EVP_CIPH_OCB_MODE; +} + bool CipherCtxPointer::isCcmMode() const { if (!ctx_) return false; return getMode() == EVP_CIPH_CCM_MODE; diff --git a/deps/ncrypto/ncrypto.h b/deps/ncrypto/ncrypto.h index 303de3cc9d..6c62aa28a5 100644 --- a/deps/ncrypto/ncrypto.h +++ b/deps/ncrypto/ncrypto.h @@ -373,6 +373,9 @@ class Cipher final { static const Cipher AES_128_KW; static const Cipher AES_192_KW; static const Cipher AES_256_KW; + static const Cipher AES_128_OCB; + static const Cipher AES_192_OCB; + static const Cipher AES_256_OCB; static const Cipher CHACHA20_POLY1305; struct CipherParams { @@ -738,6 +741,7 @@ class CipherCtxPointer final { int getNid() const; bool isGcmMode() const; + bool isOcbMode() const; bool isCcmMode() const; bool isWrapMode() const; bool isChaCha20Poly1305() const; diff --git a/doc/api/webcrypto.md b/doc/api/webcrypto.md index 565d7eb819..c8ad24668e 100644 --- a/doc/api/webcrypto.md +++ b/doc/api/webcrypto.md @@ -2,6 +2,9 @@ -* Type: {string} Must be `'AES-GCM'` or `'ChaCha20-Poly1305'`. +* Type: {string} Must be `'AES-GCM'`, `'AES-OCB'`, or `'ChaCha20-Poly1305'`. #### `aeadParams.tagLength` @@ -1515,8 +1542,7 @@ added: v15.0.0 added: v15.0.0 --> -* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, or - `'AES-KW'` +* Type: {string} Must be one of `'AES-CBC'`, `'AES-CTR'`, `'AES-GCM'`, `'AES-OCB'`, or `'AES-KW'` #### `aesDerivedKeyParams.length` @@ -2392,6 +2418,8 @@ The length (in bytes) of the random salt to use. [^modern-algos]: See [Modern Algorithms in the Web Cryptography API][] +[^openssl30]: Requires OpenSSL >= 3.0 + [^openssl35]: Requires OpenSSL >= 3.5 [JSON Web Key]: https://tools.ietf.org/html/rfc7517 diff --git a/lib/internal/crypto/aes.js b/lib/internal/crypto/aes.js index ff2cc108c5..0abffe85c9 100644 --- a/lib/internal/crypto/aes.js +++ b/lib/internal/crypto/aes.js @@ -18,14 +18,17 @@ const { kKeyVariantAES_CBC_128, kKeyVariantAES_GCM_128, kKeyVariantAES_KW_128, + kKeyVariantAES_OCB_128, kKeyVariantAES_CTR_192, kKeyVariantAES_CBC_192, kKeyVariantAES_GCM_192, kKeyVariantAES_KW_192, + kKeyVariantAES_OCB_192, kKeyVariantAES_CTR_256, kKeyVariantAES_CBC_256, kKeyVariantAES_GCM_256, kKeyVariantAES_KW_256, + kKeyVariantAES_OCB_256, kWebCryptoCipherDecrypt, kWebCryptoCipherEncrypt, } = internalBinding('crypto'); @@ -62,6 +65,7 @@ function getAlgorithmName(name, length) { case 'AES-CTR': return `A${length}CTR`; case 'AES-GCM': return `A${length}GCM`; case 'AES-KW': return `A${length}KW`; + case 'AES-OCB': return `A${length}OCB`; } } @@ -100,6 +104,13 @@ function getVariant(name, length) { case 256: return kKeyVariantAES_KW_256; } break; + case 'AES-OCB': + switch (length) { + case 128: return kKeyVariantAES_OCB_128; + case 192: return kKeyVariantAES_OCB_192; + case 256: return kKeyVariantAES_OCB_256; + } + break; } } @@ -173,11 +184,49 @@ function asyncAesGcmCipher(mode, key, data, algorithm) { algorithm.additionalData)); } +function asyncAesOcbCipher(mode, key, data, algorithm) { + const { tagLength = 128 } = algorithm; + + const tagByteLength = tagLength / 8; + let tag; + switch (mode) { + case kWebCryptoCipherDecrypt: { + const slice = ArrayBufferIsView(data) ? + TypedArrayPrototypeSlice : ArrayBufferPrototypeSlice; + tag = slice(data, -tagByteLength); + + // Similar to GCM, OCB requires the tag to be present for decryption + if (tagByteLength > tag.byteLength) { + return PromiseReject(lazyDOMException( + 'The provided data is too small.', + 'OperationError')); + } + + data = slice(data, 0, -tagByteLength); + break; + } + case kWebCryptoCipherEncrypt: + tag = tagByteLength; + break; + } + + return jobPromise(() => new AESCipherJob( + kCryptoJobAsync, + mode, + key[kKeyObject][kHandle], + data, + getVariant('AES-OCB', key.algorithm.length), + algorithm.iv, + tag, + algorithm.additionalData)); +} + function aesCipher(mode, key, data, algorithm) { switch (algorithm.name) { case 'AES-CTR': return asyncAesCtrCipher(mode, key, data, algorithm); case 'AES-CBC': return asyncAesCbcCipher(mode, key, data, algorithm); case 'AES-GCM': return asyncAesGcmCipher(mode, key, data, algorithm); + case 'AES-OCB': return asyncAesOcbCipher(mode, key, data, algorithm); case 'AES-KW': return asyncAesKwCipher(mode, key, data); } } @@ -236,7 +285,11 @@ function aesImportKey( keyObject = keyData; break; } + case 'raw-secret': case 'raw': { + if (format === 'raw' && name === 'AES-OCB') { + return undefined; + } validateKeyLength(keyData.byteLength * 8); keyObject = createSecretKey(keyData); break; diff --git a/lib/internal/crypto/keys.js b/lib/internal/crypto/keys.js index bea4c4b142..c8b3d870ac 100644 --- a/lib/internal/crypto/keys.js +++ b/lib/internal/crypto/keys.js @@ -199,6 +199,8 @@ const { case 'AES-GCM': // Fall through case 'AES-KW': + // Fall through + case 'AES-OCB': result = require('internal/crypto/aes') .aesImportKey(algorithm, 'KeyObject', this, extractable, keyUsages); break; diff --git a/lib/internal/crypto/util.js b/lib/internal/crypto/util.js index b33d36fc8d..98d638909e 100644 --- a/lib/internal/crypto/util.js +++ b/lib/internal/crypto/util.js @@ -39,6 +39,7 @@ const { EVP_PKEY_ML_KEM_512, EVP_PKEY_ML_KEM_768, EVP_PKEY_ML_KEM_1024, + kKeyVariantAES_OCB_128: hasAesOcbMode, } = internalBinding('crypto'); const { getOptionValue } = require('internal/options'); @@ -208,6 +209,14 @@ const kAlgorithmDefinitions = { 'wrapKey': null, 'unwrapKey': null, }, + 'AES-OCB': { + 'generateKey': 'AesKeyGenParams', + 'exportKey': null, + 'importKey': null, + 'encrypt': 'AeadParams', + 'decrypt': 'AeadParams', + 'get key length': 'AesDerivedKeyParams', + }, 'ChaCha20-Poly1305': { 'generateKey': null, 'exportKey': null, @@ -350,6 +359,7 @@ const kAlgorithmDefinitions = { // Conditionally supported algorithms const conditionalAlgorithms = { 'AES-KW': !process.features.openssl_is_boringssl, + 'AES-OCB': !!hasAesOcbMode, 'ChaCha20-Poly1305': !process.features.openssl_is_boringssl || ArrayPrototypeIncludes(getCiphers(), 'chacha20-poly1305'), 'cSHAKE128': !process.features.openssl_is_boringssl || @@ -374,6 +384,7 @@ const conditionalAlgorithms = { // Experimental algorithms const experimentalAlgorithms = [ + 'AES-OCB', 'ChaCha20-Poly1305', 'cSHAKE128', 'cSHAKE256', diff --git a/lib/internal/crypto/webcrypto.js b/lib/internal/crypto/webcrypto.js index b084986833..60f9de5fbf 100644 --- a/lib/internal/crypto/webcrypto.js +++ b/lib/internal/crypto/webcrypto.js @@ -155,6 +155,8 @@ async function generateKey( // Fall through case 'AES-GCM': // Fall through + case 'AES-OCB': + // Fall through case 'AES-KW': resultType = 'CryptoKey'; result = await require('internal/crypto/aes') @@ -253,6 +255,7 @@ function getKeyLength({ name, length, hash }) { case 'AES-CTR': case 'AES-CBC': case 'AES-GCM': + case 'AES-OCB': case 'AES-KW': if (length !== 128 && length !== 192 && length !== 256) throw lazyDOMException('Invalid key length', 'OperationError'); @@ -510,6 +513,8 @@ async function exportKeyRawSecret(key, format) { // Fall through case 'HMAC': return key[kKeyObject][kHandle].export().buffer; + case 'AES-OCB': + // Fall through case 'ChaCha20-Poly1305': if (format === 'raw-secret') { return key[kKeyObject][kHandle].export().buffer; @@ -572,6 +577,8 @@ async function exportKeyJWK(key) { // Fall through case 'AES-GCM': // Fall through + case 'AES-OCB': + // Fall through case 'AES-KW': parameters.alg = require('internal/crypto/aes') .getAlgorithmName(key[kAlgorithm].name, key[kAlgorithm].length); @@ -758,7 +765,11 @@ async function importKey( case 'AES-GCM': // Fall through case 'AES-KW': - format = aliasKeyFormat(format); + // Fall through + case 'AES-OCB': + if (algorithm.name !== 'AES-OCB') { + format = aliasKeyFormat(format); + } result = require('internal/crypto/aes') .aesImportKey(algorithm, format, keyData, extractable, keyUsages); break; @@ -1060,6 +1071,8 @@ async function cipherOrWrap(mode, algorithm, key, data, op) { case 'AES-CBC': // Fall through case 'AES-GCM': + // Fall through + case 'AES-OCB': return require('internal/crypto/aes') .aesCipher(mode, key, data, algorithm); case 'ChaCha20-Poly1305': diff --git a/lib/internal/crypto/webidl.js b/lib/internal/crypto/webidl.js index ed6a1fdc25..1bec09b174 100644 --- a/lib/internal/crypto/webidl.js +++ b/lib/internal/crypto/webidl.js @@ -699,6 +699,13 @@ converters.AeadParams = createDictionaryConverter( case 'aes-gcm': validateMaxBufferLength(V, 'algorithm.iv'); break; + case 'aes-ocb': + if (V.byteLength > 15) { + throw lazyDOMException( + 'AES-OCB algorithm.iv must be no more than 15 bytes', + 'OperationError'); + } + break; } }, required: true, @@ -723,6 +730,13 @@ converters.AeadParams = createDictionaryConverter( 'OperationError'); } break; + case 'aes-ocb': + if (!ArrayPrototypeIncludes([64, 96, 128], V)) { + throw lazyDOMException( + `${V} is not a valid AES-OCB tag length`, + 'OperationError'); + } + break; } }, }, diff --git a/src/crypto/crypto_aes.cc b/src/crypto/crypto_aes.cc index 48c2fbde65..b5495e5973 100644 --- a/src/crypto/crypto_aes.cc +++ b/src/crypto/crypto_aes.cc @@ -61,7 +61,8 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, return WebCryptoCipherStatus::FAILED; } - if (params.cipher.isGcmMode() && !ctx.setIvLength(params.iv.size())) { + if ((params.cipher.isGcmMode() || params.cipher.isOcbMode()) && + !ctx.setIvLength(params.iv.size())) { return WebCryptoCipherStatus::FAILED; } @@ -76,11 +77,20 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, size_t tag_len = 0; - if (params.cipher.isGcmMode()) { + if (params.cipher.isGcmMode() || params.cipher.isOcbMode()) { switch (cipher_mode) { case kWebCryptoCipherDecrypt: { // If in decrypt mode, the auth tag must be set in the params.tag. CHECK(params.tag); + + // For OCB mode, we need to set the auth tag length before setting the + // tag + if (params.cipher.isOcbMode()) { + if (!ctx.setAeadTagLength(params.tag.size())) { + return WebCryptoCipherStatus::FAILED; + } + } + ncrypto::Buffer buffer = { .data = params.tag.data(), .len = params.tag.size(), @@ -91,12 +101,19 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, break; } case kWebCryptoCipherEncrypt: { - // In decrypt mode, we grab the tag length here. We'll use it to + // In encrypt mode, we grab the tag length here. We'll use it to // ensure that that allocated buffer has enough room for both the // final block and the auth tag. Unlike our other AES-GCM implementation // in CipherBase, in WebCrypto, the auth tag is concatenated to the end // of the generated ciphertext and returned in the same ArrayBuffer. tag_len = params.length; + + // For OCB mode, we need to set the auth tag length + if (params.cipher.isOcbMode()) { + if (!ctx.setAeadTagLength(tag_len)) { + return WebCryptoCipherStatus::FAILED; + } + } break; } default: @@ -112,8 +129,8 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, .data = params.additional_data.data(), .len = params.additional_data.size(), }; - if (params.cipher.isGcmMode() && params.additional_data.size() && - !ctx.update(buffer, nullptr, &out_len)) { + if ((params.cipher.isGcmMode() || params.cipher.isOcbMode()) && + params.additional_data.size() && !ctx.update(buffer, nullptr, &out_len)) { return WebCryptoCipherStatus::FAILED; } @@ -147,9 +164,9 @@ WebCryptoCipherStatus AES_Cipher(Environment* env, } total += out_len; - // If using AES_GCM, grab the generated auth tag and append + // If using AES_GCM or AES_OCB, grab the generated auth tag and append // it to the end of the ciphertext. - if (encrypt && params.cipher.isGcmMode()) { + if (encrypt && (params.cipher.isGcmMode() || params.cipher.isOcbMode())) { if (!ctx.getAeadTag(tag_len, ptr + total)) { return WebCryptoCipherStatus::FAILED; } @@ -492,7 +509,7 @@ Maybe AESCipherTraits::AdditionalConfig( if (!ValidateCounter(env, args[offset + 2], params)) { return Nothing(); } - } else if (params->cipher.isGcmMode()) { + } else if (params->cipher.isGcmMode() || params->cipher.isOcbMode()) { if (!ValidateAuthTag(env, mode, cipher_mode, args[offset + 2], params) || !ValidateAdditionalData(env, mode, args[offset + 3], params)) { return Nothing(); @@ -502,9 +519,18 @@ Maybe AESCipherTraits::AdditionalConfig( UseDefaultIV(params); } - if (params->iv.size() < static_cast(params->cipher.getIvLength())) { - THROW_ERR_CRYPTO_INVALID_IV(env); - return Nothing(); + // For OCB mode, allow variable IV lengths (1-15 bytes) + if (params->cipher.isOcbMode()) { + if (params->iv.size() == 0 || params->iv.size() > 15) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return Nothing(); + } + } else { + // For other modes, check against the cipher's expected IV length + if (params->iv.size() < static_cast(params->cipher.getIvLength())) { + THROW_ERR_CRYPTO_INVALID_IV(env); + return Nothing(); + } } return JustVoid(); diff --git a/src/crypto/crypto_aes.h b/src/crypto/crypto_aes.h index 74cfdb8081..401ef70a5e 100644 --- a/src/crypto/crypto_aes.h +++ b/src/crypto/crypto_aes.h @@ -12,7 +12,7 @@ namespace node::crypto { constexpr unsigned kNoAuthTagLength = static_cast(-1); -#define VARIANTS(V) \ +#define VARIANTS_COMMON(V) \ V(CTR_128, AES_CTR_Cipher, ncrypto::Cipher::AES_128_CTR) \ V(CTR_192, AES_CTR_Cipher, ncrypto::Cipher::AES_192_CTR) \ V(CTR_256, AES_CTR_Cipher, ncrypto::Cipher::AES_256_CTR) \ @@ -26,6 +26,19 @@ constexpr unsigned kNoAuthTagLength = static_cast(-1); V(KW_192, AES_Cipher, ncrypto::Cipher::AES_192_KW) \ V(KW_256, AES_Cipher, ncrypto::Cipher::AES_256_KW) +#if OPENSSL_VERSION_MAJOR >= 3 +#define VARIANTS_OCB(V) \ + V(OCB_128, AES_Cipher, ncrypto::Cipher::AES_128_OCB) \ + V(OCB_192, AES_Cipher, ncrypto::Cipher::AES_192_OCB) \ + V(OCB_256, AES_Cipher, ncrypto::Cipher::AES_256_OCB) +#else +#define VARIANTS_OCB(V) +#endif + +#define VARIANTS(V) \ + VARIANTS_COMMON(V) \ + VARIANTS_OCB(V) + enum class AESKeyVariant { #define V(name, _, __) name, VARIANTS(V) diff --git a/test/fixtures/crypto/aes_ocb.js b/test/fixtures/crypto/aes_ocb.js new file mode 100644 index 0000000000..add970d478 --- /dev/null +++ b/test/fixtures/crypto/aes_ocb.js @@ -0,0 +1,124 @@ +'use strict'; + +module.exports = function() { + const kPlaintext = + Buffer.from('546869732073706563696669636174696f6e206465736372696265' + + '732061204a6176615363726970742041504920666f722070657266' + + '6f726d696e672062617369632063727970746f6772617068696320' + + '6f7065726174696f6e7320696e20776562206170706c6963617469' + + '6f6e732c20737563682061732068617368696e672c207369676e61' + + '747572652067656e65726174696f6e20616e642076657269666963' + + '6174696f6e2c20616e6420656e6372797074696f6e20616e642064' + + '656372797074696f6e2e204164646974696f6e616c6c792c206974' + + '2064657363726962657320616e2041504920666f72206170706c69' + + '636174696f6e7320746f2067656e657261746520616e642f6f7220' + + '6d616e61676520746865206b6579696e67206d6174657269616c20' + + '6e656365737361727920746f20706572666f726d20746865736520' + + '6f7065726174696f6e732e205573657320666f7220746869732041' + + '50492072616e67652066726f6d2075736572206f72207365727669' + + '63652061757468656e7469636174696f6e2c20646f63756d656e74' + + '206f7220636f6465207369676e696e672c20616e64207468652063' + + '6f6e666964656e7469616c69747920616e6420696e746567726974' + + '79206f6620636f6d6d756e69636174696f6e732e', 'hex'); + + const kKeyBytes = { + '128': Buffer.from('dec0d4fcbf3c4741c892dabd1cd4c04e', 'hex'), + '256': Buffer.from('67693823fb1d58073f91ece9cc3af910e5532616a4d27b1' + + '3eb7b74d8000bbf30', 'hex') + } + + const iv = Buffer.from('3a92732aa6ea39bf3986e0c73fa920', 'hex'); + + const additionalData = Buffer.from( + '5468657265206172652037206675727468657220656469746f72696' + + '16c206e6f74657320696e2074686520646f63756d656e742e', 'hex'); + + const vectorData = { + '128': { + '64': { + ciphertext: Buffer.from('4680d176c2fa66ef4376bc013ca5435ebd27b260c1236ae0148eb84eb24869ec1f1ebba2ba5356a2ee36944e717f668ab180c94817058216930d0192f403652bd2b0f3adac6466a74a69a8676d8460e2d81811de0cf8c0ec0c1aea48d470d0b6818fffb30dcdba67ffcf4bcf62e241e853c04370014cbea9cd68de4b90f8e52b5d40e972df70104fb70a78ddff9e7eb6e0c528c52aca9738030a6ad253d042697de254a059d06606ce718e8c95afd35767d05640b11367c5de4be405dd0c0bbbff54c8adfdae259b6588a44af382b3c5a2dec4c91bc8c3c156ae4859bd95e1a12f13fd292e0e80de25267941b8c5974e53dcff3741211d9c9e312919283f625201b201bb208f341b792d50c26b3c5769107e28c694ee55396a92b8ef18f6aa5849e44f63da4ab7d6d27d0b7c0869be21c650049dba5c3691de3fdc0dc9cd9676857d35d924372487e87c5ce4d656f69ee0cd62edbd949db134f9850eb6f017d5ba1933e8a39e56822fbe6a35eb9590e28bd1bbd46217c2db14264518caad1929885c143d28f4274fb4a655de0e24b2f37f1351c4820cb5c4fe49e9433f28bc1a0ac63a52200ac0876471c4db9a7ef1852b679f8a1d9bd54e739ce642bdfca700ed162516a33798733b52b726376e10f840714109150c7afeb71c652970ea86', 'hex'), + tagWithAD: Buffer.from('cd1d3fa016ffebf8', 'hex'), + tagWithoutAD: Buffer.from('85917be096f12614', 'hex') + }, + '96': { + ciphertext: Buffer.from('165e7cb1789fc9cb1e9b81e48d2c30d22a019bce5ff79d45ea7adbcf585b7bfc015a5959c9c1478714f4621ee0675f785a1689f1a9254b76580d368cc4a02b18b3f2a8abb5173e2b9f27042af4c0daeae44d88679e7bb79cab48a8f100804c0dad11547c68f2ac0e9a74f1abdeaa7c95e12b97361c4217905f25a03d9a5f8982af979b7756768cd9b044cc928d25ccd56e4fc494e4f62d96aabf3a4bd4889478990e58dcc180c4a81aceaf93afbbcf866a47030d579a981e42d78fae1907df32fd6c8cb37e1bd12e9ac7e81636e411e1717dac7836cf35b2683dd055fd0032a37d048835ef977b381d282ebb4c743eb09126d37764bd177af48d40f0c50534484dfd23ca9d046be673f493a83f705bd3a7d6579814690ee936095f1d80175271f33832ce9d93fff24d4c4ac3fbfa5e12b57109a56fdd5fa302391fe561095dafa4e41ce8e6dc5aac6091aefd7ca3b694ff6301ffdbe02c0c2ce438101ee92a08f85f3b153aa3116a80bc7778040ed9ee8b408909fc6d86004f23798ae85d9b1957435c9f74becdc53b38a7f0b9ac3d515e17d0ca9f5874096db5fb234d0d45e8149f6d15e2d7d3138622fa6fa7eded639fb6929fcaacf03060ec9db0106e58a3fa45d9ab2f6a2b56eee39cfc8cb305901c8f612e24da4cd3b07d4cf966cdf1', 'hex'), + tagWithAD: Buffer.from('80b55f5111770c4fd51ae2a1', 'hex'), + tagWithoutAD: Buffer.from('c8391b119179c1a3c534f03b', 'hex') + }, + '128': { + ciphertext: Buffer.from('0e33334c6fd3cf8c371d06875f342d239832a94c43b2f721d8bd70b6d62e7ac34bfd2041b214cd77624e0330e0892abfd696577144205882a24a1f4f0234602503222558c8c0e7dd033c3888d7f747107d3b11ad3f4d2c6088a80413d12a83587503a7393022ec541b284b358fa1fe3236ae706cd49fb8e2d4216318e8659275d80616940d2f3762e672a19ece3f2ee918c4e99b173c544dfb3300a867564790a436967563fa2bd3240dbb4d370d9153110411d772ff7542651db8c38672cc0f0ceae4f24065dfc996dd8b8d915f1bce206878ac54fad4df8a8157af6f1a8dc0344f526cd6cc398e1f049af3af9334204c5025a653292c0db11985ab83acf1bce2879754c0684a40d7e64e1f062a5d586a7c8702f326119dec9b1d0d316f8ba93f63d07546ee796db70fa66738499126c3a4bdada811dfc698b96569fbfcb935059c9b80349ce2b5caf6def0f2f6ba0d8ebf3395bb1766cddbc93a946d9706342bc378cda55eaee8edb411314c73bb2c480dea05e2eab5f83d089624bc9884dd14ba714d62e15767f730782e37c519b608d8d4ccee98e6d4ba28171417753f72a3b476403ccd5f0bbc4d7021170c751f8d844ee58ce6d2558270333d26e14e184d07e46a22d9270258517c7d6fa55875642d07a74ae0056c41e2931ef08d3f', 'hex'), + tagWithAD: Buffer.from('cdee14358fba08d170fe5906ab34a56f', 'hex'), + tagWithoutAD: Buffer.from('856250750fb4c53d60d04b9cb18534ba', 'hex') + } + }, + '256': { + '64': { + ciphertext: Buffer.from('188ee89ebee501f6a1dd111aa09b00eb67e1b2c6e1f205737d7f47e2cca0b80c9408daccfe820ebeab75f290589dc2175039d60c891002dea5214237393a5672bf91403d69fe41122b666fdd4b796ca18eb84a219895a58ca91689758dfc578079f89f27016a3b3080d0d8177a5ed8fc4b6e0af40604eaf4d91125aeac6656277a429c120bd9a2fa73086eb93302e3cc4d31dd2433d2f07cfbf604e60e380b7f94fb9de182f9752664c57dcb5c9797a952cc8a27b88a582342747c84bdcaae7ddfce460ab681856432429c6cc6e3658929f5669d5088a123a9158b680a8601960b068ea5a7b8f9bac98c3b7399c8fb8067ecbf6313606afdc3d1528d048b6803e12bdb44b119a2107463d01db4bc2791df8f3d0761ce5b401f8e0383f279fe7b1af335b10b48bef05d0e91bdab9631fa79a67a03a4790b4b1d325be028f6beda26d68958811670c86050d05b745f399ac77ff97be3b91cff64fd15e5047b1698c2dffe6d3d7c6cb0c8b5956cee43e8ecf7bc22199bfd7d61178843f9554bc0db539e7a59001ba3c963f299a1d838b8629bddc7c5646145a86ce52077af7785d213c787640b2010424b73b7b786ac7ca946fff501fecd6793ac00e9fb10ce7fb3a6d7b277b7096c83c94a421747f63c43723dd098b144549edbaeda4536ded1', 'hex'), + tagWithAD: Buffer.from('dd2a37097d430860', 'hex'), + tagWithoutAD: Buffer.from('4aefbda08fd14436', 'hex') + }, + '96': { + ciphertext: Buffer.from('18da5c8e77a1fa3cbe6c510c594b905f027ba3b56e446f23bbc17d12f265f260799ec5531792b8ec5cfec2b660bef94760ff5e7a714947cb342a7e9a1e0f3085e7c1608ea9dd1c71507cf09a7683e855b664615702cb556553319b5bd0fcb600d5c3a3bb8c36d8014f30b85a2d26ac36e83cfe8cebb6f7118c3e5875597fa39efd269f5ea237edd60036a906ea592fbe2868455503e9b1928396a894fefe2321138e8f5df4fcc932f4f05f3c15cd16cd5da9d54399dd0a90448f12c54a288b1724b60dfb7a434e72e357ddb631c1038efe9331a5037eb98e61f2a69df51d17de14799df035b5e468782d95d58d0cfd68d69c1b1529b4ad191bbf6f4c10f8a85e2c4ac7c525b3ef258de9a2f9b6193d74ca6e0180333167de426039cf8490d15e7f8ea86bfd143f98f2bb8e5c32a17e19aa0370cc7cbad8cfaeb69be29b6a6a7c27354a9a0dae13258578c681d182855c917c300e96912d24a80db1824e719fd5bddafd839f67fe18f632a892e69e46e0bef56aa94ff26ab7943a4b7de052f5d24302feb3add1c481b019d0d97bd442bd7c0021721d13b9e471686409c7c69551b3977ed3505eff8213e5564d1afe6042dc1ea572aca30cd1b7540e954b2e6c0498ebd2526fb0fb5ffcf48c6ef23b385e99649fd49290e0ce4de6d0a57498b7', 'hex'), + tagWithAD: Buffer.from('06a22a5a7776797981c5ee6d', 'hex'), + tagWithoutAD: Buffer.from('9167a0f385e4352f4c97aa94', 'hex') + }, + '128': { + ciphertext: Buffer.from('4604fafe03ab3897dd54e41c5884cac385d575768a2eb1c4c21c8470636a62565309605c8a556c4f3837be13df6f02de9b5a1ebbedba07e34a1fe0cd037eefc5c324ddfd95a9d16eaebecafd5b93d3c8d9a2deadb079497437d120c1fb0ae4b4d60b9fe220486c54f49c23c00fc3ffb1d57022761315c51f609fb28d70e4de2479325851af9c71671b6507c81fdecc5e5b8335b9d78320b0a4dfdff2f4a0dc86401128fdbe5601491acc6b0876d72fa842e95b75626dde15e602593b82874ed9233ddc64b06c41ea25dcccd678eb720d10d1c85f17635aceef2f102706f6de89b6d0fe6dcd686677d0a682fa3bf781a1fdb13c506be5b1c46ead578c54161129dbe0763d897fde4bfaf87ba61c5cf6884bd1e75678c086aeb2fcf057faf14ec38492ecba850595fa5b84d66c07576486da9cff68dbd961872985b1094d23f9dc31dda35cbe68ee570323843374cb89e07d2f11adf3476e6bdc2b2525cffdffcb7ee58190b19d8601b73d175bd8ebda079c97ed36e77c09a7c1c5e48f57c1881b91e2b17b6a737c79a1528c90ffbc1504914677593b9f6eca64fad08cdd4d318cd7f163cdf325667e949d829bebcccd932481ef132b49ac156eef34947924c165ce64b9e1e1461bd8d3d1e4e928411b448faa5d7db7f8bddc4fdce1ea035d60', 'hex'), + tagWithAD: Buffer.from('a36db50a9235d475609287268818792c', 'hex'), + tagWithoutAD: Buffer.from('34a83fa360a79823adc0c3dfc8e64d20', 'hex') + } + } + }; + + const kKeyLengths = [128, 256]; + const kTagLengths = [64, 96, 128]; + + const passing = []; + kKeyLengths.forEach((keyLength) => { + kTagLengths.forEach((tagLength) => { + const byteCount = tagLength / 8; + const data = vectorData[keyLength][tagLength]; + + // With additional data + const result = new Uint8Array(data.ciphertext.byteLength + byteCount); + result.set(data.ciphertext, 0); + result.set(data.tagWithAD.slice(0, byteCount), data.ciphertext.byteLength); + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-OCB', iv, additionalData, tagLength }, + plaintext: kPlaintext, + result + }); + + // Without additional data + const noadresult = new Uint8Array(data.ciphertext.byteLength + byteCount); + noadresult.set(data.ciphertext, 0); + noadresult.set(data.tagWithoutAD.slice(0, byteCount), data.ciphertext.byteLength); + passing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { name: 'AES-OCB', iv, tagLength }, + plaintext: kPlaintext, + result: noadresult + }); + }); + }); + + const failing = []; + kKeyLengths.forEach((keyLength) => { + [24, 48, 72, 95, 129].forEach((badTagLength) => { + failing.push({ + keyBuffer: kKeyBytes[keyLength], + algorithm: { + name: 'AES-OCB', + iv, + additionalData, + tagLength: badTagLength + }, + plaintext: kPlaintext, + result: vectorData[keyLength]['128'].ciphertext, + }); + }); + }); + + return { passing, failing, decryptionFailing: [] }; +}; diff --git a/test/fixtures/webcrypto/supports-modern-algorithms.mjs b/test/fixtures/webcrypto/supports-modern-algorithms.mjs index 5b35885294..8d0df6c701 100644 --- a/test/fixtures/webcrypto/supports-modern-algorithms.mjs +++ b/test/fixtures/webcrypto/supports-modern-algorithms.mjs @@ -6,6 +6,7 @@ const pqc = hasOpenSSL(3, 5); const shake128 = crypto.getHashes().includes('shake128'); const shake256 = crypto.getHashes().includes('shake256'); const chacha = crypto.getCiphers().includes('chacha20-poly1305'); +const ocb = hasOpenSSL(3); export const vectors = { 'digest': [ @@ -35,6 +36,7 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], + [ocb, { name: 'AES-OCB', length: 128 }], ], 'importKey': [ [pqc, 'ML-DSA-44'], @@ -44,6 +46,7 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], + [ocb, { name: 'AES-OCB', length: 128 }], ], 'exportKey': [ [pqc, 'ML-DSA-44'], @@ -53,6 +56,7 @@ export const vectors = { [pqc, 'ML-KEM-768'], [pqc, 'ML-KEM-1024'], [chacha, 'ChaCha20-Poly1305'], + [ocb, 'AES-OCB'], ], 'getPublicKey': [ [true, 'RSA-OAEP'], @@ -73,6 +77,7 @@ export const vectors = { [false, 'AES-CTR'], [false, 'AES-CBC'], [false, 'AES-GCM'], + [false, 'AES-OCB'], [false, 'AES-KW'], [false, 'ChaCha20-Poly1305'], ], @@ -82,6 +87,13 @@ export const vectors = { [chacha, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 128 }], [false, { name: 'ChaCha20-Poly1305', iv: Buffer.alloc(12), tagLength: 64 }], [false, 'ChaCha20-Poly1305'], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(15) }], + [false, { name: 'AES-OCB', iv: Buffer.alloc(16) }], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 128 }], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 96 }], + [ocb, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 64 }], + [false, { name: 'AES-OCB', iv: Buffer.alloc(12), tagLength: 32 }], + [false, 'AES-OCB'], ], 'encapsulateBits': [ [pqc, 'ML-KEM-512'], diff --git a/test/parallel/test-webcrypto-derivebits-hkdf.js b/test/parallel/test-webcrypto-derivebits-hkdf.js index 590fef60dc..0629f85b0f 100644 --- a/test/parallel/test-webcrypto-derivebits-hkdf.js +++ b/test/parallel/test-webcrypto-derivebits-hkdf.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; function getDeriveKeyInfo(name, length, hash, ...usages) { @@ -34,7 +35,16 @@ if (!process.features.openssl_is_boringssl) { ['HMAC', 256, 'SHA3-512', 'sign', 'verify'], ); } else { - common.printSkipMessage('Skipping unsupported AES-KW test cases'); + common.printSkipMessage('Skipping unsupported test cases'); +} + +if (hasOpenSSL(3)) { + kDerivedKeyTypes.push( + ['AES-OCB', 128, undefined, 'encrypt', 'decrypt'], + ['AES-OCB', 256, undefined, 'encrypt', 'decrypt'], + ); +} else { + common.printSkipMessage('Skipping unsupported test cases'); } const kDerivedKeys = { @@ -464,7 +474,7 @@ async function testDeriveKey( true, usages); - const bits = await subtle.exportKey('raw', key); + const bits = await subtle.exportKey(key.algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw', key); assert.strictEqual( Buffer.from(bits).toString('hex'), diff --git a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js index 298f6d6069..e03be277f0 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt-aes.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt-aes.js @@ -5,6 +5,8 @@ const common = require('../common'); if (!common.hasCrypto) common.skip('missing crypto'); +const { hasOpenSSL } = require('../common/crypto'); + const assert = require('assert'); const { subtle } = globalThis.crypto; @@ -12,8 +14,9 @@ async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { // Using a copy of plaintext to prevent tampering of the original plaintext = Buffer.from(plaintext); + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -37,8 +40,9 @@ async function testEncrypt({ keyBuffer, algorithm, plaintext, result }) { } async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -50,8 +54,9 @@ async function testEncryptNoEncrypt({ keyBuffer, algorithm, plaintext }) { } async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -66,8 +71,9 @@ async function testEncryptNoDecrypt({ keyBuffer, algorithm, plaintext }) { async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { assert.notStrictEqual(algorithm.name, alg); + const keyFormat = alg === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: alg }, false, @@ -79,8 +85,9 @@ async function testEncryptWrongAlg({ keyBuffer, algorithm, plaintext }, alg) { } async function testDecrypt({ keyBuffer, algorithm, result }) { + const keyFormat = algorithm.name === 'AES-OCB' ? 'raw-secret' : 'raw'; const key = await subtle.importKey( - 'raw', + keyFormat, keyBuffer, { name: algorithm.name }, false, @@ -202,6 +209,43 @@ async function testDecrypt({ keyBuffer, algorithm, result }) { })().then(common.mustCall()); } +// Test aes-ocb vectors +if (hasOpenSSL(3)) { + const { + passing, + failing, + decryptionFailing + } = require('../fixtures/crypto/aes_ocb')(); + + (async function() { + const variations = []; + + passing.forEach((vector) => { + variations.push(testEncrypt(vector)); + variations.push(testEncryptNoEncrypt(vector)); + variations.push(testEncryptNoDecrypt(vector)); + variations.push(testEncryptWrongAlg(vector, 'AES-GCM')); + }); + + failing.forEach((vector) => { + variations.push(assert.rejects(testEncrypt(vector), { + message: /is not a valid AES-OCB tag length/ + })); + variations.push(assert.rejects(testDecrypt(vector), { + message: /is not a valid AES-OCB tag length/ + })); + }); + + decryptionFailing.forEach((vector) => { + variations.push(assert.rejects(testDecrypt(vector), { + name: 'OperationError' + })); + }); + + await Promise.all(variations); + })().then(common.mustCall()); +} + { (async function() { const secretKey = await subtle.generateKey( diff --git a/test/parallel/test-webcrypto-encrypt-decrypt.js b/test/parallel/test-webcrypto-encrypt-decrypt.js index c21aa3c29d..a00c7d214b 100644 --- a/test/parallel/test-webcrypto-encrypt-decrypt.js +++ b/test/parallel/test-webcrypto-encrypt-decrypt.js @@ -6,6 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const assert = require('assert'); +const { hasOpenSSL } = require('../common/crypto'); const { subtle } = globalThis.crypto; // This is only a partial test. The WebCrypto Web Platform Tests @@ -181,3 +182,32 @@ if (!process.features.openssl_is_boringssl) { test().then(common.mustCall()); } + +// Test Encrypt/Decrypt AES-OCB +if (hasOpenSSL(3)) { + const buf = globalThis.crypto.getRandomValues(new Uint8Array(50)); + const iv = globalThis.crypto.getRandomValues(new Uint8Array(12)); + + async function test() { + const key = await subtle.generateKey({ + name: 'AES-OCB', + length: 256 + }, true, ['encrypt', 'decrypt']); + + const ciphertext = await subtle.encrypt( + { name: 'AES-OCB', iv }, key, buf, + ); + + const plaintext = await subtle.decrypt( + { name: 'AES-OCB', iv }, key, ciphertext, + ); + + assert.strictEqual( + Buffer.from(plaintext).toString('hex'), + Buffer.from(buf).toString('hex')); + } + + test().then(common.mustCall()); +} else { + common.printSkipMessage('Skipping unsupported AES-OCB test cases'); +} diff --git a/test/parallel/test-webcrypto-keygen.js b/test/parallel/test-webcrypto-keygen.js index 9a1f3d115f..604caa4b9f 100644 --- a/test/parallel/test-webcrypto-keygen.js +++ b/test/parallel/test-webcrypto-keygen.js @@ -173,6 +173,19 @@ if (!process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported test cases'); } +if (hasOpenSSL(3)) { + vectors['AES-OCB'] = { + algorithm: { length: 256 }, + result: 'CryptoKey', + usages: [ + 'encrypt', + 'decrypt', + 'wrapKey', + 'unwrapKey', + ], + }; +} + if (hasOpenSSL(3, 5)) { for (const name of ['ML-DSA-44', 'ML-DSA-65', 'ML-DSA-87']) { vectors[name] = { diff --git a/test/parallel/test-webcrypto-wrap-unwrap.js b/test/parallel/test-webcrypto-wrap-unwrap.js index 6a504692ec..bd788ec4ed 100644 --- a/test/parallel/test-webcrypto-wrap-unwrap.js +++ b/test/parallel/test-webcrypto-wrap-unwrap.js @@ -59,6 +59,18 @@ if (!process.features.openssl_is_boringssl) { common.printSkipMessage('Skipping unsupported AES-KW test case'); } +if (hasOpenSSL(3)) { + kWrappingData['AES-OCB'] = { + generate: { length: 128 }, + wrap: { + iv: new Uint8Array(15), + additionalData: new Uint8Array(16), + tagLength: 128 + }, + pair: false + }; +} + function generateWrappingKeys() { return Promise.all(Object.keys(kWrappingData).map(async (name) => { const keys = await subtle.generateKey(