Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/internal/crypto/hkdf.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const {
const { kMaxLength } = require('buffer');

const {
getDigestSizeInBytes,
jobPromise,
normalizeHashName,
toBuf,
Expand Down Expand Up @@ -141,19 +142,24 @@ function hkdfSync(hash, key, salt, info, length) {
return bits;
}

function validateHkdfDeriveBitsLength(length) {
function validateHkdfDeriveBitsLength(length, hash) {
if (length === null)
throw lazyDOMException('length cannot be null', 'OperationError');
if (length % 8) {
throw lazyDOMException(
'length must be a multiple of 8',
'OperationError');
}
if (length > 255 * getDigestSizeInBytes(hash.name) * 8) {
throw lazyDOMException(
'length exceeds the maximum derived bit length',
'OperationError');
}
}

function hkdfDeriveBits(algorithm, baseKey, length) {
validateHkdfDeriveBitsLength(length);
const { hash, salt, info } = algorithm;
validateHkdfDeriveBitsLength(length, hash);

if (length === 0)
return PromiseResolve(new ArrayBuffer(0));
Expand Down
13 changes: 1 addition & 12 deletions lib/internal/crypto/rsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,6 @@ function rsaKeyGenerate(
extractable,
usages,
) {
const publicExponentConverted = bigIntArrayToUnsignedInt(algorithm.publicExponent);
if (publicExponentConverted === undefined) {
throw lazyDOMException(
'The publicExponent must be equivalent to an unsigned 32-bit value',
'OperationError');
}
const {
name,
modulusLength,
Expand All @@ -118,6 +112,7 @@ function rsaKeyGenerate(

const allowedUsages = kUsages[name];
const usagesSet = validateKeyUsages(usages, allowedUsages.keygen, name);
const publicExponentConverted = bigIntArrayToUnsignedInt(publicExponent);

const keyAlgorithm = {
name,
Expand All @@ -126,12 +121,6 @@ function rsaKeyGenerate(
hash,
};

if (publicExponentConverted < 3 || publicExponentConverted % 2 === 0) {
throw lazyDOMException(
'The operation failed for an operation-specific reason',
'OperationError');
}

const keyUsages = getKeyPairUsages(usagesSet, allowedUsages);
validateUsagesNotEmpty(keyUsages.private);

Expand Down
12 changes: 9 additions & 3 deletions lib/internal/crypto/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -349,20 +349,23 @@ const kAlgorithmDefinitions = {
'importKey': null,
'encapsulate': null,
'decapsulate': null,
'get shared key length': null,
},
'ML-KEM-768': {
'generateKey': null,
'exportKey': null,
'importKey': null,
'encapsulate': null,
'decapsulate': null,
'get shared key length': null,
},
'ML-KEM-1024': {
'generateKey': null,
'exportKey': null,
'importKey': null,
'encapsulate': null,
'decapsulate': null,
'get shared key length': null,
},
'PBKDF2': {
'importKey': null,
Expand Down Expand Up @@ -894,15 +897,18 @@ function jobPromiseThen(promise, onFulfilled, onRejected) {
// an unsigned int from a Buffer are not adequate. The implementation
// here is adapted from the chromium implementation here:
// https://github.com/chromium/chromium/blob/HEAD/third_party/blink/public/platform/web_crypto_algorithm_params.h, but ported to JavaScript
// Returns undefined if the conversion was unsuccessful.
// Throws an OperationError if the value does not fit in an unsigned 32-bit integer.
function bigIntArrayToUnsignedInt(input) {
let result = 0;
const length = TypedArrayPrototypeGetLength(input);

for (let n = 0; n < length; ++n) {
const n_reversed = length - n - 1;
if (n_reversed >= 4 && input[n])
return; // Too large
if (n_reversed >= 4 && input[n]) {
throw lazyDOMException(
'algorithm.publicExponent must fit in an unsigned 32-bit integer',
'OperationError');
}
result |= input[n] << 8 * n_reversed;
}

Expand Down
85 changes: 58 additions & 27 deletions lib/internal/crypto/webcrypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,54 @@ function getKeyLength({ name, length, hash }) {
}
}

function getSharedKeyLength({ name }) {
switch (name) {
case 'ML-KEM-512':
// Fall through
case 'ML-KEM-768':
// Fall through
case 'ML-KEM-1024':
return 256;
/* c8 ignore start */
default: {
const assert = require('internal/assert');
assert.fail('Unreachable code');
}
/* c8 ignore stop */
}
}

function canImportRawSecret(algorithm, sharedKeyLength) {
switch (algorithm.name) {
case 'AES-OCB':
case 'AES-KW':
case 'AES-GCM':
case 'AES-CTR':
case 'AES-CBC':
return sharedKeyLength === 128 ||
sharedKeyLength === 192 ||
sharedKeyLength === 256;
case 'ChaCha20-Poly1305':
return sharedKeyLength === 256;
case 'HKDF':
case 'PBKDF2':
case 'Argon2i':
case 'Argon2d':
case 'Argon2id':
return true;
case 'HMAC':
if (sharedKeyLength === 0)
return false;
// Fall through
case 'KMAC128':
case 'KMAC256':
return algorithm.length === undefined ||
numBitsToBytes(algorithm.length) * 8 === sharedKeyLength;
default:
return false;
}
}

function deriveKey(
algorithm,
baseKey,
Expand Down Expand Up @@ -1741,37 +1789,19 @@ class SubtleCrypto {
},
);

let sharedKeyLength;
let normalizedAdditionalAlgorithm;
try {
const normalizedAlgorithm =
normalizeAlgorithm(algorithm, 'get shared key length');
sharedKeyLength = getSharedKeyLength(normalizedAlgorithm);
normalizedAdditionalAlgorithm = normalizeAlgorithm(additionalAlgorithm, 'importKey');
} catch {
return false;
}

switch (normalizedAdditionalAlgorithm.name) {
case 'AES-OCB':
case 'AES-KW':
case 'AES-GCM':
case 'AES-CTR':
case 'AES-CBC':
case 'ChaCha20-Poly1305':
case 'HKDF':
case 'PBKDF2':
case 'Argon2i':
case 'Argon2d':
case 'Argon2id':
break;
case 'HMAC':
case 'KMAC128':
case 'KMAC256':
if (normalizedAdditionalAlgorithm.length === undefined ||
numBitsToBytes(normalizedAdditionalAlgorithm.length) === 32) {
break;
}
return false;
default:
return false;
}
if (!canImportRawSecret(normalizedAdditionalAlgorithm, sharedKeyLength))
return false;
}

try {
Expand Down Expand Up @@ -1807,8 +1837,6 @@ function check(op, alg, length) {
}

switch (op) {
case 'decapsulate':
case 'decrypt':
case 'digest': {
if ((normalizedAlgorithm.name === 'cSHAKE128' ||
normalizedAlgorithm.name === 'cSHAKE256') &&
Expand All @@ -1818,6 +1846,8 @@ function check(op, alg, length) {
}
return true;
}
case 'decapsulate':
case 'decrypt':
case 'encapsulate':
case 'encrypt':
case 'exportKey':
Expand All @@ -1829,7 +1859,8 @@ function check(op, alg, length) {
return true;
case 'deriveBits': {
if (normalizedAlgorithm.name === 'HKDF') {
require('internal/crypto/hkdf').validateHkdfDeriveBitsLength(length);
require('internal/crypto/hkdf')
.validateHkdfDeriveBitsLength(length, normalizedAlgorithm.hash);
}

if (normalizedAlgorithm.name === 'PBKDF2') {
Expand Down
40 changes: 36 additions & 4 deletions lib/internal/crypto/webidl.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
getCryptoKeyType,
} = require('internal/crypto/keys');
const {
bigIntArrayToUnsignedInt,
validateMaxBufferLength,
getBufferSourceByteLength,
getBufferSourceBytes,
Expand All @@ -41,6 +42,8 @@ const {
type,
} = require('internal/webidl');

const kRsaKeyGenMinimumModulusLength = isFips ? 2048 : 512;

function validateByteLength(buf, name, target) {
if (getBufferSourceByteLength(buf) !== target) {
throw lazyDOMException(
Expand Down Expand Up @@ -152,11 +155,33 @@ const dictRsaKeyGenParams = [
key: 'modulusLength',
converter: (V, opts) =>
converters['unsigned long'](V, enforceRangeOptions(opts)),
validator: (modulusLength) => {
if (modulusLength < kRsaKeyGenMinimumModulusLength) {
throw lazyDOMException(
`algorithm.modulusLength must be at least ${kRsaKeyGenMinimumModulusLength}`,
'OperationError');
}
},
required: true,
},
{
key: 'publicExponent',
converter: converters.BigInteger,
validator: (publicExponent) => {
const converted = bigIntArrayToUnsignedInt(publicExponent);

if (converted < 3) {
throw lazyDOMException(
'algorithm.publicExponent must be at least 3',
'OperationError');
}

if (converted % 2 === 0) {
throw lazyDOMException(
'algorithm.publicExponent must be odd',
'OperationError');
}
},
required: true,
},
];
Expand Down Expand Up @@ -625,20 +650,27 @@ converters.ContextParams = createDictionaryConverter(
key: 'context',
converter: converters.BufferSource,
validator(V, dict) {
const validateLength = (V) =>
validateMaxBufferLength(V, 'ContextParams.context', 255);

if (process.features.openssl_is_boringssl) {
this.validator = undefined;
this.validator = validateLength;
} else {
let { 0: major, 1: minor } =
StringPrototypeSplit(process.versions.openssl, '.');
major = NumberParseInt(major, 10);
minor = NumberParseInt(minor, 10);
if (major > 3 || (major === 3 && minor >= 2)) {
this.validator = undefined;
this.validator = validateLength;
} else {
this.validator = validateZeroLength('ContextParams.context');
this.validator(V, dict);
const validateEmpty = validateZeroLength('ContextParams.context');
this.validator = (V, dict) => {
validateLength(V);
validateEmpty(V, dict);
};
}
}
this.validator(V, dict);
},
},
],
Expand Down
32 changes: 32 additions & 0 deletions test/fixtures/webcrypto/supports-level-2.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { getFips } from 'node:crypto';

const { subtle } = globalThis.crypto;
const RSA_MINIMUM_MODULUS_LENGTH = getFips() === 1 ? 2048 : 512;

const RSA_KEY_GEN = {
modulusLength: 2048,
Expand Down Expand Up @@ -66,6 +69,30 @@ export const vectors = {
[true, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256', ...RSA_KEY_GEN }],
[true, { name: 'RSA-PSS', hash: 'SHA-256', ...RSA_KEY_GEN }],
[true, { name: 'RSA-OAEP', hash: 'SHA-256', ...RSA_KEY_GEN }],
[true, {
name: 'RSA-PSS',
hash: 'SHA-256',
modulusLength: RSA_MINIMUM_MODULUS_LENGTH,
publicExponent: new Uint8Array([1, 0, 1]),
}],
[false, {
name: 'RSASSA-PKCS1-v1_5',
hash: 'SHA-256',
modulusLength: RSA_MINIMUM_MODULUS_LENGTH - 1,
publicExponent: new Uint8Array([1, 0, 1]),
}],
[false, {
name: 'RSA-PSS',
hash: 'SHA-256',
...RSA_KEY_GEN,
publicExponent: new Uint8Array([2]),
}],
[false, {
name: 'RSA-OAEP',
hash: 'SHA-256',
...RSA_KEY_GEN,
publicExponent: new Uint8Array([1, 0, 0, 0, 1]),
}],
[true, { name: 'ECDSA', namedCurve: 'P-256' }],
[false, { name: 'ECDSA', namedCurve: 'X25519' }],
[true, { name: 'AES-CTR', length: 128 }],
Expand Down Expand Up @@ -146,6 +173,8 @@ export const vectors = {
'deriveBits': [
[true, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8],
[true, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 0],
[true, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 65280],
[false, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 65288],
[false, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, null],
[false, { name: 'HKDF', hash: 'SHA-256', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 7],
[false, { name: 'HKDF', hash: 'Invalid', salt: Buffer.alloc(0), info: Buffer.alloc(0) }, 8],
Expand Down Expand Up @@ -234,4 +263,7 @@ export const vectors = {
'get key length': [
[false, { name: 'HMAC', hash: 'SHA-256' }],
],
'get shared key length': [
[false, 'ML-KEM-768'],
],
};
Loading
Loading