crypto: add AES-OCB Web Cryptography algorithm

PR-URL: https://github.com/nodejs/node/pull/59539
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Filip Skokan 2025-08-24 11:47:20 +02:00 committed by GitHub
parent 28e6ba8e4d
commit 8692e601cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 440 additions and 23 deletions

View File

@ -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;

View File

@ -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;

View File

@ -2,6 +2,9 @@
<!-- YAML
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59539
description: AES-OCB algorithm is now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59569
description: ML-KEM algorithms are now supported.
@ -104,6 +107,7 @@ WICG proposal:
Algorithms:
* `'AES-OCB'`[^openssl30]
* `'ChaCha20-Poly1305'`
* `'cSHAKE128'`
* `'cSHAKE256'`
@ -501,6 +505,7 @@ implementation and the APIs supported for each:
| `'AES-CTR'` | ✔ | ✔ | ✔ | |
| `'AES-GCM'` | ✔ | ✔ | ✔ | |
| `'AES-KW'` | ✔ | ✔ | ✔ | |
| `'AES-OCB'` | ✔ | ✔ | ✔ | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | ✔ | ✔ | |
| `'ECDH'` | ✔ | ✔ | ✔ | ✔ |
| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ |
@ -539,6 +544,7 @@ implementation and the APIs supported for each:
| `'AES-CTR'` | ✔ | | | ✔ | | |
| `'AES-GCM'` | ✔ | | | ✔ | | |
| `'AES-KW'` | | | | ✔ | | |
| `'AES-OCB'` | ✔ | | | ✔ | | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | | | ✔ | | |
| `'cSHAKE128'`[^modern-algos] | | | | | | ✔ |
| `'cSHAKE256'`[^modern-algos] | | | | | | ✔ |
@ -707,6 +713,7 @@ Valid key usages depend on the key algorithm (identified by
| `'AES-CTR'` | ✔ | | | ✔ | |
| `'AES-GCM'` | ✔ | | | ✔ | |
| `'AES-KW'` | | | | ✔ | |
| `'AES-OCB'` | ✔ | | | ✔ | |
| `'ChaCha20-Poly1305'`[^modern-algos] | ✔ | | | ✔ | |
| `'ECDH'` | | | ✔ | | |
| `'ECDSA'` | | ✔ | | | |
@ -825,6 +832,9 @@ The algorithms currently supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59539
description: AES-OCB algorithm is now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -845,6 +855,7 @@ The algorithms currently supported include:
* `'AES-CBC'`
* `'AES-CTR'`
* `'AES-GCM'`
* `'AES-OCB'`[^modern-algos]
* `'ChaCha20-Poly1305'`[^modern-algos]
* `'RSA-OAEP'`
@ -1015,6 +1026,9 @@ The algorithms currently supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59539
description: AES-OCB algorithm is now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -1035,6 +1049,7 @@ The algorithms currently supported include:
* `'AES-CBC'`
* `'AES-CTR'`
* `'AES-GCM'`
* `'AES-OCB'`[^modern-algos]
* `'ChaCha20-Poly1305'`[^modern-algos]
* `'RSA-OAEP'`
@ -1086,6 +1101,7 @@ specification.
| `'AES-CTR'` | | | ✔ | ✔ | ✔ | | |
| `'AES-GCM'` | | | ✔ | ✔ | ✔ | | |
| `'AES-KW'` | | | ✔ | ✔ | ✔ | | |
| `'AES-OCB'`[^modern-algos] | | | ✔ | | ✔ | | |
| `'ChaCha20-Poly1305'`[^modern-algos] | | | ✔ | | ✔ | | |
| `'ECDH'` | ✔ | ✔ | ✔ | ✔ | | ✔ | |
| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ | | ✔ | |
@ -1171,6 +1187,7 @@ The {CryptoKey} (secret key) generating algorithms supported include:
* `'AES-CTR'`
* `'AES-GCM'`
* `'AES-KW'`
* `'AES-OCB'`[^modern-algos]
* `'ChaCha20-Poly1305'`[^modern-algos]
* `'HMAC'`
@ -1228,6 +1245,7 @@ The algorithms currently supported include:
| `'AES-CTR'` | | | ✔ | ✔ | ✔ | | |
| `'AES-GCM'` | | | ✔ | ✔ | ✔ | | |
| `'AES-KW'` | | | ✔ | ✔ | ✔ | | |
| `'AES-OCB'`[^modern-algos] | | | ✔ | | ✔ | | |
| `'ChaCha20-Poly1305'`[^modern-algos] | | | ✔ | | ✔ | | |
| `'ECDH'` | ✔ | ✔ | ✔ | ✔ | | ✔ | |
| `'ECDSA'` | ✔ | ✔ | ✔ | ✔ | | ✔ | |
@ -1294,6 +1312,9 @@ The algorithms currently supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59539
description: AES-OCB algorithm is now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -1330,6 +1351,7 @@ The wrapping algorithms currently supported include:
* `'AES-CTR'`
* `'AES-GCM'`
* `'AES-KW'`
* `'AES-OCB'`[^modern-algos]
* `'ChaCha20-Poly1305'`[^modern-algos]
* `'RSA-OAEP'`
@ -1339,6 +1361,7 @@ The unwrapped key algorithms supported include:
* `'AES-CTR'`
* `'AES-GCM'`
* `'AES-KW'`
* `'AES-OCB'`[^modern-algos]
* `'ChaCha20-Poly1305'`[^modern-algos]
* `'ECDH'`
* `'ECDSA'`
@ -1404,6 +1427,9 @@ The algorithms currently supported include:
<!-- YAML
added: v15.0.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59539
description: AES-OCB algorithm is now supported.
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59365
description: ChaCha20-Poly1305 algorithm is now supported.
@ -1436,6 +1462,7 @@ The wrapping algorithms currently supported include:
* `'AES-CTR'`
* `'AES-GCM'`
* `'AES-KW'`
* `'AES-OCB'`[^modern-algos]
* `'ChaCha20-Poly1305'`[^modern-algos]
* `'RSA-OAEP'`
@ -1493,7 +1520,7 @@ given key.
added: v15.0.0
-->
* 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

View File

@ -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;

View File

@ -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;

View File

@ -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',

View File

@ -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':

View File

@ -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;
}
},
},

View File

@ -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<const char> buffer = {
.data = params.tag.data<char>(),
.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<unsigned char>(),
.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<void> AESCipherTraits::AdditionalConfig(
if (!ValidateCounter(env, args[offset + 2], params)) {
return Nothing<void>();
}
} 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<void>();
@ -502,9 +519,18 @@ Maybe<void> AESCipherTraits::AdditionalConfig(
UseDefaultIV(params);
}
if (params->iv.size() < static_cast<size_t>(params->cipher.getIvLength())) {
THROW_ERR_CRYPTO_INVALID_IV(env);
return Nothing<void>();
// 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<void>();
}
} else {
// For other modes, check against the cipher's expected IV length
if (params->iv.size() < static_cast<size_t>(params->cipher.getIvLength())) {
THROW_ERR_CRYPTO_INVALID_IV(env);
return Nothing<void>();
}
}
return JustVoid();

View File

@ -12,7 +12,7 @@
namespace node::crypto {
constexpr unsigned kNoAuthTagLength = static_cast<unsigned>(-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<unsigned>(-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)

124
test/fixtures/crypto/aes_ocb.js vendored Normal file
View File

@ -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: [] };
};

View File

@ -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'],

View File

@ -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'),

View File

@ -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(

View File

@ -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');
}

View File

@ -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] = {

View File

@ -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(