react-native-quick-crypto 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QuickCrypto.podspec +1 -0
- package/android/CMakeLists.txt +4 -0
- package/cpp/cipher/CCMCipher.cpp +7 -11
- package/cpp/cipher/ChaCha20Cipher.cpp +6 -10
- package/cpp/cipher/ChaCha20Poly1305Cipher.cpp +10 -16
- package/cpp/cipher/GCMCipher.cpp +3 -5
- package/cpp/cipher/HybridCipher.cpp +7 -13
- package/cpp/cipher/HybridRsaCipher.cpp +19 -27
- package/cpp/cipher/OCBCipher.cpp +2 -3
- package/cpp/cipher/XChaCha20Poly1305Cipher.cpp +13 -19
- package/cpp/cipher/XSalsa20Cipher.cpp +8 -12
- package/cpp/cipher/XSalsa20Poly1305Cipher.cpp +11 -16
- package/cpp/keys/HybridKeyObjectHandle.cpp +630 -2
- package/cpp/keys/HybridKeyObjectHandle.hpp +21 -1
- package/cpp/sign/HybridSignHandle.cpp +26 -8
- package/cpp/sign/HybridVerifyHandle.cpp +28 -11
- package/cpp/slhdsa/HybridSlhDsaKeyPair.cpp +245 -0
- package/cpp/slhdsa/HybridSlhDsaKeyPair.hpp +48 -0
- package/cpp/turboshake/HybridTurboShake.cpp +379 -0
- package/cpp/turboshake/HybridTurboShake.hpp +28 -0
- package/cpp/utils/HybridUtils.cpp +26 -14
- package/deps/blake3/README.md +6 -7
- package/deps/blake3/c/blake3.c +3 -2
- package/deps/blake3/c/blake3.h +2 -2
- package/deps/blake3/c/blake3_dispatch.c +2 -2
- package/deps/blake3/c/blake3_impl.h +1 -1
- package/deps/blake3/c/blake3_neon.c +5 -4
- package/deps/ncrypto/include/ncrypto/version.h +2 -2
- package/deps/ncrypto/include/ncrypto.h +9 -2
- package/deps/ncrypto/src/ncrypto.cpp +130 -35
- package/lib/commonjs/dhKeyPair.js +3 -0
- package/lib/commonjs/dhKeyPair.js.map +1 -1
- package/lib/commonjs/dsa.js +3 -0
- package/lib/commonjs/dsa.js.map +1 -1
- package/lib/commonjs/ec.js +37 -30
- package/lib/commonjs/ec.js.map +1 -1
- package/lib/commonjs/ed.js +60 -6
- package/lib/commonjs/ed.js.map +1 -1
- package/lib/commonjs/hash.js +52 -5
- package/lib/commonjs/hash.js.map +1 -1
- package/lib/commonjs/keys/classes.js +33 -7
- package/lib/commonjs/keys/classes.js.map +1 -1
- package/lib/commonjs/keys/generateKeyPair.js +85 -4
- package/lib/commonjs/keys/generateKeyPair.js.map +1 -1
- package/lib/commonjs/keys/index.js +50 -2
- package/lib/commonjs/keys/index.js.map +1 -1
- package/lib/commonjs/keys/signVerify.js +9 -2
- package/lib/commonjs/keys/signVerify.js.map +1 -1
- package/lib/commonjs/keys/utils.js +59 -1
- package/lib/commonjs/keys/utils.js.map +1 -1
- package/lib/commonjs/random.js +63 -9
- package/lib/commonjs/random.js.map +1 -1
- package/lib/commonjs/rsa.js +3 -0
- package/lib/commonjs/rsa.js.map +1 -1
- package/lib/commonjs/slhdsa.js +70 -0
- package/lib/commonjs/slhdsa.js.map +1 -0
- package/lib/commonjs/specs/slhDsaKeyPair.nitro.js +6 -0
- package/lib/commonjs/specs/slhDsaKeyPair.nitro.js.map +1 -0
- package/lib/commonjs/specs/turboshake.nitro.js +6 -0
- package/lib/commonjs/specs/turboshake.nitro.js.map +1 -0
- package/lib/commonjs/subtle.js +926 -275
- package/lib/commonjs/subtle.js.map +1 -1
- package/lib/commonjs/utils/conversion.js +53 -19
- package/lib/commonjs/utils/conversion.js.map +1 -1
- package/lib/commonjs/utils/errors.js +63 -4
- package/lib/commonjs/utils/errors.js.map +1 -1
- package/lib/commonjs/utils/types.js.map +1 -1
- package/lib/commonjs/utils/validation.js +46 -0
- package/lib/commonjs/utils/validation.js.map +1 -1
- package/lib/module/dhKeyPair.js +3 -0
- package/lib/module/dhKeyPair.js.map +1 -1
- package/lib/module/dsa.js +3 -0
- package/lib/module/dsa.js.map +1 -1
- package/lib/module/ec.js +38 -31
- package/lib/module/ec.js.map +1 -1
- package/lib/module/ed.js +61 -7
- package/lib/module/ed.js.map +1 -1
- package/lib/module/hash.js +52 -5
- package/lib/module/hash.js.map +1 -1
- package/lib/module/keys/classes.js +31 -5
- package/lib/module/keys/classes.js.map +1 -1
- package/lib/module/keys/generateKeyPair.js +86 -5
- package/lib/module/keys/generateKeyPair.js.map +1 -1
- package/lib/module/keys/index.js +50 -2
- package/lib/module/keys/index.js.map +1 -1
- package/lib/module/keys/signVerify.js +9 -2
- package/lib/module/keys/signVerify.js.map +1 -1
- package/lib/module/keys/utils.js +57 -1
- package/lib/module/keys/utils.js.map +1 -1
- package/lib/module/random.js +63 -10
- package/lib/module/random.js.map +1 -1
- package/lib/module/rsa.js +3 -0
- package/lib/module/rsa.js.map +1 -1
- package/lib/module/slhdsa.js +64 -0
- package/lib/module/slhdsa.js.map +1 -0
- package/lib/module/specs/slhDsaKeyPair.nitro.js +4 -0
- package/lib/module/specs/slhDsaKeyPair.nitro.js.map +1 -0
- package/lib/module/specs/turboshake.nitro.js +4 -0
- package/lib/module/specs/turboshake.nitro.js.map +1 -0
- package/lib/module/subtle.js +927 -276
- package/lib/module/subtle.js.map +1 -1
- package/lib/module/utils/conversion.js +51 -19
- package/lib/module/utils/conversion.js.map +1 -1
- package/lib/module/utils/errors.js +61 -4
- package/lib/module/utils/errors.js.map +1 -1
- package/lib/module/utils/types.js.map +1 -1
- package/lib/module/utils/validation.js +44 -0
- package/lib/module/utils/validation.js.map +1 -1
- package/lib/typescript/dhKeyPair.d.ts.map +1 -1
- package/lib/typescript/dsa.d.ts.map +1 -1
- package/lib/typescript/ec.d.ts.map +1 -1
- package/lib/typescript/ed.d.ts.map +1 -1
- package/lib/typescript/hash.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +12 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/keys/classes.d.ts +10 -1
- package/lib/typescript/keys/classes.d.ts.map +1 -1
- package/lib/typescript/keys/generateKeyPair.d.ts +12 -1
- package/lib/typescript/keys/generateKeyPair.d.ts.map +1 -1
- package/lib/typescript/keys/index.d.ts +3 -1
- package/lib/typescript/keys/index.d.ts.map +1 -1
- package/lib/typescript/keys/signVerify.d.ts.map +1 -1
- package/lib/typescript/keys/utils.d.ts +21 -4
- package/lib/typescript/keys/utils.d.ts.map +1 -1
- package/lib/typescript/random.d.ts +5 -1
- package/lib/typescript/random.d.ts.map +1 -1
- package/lib/typescript/rsa.d.ts.map +1 -1
- package/lib/typescript/slhdsa.d.ts +19 -0
- package/lib/typescript/slhdsa.d.ts.map +1 -0
- package/lib/typescript/specs/keyObjectHandle.nitro.d.ts +9 -0
- package/lib/typescript/specs/keyObjectHandle.nitro.d.ts.map +1 -1
- package/lib/typescript/specs/slhDsaKeyPair.nitro.d.ts +16 -0
- package/lib/typescript/specs/slhDsaKeyPair.nitro.d.ts.map +1 -0
- package/lib/typescript/specs/turboshake.nitro.d.ts +11 -0
- package/lib/typescript/specs/turboshake.nitro.d.ts.map +1 -0
- package/lib/typescript/subtle.d.ts +3 -2
- package/lib/typescript/subtle.d.ts.map +1 -1
- package/lib/typescript/utils/conversion.d.ts +4 -3
- package/lib/typescript/utils/conversion.d.ts.map +1 -1
- package/lib/typescript/utils/errors.d.ts +12 -0
- package/lib/typescript/utils/errors.d.ts.map +1 -1
- package/lib/typescript/utils/types.d.ts +32 -15
- package/lib/typescript/utils/types.d.ts.map +1 -1
- package/lib/typescript/utils/validation.d.ts +3 -1
- package/lib/typescript/utils/validation.d.ts.map +1 -1
- package/nitrogen/generated/android/QuickCrypto+autolinking.cmake +2 -0
- package/nitrogen/generated/android/QuickCryptoOnLoad.cpp +20 -0
- package/nitrogen/generated/ios/QuickCryptoAutolinking.mm +20 -0
- package/nitrogen/generated/shared/c++/AsymmetricKeyType.hpp +48 -0
- package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.cpp +9 -0
- package/nitrogen/generated/shared/c++/HybridKeyObjectHandleSpec.hpp +9 -0
- package/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.cpp +29 -0
- package/nitrogen/generated/shared/c++/HybridSlhDsaKeyPairSpec.hpp +72 -0
- package/nitrogen/generated/shared/c++/HybridTurboShakeSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridTurboShakeSpec.hpp +70 -0
- package/nitrogen/generated/shared/c++/JWK.hpp +9 -1
- package/nitrogen/generated/shared/c++/JWKkty.hpp +4 -0
- package/nitrogen/generated/shared/c++/KangarooTwelveVariant.hpp +76 -0
- package/nitrogen/generated/shared/c++/TurboShakeVariant.hpp +76 -0
- package/package.json +2 -3
- package/src/dhKeyPair.ts +8 -0
- package/src/dsa.ts +8 -0
- package/src/ec.ts +52 -29
- package/src/ed.ts +95 -16
- package/src/hash.ts +108 -5
- package/src/keys/classes.ts +46 -5
- package/src/keys/generateKeyPair.ts +151 -5
- package/src/keys/index.ts +73 -3
- package/src/keys/signVerify.ts +13 -2
- package/src/keys/utils.ts +78 -5
- package/src/random.ts +93 -9
- package/src/rsa.ts +8 -0
- package/src/slhdsa.ts +146 -0
- package/src/specs/keyObjectHandle.nitro.ts +17 -0
- package/src/specs/slhDsaKeyPair.nitro.ts +29 -0
- package/src/specs/turboshake.nitro.ts +21 -0
- package/src/subtle.ts +1191 -360
- package/src/utils/conversion.ts +72 -21
- package/src/utils/errors.ts +72 -4
- package/src/utils/types.ts +80 -15
- package/src/utils/validation.ts +70 -1
package/src/subtle.ts
CHANGED
|
@@ -18,7 +18,8 @@ import type {
|
|
|
18
18
|
RsaOaepParams,
|
|
19
19
|
ChaCha20Poly1305Params,
|
|
20
20
|
} from './utils';
|
|
21
|
-
import { KFormatType, KeyEncoding, KeyType } from './utils';
|
|
21
|
+
import { KFormatType, KeyEncoding, KeyType, kNamedCurveAliases } from './utils';
|
|
22
|
+
import { Buffer } from '@craftzdog/react-native-buffer';
|
|
22
23
|
import {
|
|
23
24
|
CryptoKey,
|
|
24
25
|
KeyObject,
|
|
@@ -31,7 +32,10 @@ import { bufferLikeToArrayBuffer } from './utils/conversion';
|
|
|
31
32
|
import { argon2Sync } from './argon2';
|
|
32
33
|
import { lazyDOMException } from './utils/errors';
|
|
33
34
|
import { normalizeHashName, HashContext } from './utils/hashnames';
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
validateJwkStructure,
|
|
37
|
+
validateMaxBufferLength,
|
|
38
|
+
} from './utils/validation';
|
|
35
39
|
import { asyncDigest } from './hash';
|
|
36
40
|
import { createSecretKey, createPublicKey } from './keys';
|
|
37
41
|
import { NitroModules } from 'react-native-nitro-modules';
|
|
@@ -58,6 +62,11 @@ import {
|
|
|
58
62
|
Ed,
|
|
59
63
|
} from './ed';
|
|
60
64
|
import { mldsa_generateKeyPairWebCrypto, type MlDsaVariant } from './mldsa';
|
|
65
|
+
import {
|
|
66
|
+
slhdsa_generateKeyPairWebCrypto,
|
|
67
|
+
SLH_DSA_VARIANTS,
|
|
68
|
+
type SlhDsaVariant,
|
|
69
|
+
} from './slhdsa';
|
|
61
70
|
import {
|
|
62
71
|
mlkem_generateKeyPairWebCrypto,
|
|
63
72
|
type MlKemVariant,
|
|
@@ -83,6 +92,18 @@ function hasAnyNotIn(usages: KeyUsage[], allowed: KeyUsage[]): boolean {
|
|
|
83
92
|
return usages.some(usage => !allowed.includes(usage));
|
|
84
93
|
}
|
|
85
94
|
|
|
95
|
+
// Mirrors webidl.requiredArguments. Node throws TypeError when a SubtleCrypto
|
|
96
|
+
// method is called with fewer than the spec-required number of arguments
|
|
97
|
+
// (webcrypto.js:866 etc.); we relied on TypeScript types alone, which apps
|
|
98
|
+
// catching `instanceof TypeError` could not see at runtime.
|
|
99
|
+
function requireArgs(actual: number, required: number, method: string): void {
|
|
100
|
+
if (actual < required) {
|
|
101
|
+
throw new TypeError(
|
|
102
|
+
`Failed to execute '${method}' on 'SubtleCrypto': ${required} arguments required, but only ${actual} present.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
86
107
|
// WebCrypto §18.4.4: algorithm name lookup is case-insensitive, but the
|
|
87
108
|
// canonical mixed-case form is preserved in the resulting `name` field
|
|
88
109
|
// (e.g. "aes-gcm" → "AES-GCM"). This map is built lazily on first call so
|
|
@@ -124,37 +145,6 @@ function normalizeAlgorithm(
|
|
|
124
145
|
return algorithm as SubtleAlgorithm;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
// WebCrypto §25.7.6 (JWK import): if the JWK's `ext` member is present and
|
|
128
|
-
// false, the requested `extractable` parameter must also be false. If the
|
|
129
|
-
// JWK's `key_ops` member is present, every requested usage must appear in
|
|
130
|
-
// it. We centralize the check here so every importKey path that accepts
|
|
131
|
-
// `format === 'jwk'` can reuse it.
|
|
132
|
-
function validateJwkExtAndKeyOps(
|
|
133
|
-
jwk: JWK,
|
|
134
|
-
extractable: boolean,
|
|
135
|
-
keyUsages: KeyUsage[],
|
|
136
|
-
): void {
|
|
137
|
-
if (jwk.ext === false && extractable) {
|
|
138
|
-
throw lazyDOMException(
|
|
139
|
-
'JWK "ext" is false but extractable was requested',
|
|
140
|
-
'DataError',
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
if (jwk.key_ops !== undefined) {
|
|
144
|
-
if (!Array.isArray(jwk.key_ops)) {
|
|
145
|
-
throw lazyDOMException('JWK "key_ops" must be an array', 'DataError');
|
|
146
|
-
}
|
|
147
|
-
for (const usage of keyUsages) {
|
|
148
|
-
if (!jwk.key_ops.includes(usage)) {
|
|
149
|
-
throw lazyDOMException(
|
|
150
|
-
`JWK "key_ops" does not include requested usage "${usage}"`,
|
|
151
|
-
'DataError',
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
148
|
function getAlgorithmName(name: string, length: number): string {
|
|
159
149
|
switch (name) {
|
|
160
150
|
case 'AES-CBC':
|
|
@@ -174,15 +164,70 @@ function getAlgorithmName(name: string, length: number): string {
|
|
|
174
164
|
}
|
|
175
165
|
}
|
|
176
166
|
|
|
177
|
-
//
|
|
167
|
+
// Mirrors Node's aliasKeyFormat (lib/internal/crypto/webcrypto.js): for
|
|
168
|
+
// algorithms whose import/export accepts both 'raw' and the disambiguated
|
|
169
|
+
// 'raw-secret' / 'raw-public', collapse the latter to 'raw'. Used per-algorithm
|
|
170
|
+
// — algorithms that demand the disambiguated form (KMAC, AES-OCB,
|
|
171
|
+
// ChaCha20-Poly1305, Argon2*, ML-DSA, ML-KEM) MUST NOT alias.
|
|
172
|
+
function aliasKeyFormat(format: ImportFormat): ImportFormat {
|
|
173
|
+
if (format === 'raw-secret' || format === 'raw-public') return 'raw';
|
|
174
|
+
return format;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const kUncompressedSpkiLength: Record<string, number> = {
|
|
178
|
+
'P-256': 91,
|
|
179
|
+
'P-384': 120,
|
|
180
|
+
'P-521': 158,
|
|
181
|
+
};
|
|
182
|
+
|
|
178
183
|
function ecExportKey(key: CryptoKey, format: KWebCryptoKeyFormat): ArrayBuffer {
|
|
179
184
|
const keyObject = key.keyObject;
|
|
180
185
|
|
|
181
186
|
if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw) {
|
|
182
187
|
return bufferLikeToArrayBuffer(keyObject.handle.exportKey());
|
|
183
188
|
} else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI) {
|
|
184
|
-
const exported =
|
|
185
|
-
|
|
189
|
+
const exported = bufferLikeToArrayBuffer(
|
|
190
|
+
keyObject.export({ format: 'der', type: 'spki' }),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
// WebCrypto requires uncompressed point format for SPKI exports.
|
|
194
|
+
// If the key was imported in compressed form, re-export as uncompressed
|
|
195
|
+
// by reconstructing the point from the JWK x,y coordinates and
|
|
196
|
+
// round-tripping through initECRaw.
|
|
197
|
+
const namedCurve = key.algorithm.namedCurve;
|
|
198
|
+
const expected =
|
|
199
|
+
namedCurve === undefined
|
|
200
|
+
? undefined
|
|
201
|
+
: kUncompressedSpkiLength[namedCurve];
|
|
202
|
+
if (expected !== undefined && exported.byteLength !== expected) {
|
|
203
|
+
const jwk = keyObject.handle.exportJwk({}, false);
|
|
204
|
+
if (!jwk.x || !jwk.y) {
|
|
205
|
+
throw lazyDOMException(
|
|
206
|
+
'Failed to re-export EC public key as uncompressed SPKI',
|
|
207
|
+
'OperationError',
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const x = Buffer.from(jwk.x, 'base64url');
|
|
211
|
+
const y = Buffer.from(jwk.y, 'base64url');
|
|
212
|
+
const raw = new Uint8Array(1 + x.length + y.length);
|
|
213
|
+
raw[0] = 0x04;
|
|
214
|
+
raw.set(x, 1);
|
|
215
|
+
raw.set(y, 1 + x.length);
|
|
216
|
+
const tmp =
|
|
217
|
+
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
218
|
+
const curveAlias =
|
|
219
|
+
kNamedCurveAliases[namedCurve as keyof typeof kNamedCurveAliases];
|
|
220
|
+
if (!tmp.initECRaw(curveAlias, raw.buffer as ArrayBuffer)) {
|
|
221
|
+
throw lazyDOMException(
|
|
222
|
+
'Failed to re-export EC public key as uncompressed SPKI',
|
|
223
|
+
'OperationError',
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return bufferLikeToArrayBuffer(
|
|
227
|
+
tmp.exportKey(KFormatType.DER, KeyEncoding.SPKI),
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return exported;
|
|
186
231
|
} else if (format === KWebCryptoKeyFormat.kWebCryptoKeyFormatPKCS8) {
|
|
187
232
|
const exported = keyObject.export({ format: 'der', type: 'pkcs8' });
|
|
188
233
|
return bufferLikeToArrayBuffer(exported);
|
|
@@ -811,13 +856,22 @@ function kmacSignVerify(
|
|
|
811
856
|
): ArrayBuffer | boolean {
|
|
812
857
|
const { name } = algorithm;
|
|
813
858
|
|
|
814
|
-
|
|
815
|
-
|
|
859
|
+
// KmacParams.outputLength is required per
|
|
860
|
+
// https://wicg.github.io/webcrypto-modern-algos/#KmacParams-dictionary
|
|
861
|
+
// and the rename from `length` (commit ab8dc2b84c2). Mirror Node's
|
|
862
|
+
// mac.js:213-223 by reading `outputLength` (in bits).
|
|
863
|
+
if (typeof algorithm.outputLength !== 'number') {
|
|
864
|
+
throw lazyDOMException(
|
|
865
|
+
`${name}Params.outputLength is required`,
|
|
866
|
+
'OperationError',
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const outputLengthBits = algorithm.outputLength;
|
|
816
870
|
|
|
817
871
|
if (outputLengthBits % 8 !== 0) {
|
|
818
872
|
throw lazyDOMException(
|
|
819
|
-
|
|
820
|
-
'
|
|
873
|
+
`Unsupported ${name}Params outputLength`,
|
|
874
|
+
'NotSupportedError',
|
|
821
875
|
);
|
|
822
876
|
}
|
|
823
877
|
|
|
@@ -862,13 +916,6 @@ async function kmacImportKey(
|
|
|
862
916
|
): Promise<CryptoKey> {
|
|
863
917
|
const { name } = algorithm;
|
|
864
918
|
|
|
865
|
-
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
|
|
866
|
-
throw lazyDOMException(
|
|
867
|
-
`Unsupported key usage for ${name} key`,
|
|
868
|
-
'SyntaxError',
|
|
869
|
-
);
|
|
870
|
-
}
|
|
871
|
-
|
|
872
919
|
let keyObject: KeyObject;
|
|
873
920
|
|
|
874
921
|
if (format === 'jwk') {
|
|
@@ -877,31 +924,51 @@ async function kmacImportKey(
|
|
|
877
924
|
if (!jwk || typeof jwk !== 'object') {
|
|
878
925
|
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
879
926
|
}
|
|
880
|
-
|
|
881
|
-
validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
|
|
882
|
-
|
|
883
927
|
if (jwk.kty !== 'oct') {
|
|
884
|
-
throw lazyDOMException('Invalid JWK
|
|
928
|
+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
|
|
885
929
|
}
|
|
930
|
+
validateJwkStructure(jwk, extractable, keyUsages, 'sig');
|
|
886
931
|
|
|
887
932
|
const expectedAlg = name === 'KMAC128' ? 'K128' : 'K256';
|
|
888
933
|
if (jwk.alg !== undefined && jwk.alg !== expectedAlg) {
|
|
889
934
|
throw lazyDOMException(
|
|
890
|
-
'JWK "alg"
|
|
935
|
+
'JWK "alg" Parameter and algorithm name mismatch',
|
|
891
936
|
'DataError',
|
|
892
937
|
);
|
|
893
938
|
}
|
|
894
939
|
|
|
940
|
+
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
|
|
941
|
+
throw lazyDOMException(
|
|
942
|
+
`Unsupported key usage for ${name} key`,
|
|
943
|
+
'SyntaxError',
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
895
947
|
const handle =
|
|
896
948
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
897
|
-
|
|
898
|
-
|
|
949
|
+
let keyType: KeyType | undefined;
|
|
950
|
+
try {
|
|
951
|
+
keyType = handle.initJwk(jwk, undefined);
|
|
952
|
+
} catch (err) {
|
|
953
|
+
throw lazyDOMException('Invalid keyData', {
|
|
954
|
+
name: 'DataError',
|
|
955
|
+
cause: err,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
899
958
|
if (keyType === undefined || keyType !== 0) {
|
|
900
|
-
throw lazyDOMException('
|
|
959
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
901
960
|
}
|
|
902
961
|
|
|
903
962
|
keyObject = new SecretKeyObject(handle);
|
|
904
|
-
} else if (format === 'raw
|
|
963
|
+
} else if (format === 'raw-secret') {
|
|
964
|
+
// KMAC accepts only the disambiguated 'raw-secret' form (Node mac.js:141-145
|
|
965
|
+
// returns undefined for plain 'raw' when not HMAC).
|
|
966
|
+
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
|
|
967
|
+
throw lazyDOMException(
|
|
968
|
+
`Unsupported key usage for ${name} key`,
|
|
969
|
+
'SyntaxError',
|
|
970
|
+
);
|
|
971
|
+
}
|
|
905
972
|
keyObject = createSecretKey(data as BinaryLike);
|
|
906
973
|
} else {
|
|
907
974
|
throw lazyDOMException(
|
|
@@ -938,7 +1005,6 @@ function rsaImportKey(
|
|
|
938
1005
|
): CryptoKey {
|
|
939
1006
|
const { name } = algorithm;
|
|
940
1007
|
|
|
941
|
-
// Validate usages
|
|
942
1008
|
let checkSet: KeyUsage[];
|
|
943
1009
|
switch (name) {
|
|
944
1010
|
case 'RSASSA-PKCS1-v1_5':
|
|
@@ -951,40 +1017,75 @@ function rsaImportKey(
|
|
|
951
1017
|
default:
|
|
952
1018
|
throw new Error(`Unsupported RSA algorithm: ${name}`);
|
|
953
1019
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1020
|
+
const checkUsages = (): void => {
|
|
1021
|
+
if (hasAnyNotIn(keyUsages, checkSet)) {
|
|
1022
|
+
throw lazyDOMException(
|
|
1023
|
+
`Unsupported key usage for ${name} key`,
|
|
1024
|
+
'SyntaxError',
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
958
1028
|
|
|
959
1029
|
let keyObject: KeyObject;
|
|
960
1030
|
|
|
961
1031
|
if (format === 'jwk') {
|
|
962
1032
|
const jwk = data as JWK;
|
|
963
1033
|
|
|
964
|
-
|
|
1034
|
+
if (!jwk || typeof jwk !== 'object') {
|
|
1035
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
1036
|
+
}
|
|
965
1037
|
if (jwk.kty !== 'RSA') {
|
|
966
|
-
throw
|
|
1038
|
+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
|
|
1039
|
+
}
|
|
1040
|
+
const expectedUse = name === 'RSA-OAEP' ? 'enc' : 'sig';
|
|
1041
|
+
validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
|
|
1042
|
+
checkUsages();
|
|
1043
|
+
|
|
1044
|
+
if (jwk.alg !== undefined) {
|
|
1045
|
+
let jwkContext: HashContext;
|
|
1046
|
+
switch (name) {
|
|
1047
|
+
case 'RSASSA-PKCS1-v1_5':
|
|
1048
|
+
jwkContext = HashContext.JwkRsa;
|
|
1049
|
+
break;
|
|
1050
|
+
case 'RSA-PSS':
|
|
1051
|
+
jwkContext = HashContext.JwkRsaPss;
|
|
1052
|
+
break;
|
|
1053
|
+
default:
|
|
1054
|
+
jwkContext = HashContext.JwkRsaOaep;
|
|
1055
|
+
}
|
|
1056
|
+
const expectedAlg = normalizeHashName(algorithm.hash, jwkContext);
|
|
1057
|
+
if (jwk.alg !== expectedAlg) {
|
|
1058
|
+
throw lazyDOMException(
|
|
1059
|
+
'JWK "alg" does not match the requested algorithm',
|
|
1060
|
+
'DataError',
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
967
1063
|
}
|
|
968
|
-
|
|
969
|
-
validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
|
|
970
1064
|
|
|
971
1065
|
const handle =
|
|
972
1066
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
973
|
-
|
|
974
|
-
|
|
1067
|
+
let keyType: KeyType | undefined;
|
|
1068
|
+
try {
|
|
1069
|
+
keyType = handle.initJwk(jwk, undefined);
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
throw lazyDOMException('Invalid keyData', {
|
|
1072
|
+
name: 'DataError',
|
|
1073
|
+
cause: err,
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
975
1076
|
if (keyType === undefined) {
|
|
976
|
-
throw
|
|
1077
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
977
1078
|
}
|
|
978
1079
|
|
|
979
|
-
|
|
980
|
-
if (keyType === 1) {
|
|
1080
|
+
if (keyType === KeyType.PUBLIC) {
|
|
981
1081
|
keyObject = new PublicKeyObject(handle);
|
|
982
|
-
} else if (keyType ===
|
|
1082
|
+
} else if (keyType === KeyType.PRIVATE) {
|
|
983
1083
|
keyObject = new PrivateKeyObject(handle);
|
|
984
1084
|
} else {
|
|
985
|
-
throw
|
|
1085
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
986
1086
|
}
|
|
987
1087
|
} else if (format === 'spki') {
|
|
1088
|
+
checkUsages();
|
|
988
1089
|
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
989
1090
|
keyObject = KeyObject.createKeyObject(
|
|
990
1091
|
'public',
|
|
@@ -993,6 +1094,7 @@ function rsaImportKey(
|
|
|
993
1094
|
KeyEncoding.SPKI,
|
|
994
1095
|
);
|
|
995
1096
|
} else if (format === 'pkcs8') {
|
|
1097
|
+
checkUsages();
|
|
996
1098
|
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
997
1099
|
keyObject = KeyObject.createKeyObject(
|
|
998
1100
|
'private',
|
|
@@ -1001,7 +1103,10 @@ function rsaImportKey(
|
|
|
1001
1103
|
KeyEncoding.PKCS8,
|
|
1002
1104
|
);
|
|
1003
1105
|
} else {
|
|
1004
|
-
throw
|
|
1106
|
+
throw lazyDOMException(
|
|
1107
|
+
`Unsupported format for ${name} import: ${format}`,
|
|
1108
|
+
'NotSupportedError',
|
|
1109
|
+
);
|
|
1005
1110
|
}
|
|
1006
1111
|
|
|
1007
1112
|
// Get the modulus length from the key and add it to the algorithm
|
|
@@ -1043,33 +1148,30 @@ async function hmacImportKey(
|
|
|
1043
1148
|
extractable: boolean,
|
|
1044
1149
|
keyUsages: KeyUsage[],
|
|
1045
1150
|
): Promise<CryptoKey> {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1151
|
+
const checkUsages = (): void => {
|
|
1152
|
+
if (hasAnyNotIn(keyUsages, ['sign', 'verify'])) {
|
|
1153
|
+
throw new Error('Unsupported key usage for an HMAC key');
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1050
1156
|
|
|
1051
1157
|
let keyObject: KeyObject;
|
|
1052
1158
|
|
|
1053
1159
|
if (format === 'jwk') {
|
|
1054
1160
|
const jwk = data as JWK;
|
|
1055
1161
|
|
|
1056
|
-
// Validate JWK
|
|
1057
1162
|
if (!jwk || typeof jwk !== 'object') {
|
|
1058
1163
|
throw new Error('Invalid keyData');
|
|
1059
1164
|
}
|
|
1060
|
-
|
|
1061
|
-
validateJwkExtAndKeyOps(jwk, extractable, keyUsages);
|
|
1062
|
-
|
|
1063
1165
|
if (jwk.kty !== 'oct') {
|
|
1064
1166
|
throw new Error('Invalid JWK format for HMAC key');
|
|
1065
1167
|
}
|
|
1168
|
+
validateJwkStructure(jwk, extractable, keyUsages, 'sig');
|
|
1169
|
+
checkUsages();
|
|
1066
1170
|
|
|
1067
|
-
// Validate key length if specified
|
|
1068
1171
|
if (algorithm.length !== undefined) {
|
|
1069
1172
|
if (!jwk.k) {
|
|
1070
1173
|
throw new Error('JWK missing key data');
|
|
1071
1174
|
}
|
|
1072
|
-
// Decode to check length
|
|
1073
1175
|
const decoded = SBuffer.from(jwk.k, 'base64');
|
|
1074
1176
|
const keyBitLength = decoded.length * 8;
|
|
1075
1177
|
if (algorithm.length === 0) {
|
|
@@ -1082,17 +1184,29 @@ async function hmacImportKey(
|
|
|
1082
1184
|
|
|
1083
1185
|
const handle =
|
|
1084
1186
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1085
|
-
|
|
1086
|
-
|
|
1187
|
+
let keyType: KeyType | undefined;
|
|
1188
|
+
try {
|
|
1189
|
+
keyType = handle.initJwk(jwk, undefined);
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
throw lazyDOMException('Invalid keyData', {
|
|
1192
|
+
name: 'DataError',
|
|
1193
|
+
cause: err,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1087
1196
|
if (keyType === undefined || keyType !== 0) {
|
|
1088
|
-
throw
|
|
1197
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
1089
1198
|
}
|
|
1090
1199
|
|
|
1091
1200
|
keyObject = new SecretKeyObject(handle);
|
|
1092
|
-
} else if (format === 'raw') {
|
|
1201
|
+
} else if (format === 'raw' || format === 'raw-secret') {
|
|
1202
|
+
// HMAC accepts both 'raw' and 'raw-secret' (Node mac.js:141-145).
|
|
1203
|
+
checkUsages();
|
|
1093
1204
|
keyObject = createSecretKey(data as BinaryLike);
|
|
1094
1205
|
} else {
|
|
1095
|
-
throw
|
|
1206
|
+
throw lazyDOMException(
|
|
1207
|
+
`Unable to import HMAC key with format ${format}`,
|
|
1208
|
+
'NotSupportedError',
|
|
1209
|
+
);
|
|
1096
1210
|
}
|
|
1097
1211
|
|
|
1098
1212
|
// Normalize hash to { name: string } format per WebCrypto spec
|
|
@@ -1115,16 +1229,24 @@ async function aesImportKey(
|
|
|
1115
1229
|
): Promise<CryptoKey> {
|
|
1116
1230
|
const { name, length } = algorithm;
|
|
1117
1231
|
|
|
1118
|
-
// Validate usages
|
|
1119
1232
|
const validUsages: KeyUsage[] = [
|
|
1120
1233
|
'encrypt',
|
|
1121
1234
|
'decrypt',
|
|
1122
1235
|
'wrapKey',
|
|
1123
1236
|
'unwrapKey',
|
|
1124
1237
|
];
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1238
|
+
const checkUsages = (): void => {
|
|
1239
|
+
if (hasAnyNotIn(keyUsages, validUsages)) {
|
|
1240
|
+
throw new Error(`Unsupported key usage for ${name}`);
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
// AES-OCB and ChaCha20-Poly1305 require the disambiguated 'raw-secret' form
|
|
1245
|
+
// and reject 'raw' (Node aes.js:243-249, chacha20_poly1305.js:104-134).
|
|
1246
|
+
// Other AES variants accept both 'raw' and 'raw-secret'.
|
|
1247
|
+
const requiresRawSecret = name === 'AES-OCB' || name === 'ChaCha20-Poly1305';
|
|
1248
|
+
const acceptsRaw =
|
|
1249
|
+
format === 'raw-secret' || (format === 'raw' && !requiresRawSecret);
|
|
1128
1250
|
|
|
1129
1251
|
let keyObject: KeyObject;
|
|
1130
1252
|
let actualLength: number;
|
|
@@ -1132,38 +1254,53 @@ async function aesImportKey(
|
|
|
1132
1254
|
if (format === 'jwk') {
|
|
1133
1255
|
const jwk = data as JWK;
|
|
1134
1256
|
|
|
1135
|
-
// Validate JWK
|
|
1136
1257
|
if (jwk.kty !== 'oct') {
|
|
1137
1258
|
throw new Error('Invalid JWK format for AES key');
|
|
1138
1259
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1260
|
+
validateJwkStructure(jwk, extractable, keyUsages, 'enc');
|
|
1261
|
+
checkUsages();
|
|
1141
1262
|
|
|
1142
1263
|
const handle =
|
|
1143
1264
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1144
|
-
|
|
1145
|
-
|
|
1265
|
+
let keyType: KeyType | undefined;
|
|
1266
|
+
try {
|
|
1267
|
+
keyType = handle.initJwk(jwk, undefined);
|
|
1268
|
+
} catch (err) {
|
|
1269
|
+
throw lazyDOMException('Invalid keyData', {
|
|
1270
|
+
name: 'DataError',
|
|
1271
|
+
cause: err,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1146
1274
|
if (keyType === undefined || keyType !== 0) {
|
|
1147
|
-
throw
|
|
1275
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
1148
1276
|
}
|
|
1149
1277
|
|
|
1150
1278
|
keyObject = new SecretKeyObject(handle);
|
|
1151
1279
|
|
|
1152
|
-
// Get actual key length from imported key
|
|
1153
1280
|
const exported = keyObject.export();
|
|
1154
1281
|
actualLength = exported.byteLength * 8;
|
|
1155
|
-
} else if (
|
|
1282
|
+
} else if (acceptsRaw) {
|
|
1283
|
+
checkUsages();
|
|
1156
1284
|
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
1157
1285
|
actualLength = keyData.byteLength * 8;
|
|
1158
1286
|
|
|
1159
|
-
|
|
1160
|
-
|
|
1287
|
+
if (name === 'ChaCha20-Poly1305') {
|
|
1288
|
+
if (actualLength !== 256) {
|
|
1289
|
+
throw lazyDOMException(
|
|
1290
|
+
'Invalid ChaCha20-Poly1305 key length',
|
|
1291
|
+
'DataError',
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
} else if (![128, 192, 256].includes(actualLength)) {
|
|
1161
1295
|
throw new Error('Invalid AES key length');
|
|
1162
1296
|
}
|
|
1163
1297
|
|
|
1164
1298
|
keyObject = createSecretKey(keyData);
|
|
1165
1299
|
} else {
|
|
1166
|
-
throw
|
|
1300
|
+
throw lazyDOMException(
|
|
1301
|
+
`Unable to import ${name} key with format ${format}`,
|
|
1302
|
+
'NotSupportedError',
|
|
1303
|
+
);
|
|
1167
1304
|
}
|
|
1168
1305
|
|
|
1169
1306
|
// Validate length if specified
|
|
@@ -1190,23 +1327,23 @@ function edImportKey(
|
|
|
1190
1327
|
): CryptoKey {
|
|
1191
1328
|
const { name } = algorithm;
|
|
1192
1329
|
|
|
1193
|
-
// Validate usages
|
|
1194
1330
|
const isX = name === 'X25519' || name === 'X448';
|
|
1195
1331
|
const allowedUsages: KeyUsage[] = isX
|
|
1196
1332
|
? ['deriveKey', 'deriveBits']
|
|
1197
1333
|
: ['sign', 'verify'];
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1334
|
+
const checkUsages = (): void => {
|
|
1335
|
+
if (hasAnyNotIn(keyUsages, allowedUsages)) {
|
|
1336
|
+
throw lazyDOMException(
|
|
1337
|
+
`Unsupported key usage for ${name} key`,
|
|
1338
|
+
'SyntaxError',
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1205
1342
|
|
|
1206
1343
|
let keyObject: KeyObject;
|
|
1207
1344
|
|
|
1208
1345
|
if (format === 'spki') {
|
|
1209
|
-
|
|
1346
|
+
checkUsages();
|
|
1210
1347
|
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
1211
1348
|
keyObject = KeyObject.createKeyObject(
|
|
1212
1349
|
'public',
|
|
@@ -1215,7 +1352,7 @@ function edImportKey(
|
|
|
1215
1352
|
KeyEncoding.SPKI,
|
|
1216
1353
|
);
|
|
1217
1354
|
} else if (format === 'pkcs8') {
|
|
1218
|
-
|
|
1355
|
+
checkUsages();
|
|
1219
1356
|
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
1220
1357
|
keyObject = KeyObject.createKeyObject(
|
|
1221
1358
|
'private',
|
|
@@ -1224,20 +1361,51 @@ function edImportKey(
|
|
|
1224
1361
|
KeyEncoding.PKCS8,
|
|
1225
1362
|
);
|
|
1226
1363
|
} else if (format === 'raw') {
|
|
1227
|
-
|
|
1364
|
+
checkUsages();
|
|
1228
1365
|
const keyData = bufferLikeToArrayBuffer(data as BufferLike);
|
|
1229
1366
|
const handle =
|
|
1230
1367
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1231
|
-
|
|
1232
|
-
// Raw public keys are just the key bytes
|
|
1233
|
-
handle.init(1, keyData); // 1 = public key type
|
|
1368
|
+
handle.init(1, keyData);
|
|
1234
1369
|
keyObject = new PublicKeyObject(handle);
|
|
1235
1370
|
} else if (format === 'jwk') {
|
|
1236
1371
|
const jwkData = data as JWK;
|
|
1237
|
-
|
|
1372
|
+
if (!jwkData || typeof jwkData !== 'object') {
|
|
1373
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
1374
|
+
}
|
|
1375
|
+
if (jwkData.kty !== 'OKP') {
|
|
1376
|
+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
|
|
1377
|
+
}
|
|
1378
|
+
const expectedUse = isX ? 'enc' : 'sig';
|
|
1379
|
+
validateJwkStructure(jwkData, extractable, keyUsages, expectedUse);
|
|
1380
|
+
|
|
1381
|
+
if (jwkData.crv !== name) {
|
|
1382
|
+
throw lazyDOMException(
|
|
1383
|
+
'JWK "crv" Parameter and algorithm name mismatch',
|
|
1384
|
+
'DataError',
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
if (!isX && jwkData.alg !== undefined) {
|
|
1389
|
+
if (jwkData.alg !== name && jwkData.alg !== 'EdDSA') {
|
|
1390
|
+
throw lazyDOMException(
|
|
1391
|
+
'JWK "alg" does not match the requested algorithm',
|
|
1392
|
+
'DataError',
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
checkUsages();
|
|
1238
1398
|
const handle =
|
|
1239
1399
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1240
|
-
|
|
1400
|
+
let keyType: KeyType | undefined;
|
|
1401
|
+
try {
|
|
1402
|
+
keyType = handle.initJwk(jwkData);
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
throw lazyDOMException('Invalid JWK data', {
|
|
1405
|
+
name: 'DataError',
|
|
1406
|
+
cause: err,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1241
1409
|
if (keyType === undefined) {
|
|
1242
1410
|
throw lazyDOMException('Invalid JWK data', 'DataError');
|
|
1243
1411
|
}
|
|
@@ -1256,42 +1424,133 @@ function edImportKey(
|
|
|
1256
1424
|
return new CryptoKey(keyObject, { name }, keyUsages, extractable);
|
|
1257
1425
|
}
|
|
1258
1426
|
|
|
1427
|
+
// Lengths (in bytes) of seedless ML-DSA / ML-KEM PKCS#8 encodings. A PKCS#8
|
|
1428
|
+
// blob of exactly this length contains only the expanded private key with no
|
|
1429
|
+
// seed; Node rejects these to keep cross-implementation interop intact.
|
|
1430
|
+
// Refs: node lib/internal/crypto/ml_dsa.js (mlDsaImportKey, pkcs8 case)
|
|
1431
|
+
// node lib/internal/crypto/ml_kem.js (mlKemImportKey, pkcs8 case)
|
|
1432
|
+
export const PQC_SEEDLESS_PKCS8_LENGTHS: Readonly<Record<string, number>> = {
|
|
1433
|
+
'ML-DSA-44': 2588,
|
|
1434
|
+
'ML-DSA-65': 4060,
|
|
1435
|
+
'ML-DSA-87': 4924,
|
|
1436
|
+
'ML-KEM-512': 1660,
|
|
1437
|
+
'ML-KEM-768': 2428,
|
|
1438
|
+
'ML-KEM-1024': 3196,
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
// Map from PQC algorithm name to display family. Used to render the
|
|
1442
|
+
// import-rejection error message in the same form Node emits.
|
|
1443
|
+
const PQC_FAMILY: Readonly<Record<string, 'ML-DSA' | 'ML-KEM' | 'SLH-DSA'>> = {
|
|
1444
|
+
'ML-DSA-44': 'ML-DSA',
|
|
1445
|
+
'ML-DSA-65': 'ML-DSA',
|
|
1446
|
+
'ML-DSA-87': 'ML-DSA',
|
|
1447
|
+
'ML-KEM-512': 'ML-KEM',
|
|
1448
|
+
'ML-KEM-768': 'ML-KEM',
|
|
1449
|
+
'ML-KEM-1024': 'ML-KEM',
|
|
1450
|
+
'SLH-DSA-SHA2-128s': 'SLH-DSA',
|
|
1451
|
+
'SLH-DSA-SHA2-128f': 'SLH-DSA',
|
|
1452
|
+
'SLH-DSA-SHA2-192s': 'SLH-DSA',
|
|
1453
|
+
'SLH-DSA-SHA2-192f': 'SLH-DSA',
|
|
1454
|
+
'SLH-DSA-SHA2-256s': 'SLH-DSA',
|
|
1455
|
+
'SLH-DSA-SHA2-256f': 'SLH-DSA',
|
|
1456
|
+
'SLH-DSA-SHAKE-128s': 'SLH-DSA',
|
|
1457
|
+
'SLH-DSA-SHAKE-128f': 'SLH-DSA',
|
|
1458
|
+
'SLH-DSA-SHAKE-192s': 'SLH-DSA',
|
|
1459
|
+
'SLH-DSA-SHAKE-192f': 'SLH-DSA',
|
|
1460
|
+
'SLH-DSA-SHAKE-256s': 'SLH-DSA',
|
|
1461
|
+
'SLH-DSA-SHAKE-256f': 'SLH-DSA',
|
|
1462
|
+
};
|
|
1463
|
+
|
|
1259
1464
|
function pqcImportKeyObject(
|
|
1260
1465
|
format: ImportFormat,
|
|
1261
|
-
data: BufferLike,
|
|
1466
|
+
data: BufferLike | JWK,
|
|
1262
1467
|
name: string,
|
|
1263
|
-
): KeyObject {
|
|
1468
|
+
): { keyObject: KeyObject; isPublic: boolean } {
|
|
1264
1469
|
if (format === 'spki') {
|
|
1265
|
-
return
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1470
|
+
return {
|
|
1471
|
+
keyObject: KeyObject.createKeyObject(
|
|
1472
|
+
'public',
|
|
1473
|
+
bufferLikeToArrayBuffer(data as BufferLike),
|
|
1474
|
+
KFormatType.DER,
|
|
1475
|
+
KeyEncoding.SPKI,
|
|
1476
|
+
),
|
|
1477
|
+
isPublic: true,
|
|
1478
|
+
};
|
|
1271
1479
|
} else if (format === 'pkcs8') {
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
)
|
|
1278
|
-
|
|
1480
|
+
const ab = bufferLikeToArrayBuffer(data as BufferLike);
|
|
1481
|
+
const family = PQC_FAMILY[name];
|
|
1482
|
+
if (
|
|
1483
|
+
family !== undefined &&
|
|
1484
|
+
ab.byteLength === PQC_SEEDLESS_PKCS8_LENGTHS[name]
|
|
1485
|
+
) {
|
|
1486
|
+
throw lazyDOMException(
|
|
1487
|
+
`Importing an ${family} PKCS#8 key without a seed is not supported`,
|
|
1488
|
+
'NotSupportedError',
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
keyObject: KeyObject.createKeyObject(
|
|
1493
|
+
'private',
|
|
1494
|
+
ab,
|
|
1495
|
+
KFormatType.DER,
|
|
1496
|
+
KeyEncoding.PKCS8,
|
|
1497
|
+
),
|
|
1498
|
+
isPublic: false,
|
|
1499
|
+
};
|
|
1500
|
+
} else if (format === 'raw-public') {
|
|
1501
|
+
// ML-DSA / ML-KEM reject plain 'raw' — only 'raw-public' is accepted for
|
|
1502
|
+
// public-key import (Node webcrypto.js:493-499, 506-511).
|
|
1279
1503
|
const handle =
|
|
1280
1504
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1281
|
-
if (
|
|
1505
|
+
if (
|
|
1506
|
+
!handle.initPqcRaw(
|
|
1507
|
+
name,
|
|
1508
|
+
bufferLikeToArrayBuffer(data as BufferLike),
|
|
1509
|
+
true,
|
|
1510
|
+
)
|
|
1511
|
+
) {
|
|
1282
1512
|
throw lazyDOMException(
|
|
1283
1513
|
`Failed to import ${name} raw public key`,
|
|
1284
1514
|
'DataError',
|
|
1285
1515
|
);
|
|
1286
1516
|
}
|
|
1287
|
-
return new PublicKeyObject(handle);
|
|
1517
|
+
return { keyObject: new PublicKeyObject(handle), isPublic: true };
|
|
1288
1518
|
} else if (format === 'raw-seed') {
|
|
1289
1519
|
const handle =
|
|
1290
1520
|
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1291
|
-
if (
|
|
1521
|
+
if (
|
|
1522
|
+
!handle.initPqcRaw(
|
|
1523
|
+
name,
|
|
1524
|
+
bufferLikeToArrayBuffer(data as BufferLike),
|
|
1525
|
+
false,
|
|
1526
|
+
)
|
|
1527
|
+
) {
|
|
1292
1528
|
throw lazyDOMException(`Failed to import ${name} raw seed`, 'DataError');
|
|
1293
1529
|
}
|
|
1294
|
-
return new PrivateKeyObject(handle);
|
|
1530
|
+
return { keyObject: new PrivateKeyObject(handle), isPublic: false };
|
|
1531
|
+
} else if (format === 'jwk') {
|
|
1532
|
+
const jwkData = data as JWK;
|
|
1533
|
+
const isPublic = jwkData.priv === undefined;
|
|
1534
|
+
const handle =
|
|
1535
|
+
NitroModules.createHybridObject<KeyObjectHandle>('KeyObjectHandle');
|
|
1536
|
+
let keyType: KeyType | undefined;
|
|
1537
|
+
try {
|
|
1538
|
+
keyType = handle.initJwk(jwkData);
|
|
1539
|
+
} catch (err) {
|
|
1540
|
+
throw lazyDOMException('Invalid JWK data', {
|
|
1541
|
+
name: 'DataError',
|
|
1542
|
+
cause: err,
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
if (keyType === undefined) {
|
|
1546
|
+
throw lazyDOMException('Invalid JWK data', 'DataError');
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
keyObject: isPublic
|
|
1550
|
+
? new PublicKeyObject(handle)
|
|
1551
|
+
: new PrivateKeyObject(handle),
|
|
1552
|
+
isPublic,
|
|
1553
|
+
};
|
|
1295
1554
|
}
|
|
1296
1555
|
throw lazyDOMException(
|
|
1297
1556
|
`Unsupported format for ${name} import: ${format}`,
|
|
@@ -1299,39 +1558,109 @@ function pqcImportKeyObject(
|
|
|
1299
1558
|
);
|
|
1300
1559
|
}
|
|
1301
1560
|
|
|
1561
|
+
// Per WebCrypto AKP JWK rules, public-vs-private is determined by the presence
|
|
1562
|
+
// of `priv`. For binary formats it follows from the format itself.
|
|
1563
|
+
function pqcIsPublicImport(
|
|
1564
|
+
format: ImportFormat,
|
|
1565
|
+
data: BufferLike | JWK,
|
|
1566
|
+
): boolean {
|
|
1567
|
+
if (format === 'jwk') {
|
|
1568
|
+
return (
|
|
1569
|
+
typeof data === 'object' &&
|
|
1570
|
+
data !== null &&
|
|
1571
|
+
(data as JWK).priv === undefined
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
return format === 'spki' || format === 'raw-public';
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function validatePqcJwk(
|
|
1578
|
+
data: BufferLike | JWK,
|
|
1579
|
+
name: string,
|
|
1580
|
+
extractable: boolean,
|
|
1581
|
+
keyUsages: KeyUsage[],
|
|
1582
|
+
expectedUse: 'sig' | 'enc',
|
|
1583
|
+
): void {
|
|
1584
|
+
if (typeof data !== 'object' || data === null) {
|
|
1585
|
+
throw lazyDOMException('Invalid keyData', 'DataError');
|
|
1586
|
+
}
|
|
1587
|
+
const jwk = data as JWK;
|
|
1588
|
+
if (jwk.kty !== 'AKP') {
|
|
1589
|
+
throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError');
|
|
1590
|
+
}
|
|
1591
|
+
validateJwkStructure(jwk, extractable, keyUsages, expectedUse);
|
|
1592
|
+
if (jwk.alg !== name) {
|
|
1593
|
+
throw lazyDOMException(
|
|
1594
|
+
'JWK "alg" Parameter and algorithm name mismatch',
|
|
1595
|
+
'DataError',
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Validates that `format` is one of the formats PQC algorithms accept; rejects
|
|
1601
|
+
// plain 'raw' early so the format error wins over usage-based errors.
|
|
1602
|
+
function validatePqcFormat(format: ImportFormat, name: string): void {
|
|
1603
|
+
if (
|
|
1604
|
+
format !== 'spki' &&
|
|
1605
|
+
format !== 'pkcs8' &&
|
|
1606
|
+
format !== 'raw-public' &&
|
|
1607
|
+
format !== 'raw-seed' &&
|
|
1608
|
+
format !== 'jwk'
|
|
1609
|
+
) {
|
|
1610
|
+
throw lazyDOMException(
|
|
1611
|
+
`Unsupported format for ${name} import: ${format}`,
|
|
1612
|
+
'NotSupportedError',
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1302
1617
|
function mldsaImportKey(
|
|
1303
1618
|
format: ImportFormat,
|
|
1304
|
-
data: BufferLike,
|
|
1619
|
+
data: BufferLike | JWK,
|
|
1305
1620
|
algorithm: SubtleAlgorithm,
|
|
1306
1621
|
extractable: boolean,
|
|
1307
1622
|
keyUsages: KeyUsage[],
|
|
1308
1623
|
): CryptoKey {
|
|
1309
1624
|
const { name } = algorithm;
|
|
1310
|
-
|
|
1311
|
-
if (
|
|
1625
|
+
validatePqcFormat(format, name);
|
|
1626
|
+
if (format === 'jwk') {
|
|
1627
|
+
validatePqcJwk(data, name, extractable, keyUsages, 'sig');
|
|
1628
|
+
}
|
|
1629
|
+
const isPublic = pqcIsPublicImport(format, data);
|
|
1630
|
+
if (hasAnyNotIn(keyUsages, isPublic ? ['verify'] : ['sign'])) {
|
|
1312
1631
|
throw lazyDOMException(
|
|
1313
1632
|
`Unsupported key usage for ${name} key`,
|
|
1314
1633
|
'SyntaxError',
|
|
1315
1634
|
);
|
|
1316
1635
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1636
|
+
const { keyObject } = pqcImportKeyObject(format, data, name);
|
|
1637
|
+
return new CryptoKey(keyObject, { name }, keyUsages, extractable);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function slhdsaImportKey(
|
|
1641
|
+
format: ImportFormat,
|
|
1642
|
+
data: BufferLike | JWK,
|
|
1643
|
+
algorithm: SubtleAlgorithm,
|
|
1644
|
+
extractable: boolean,
|
|
1645
|
+
keyUsages: KeyUsage[],
|
|
1646
|
+
): CryptoKey {
|
|
1647
|
+
return mldsaImportKey(format, data, algorithm, extractable, keyUsages);
|
|
1323
1648
|
}
|
|
1324
1649
|
|
|
1325
1650
|
function mlkemImportKey(
|
|
1326
1651
|
format: ImportFormat,
|
|
1327
|
-
data: BufferLike,
|
|
1652
|
+
data: BufferLike | JWK,
|
|
1328
1653
|
algorithm: SubtleAlgorithm,
|
|
1329
1654
|
extractable: boolean,
|
|
1330
1655
|
keyUsages: KeyUsage[],
|
|
1331
1656
|
): CryptoKey {
|
|
1332
1657
|
const { name } = algorithm;
|
|
1333
|
-
|
|
1334
|
-
|
|
1658
|
+
validatePqcFormat(format, name);
|
|
1659
|
+
if (format === 'jwk') {
|
|
1660
|
+
validatePqcJwk(data, name, extractable, keyUsages, 'enc');
|
|
1661
|
+
}
|
|
1662
|
+
const isPublic = pqcIsPublicImport(format, data);
|
|
1663
|
+
const allowedUsages: KeyUsage[] = isPublic
|
|
1335
1664
|
? ['encapsulateBits', 'encapsulateKey']
|
|
1336
1665
|
: ['decapsulateBits', 'decapsulateKey'];
|
|
1337
1666
|
if (hasAnyNotIn(keyUsages, allowedUsages)) {
|
|
@@ -1340,12 +1669,8 @@ function mlkemImportKey(
|
|
|
1340
1669
|
'SyntaxError',
|
|
1341
1670
|
);
|
|
1342
1671
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
{ name },
|
|
1346
|
-
keyUsages,
|
|
1347
|
-
extractable,
|
|
1348
|
-
);
|
|
1672
|
+
const { keyObject } = pqcImportKeyObject(format, data, name);
|
|
1673
|
+
return new CryptoKey(keyObject, { name }, keyUsages, extractable);
|
|
1349
1674
|
}
|
|
1350
1675
|
|
|
1351
1676
|
const exportKeySpki = async (
|
|
@@ -1387,8 +1712,21 @@ const exportKeySpki = async (
|
|
|
1387
1712
|
case 'ML-DSA-65':
|
|
1388
1713
|
// Fall through
|
|
1389
1714
|
case 'ML-DSA-87':
|
|
1715
|
+
// Fall through
|
|
1716
|
+
case 'SLH-DSA-SHA2-128s':
|
|
1717
|
+
case 'SLH-DSA-SHA2-128f':
|
|
1718
|
+
case 'SLH-DSA-SHA2-192s':
|
|
1719
|
+
case 'SLH-DSA-SHA2-192f':
|
|
1720
|
+
case 'SLH-DSA-SHA2-256s':
|
|
1721
|
+
case 'SLH-DSA-SHA2-256f':
|
|
1722
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
1723
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
1724
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
1725
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
1726
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
1727
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
1390
1728
|
if (key.type === 'public') {
|
|
1391
|
-
// Export ML-DSA key in SPKI DER format
|
|
1729
|
+
// Export ML-DSA / SLH-DSA key in SPKI DER format
|
|
1392
1730
|
return bufferLikeToArrayBuffer(
|
|
1393
1731
|
key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.SPKI),
|
|
1394
1732
|
);
|
|
@@ -1451,18 +1789,40 @@ const exportKeyPkcs8 = async (
|
|
|
1451
1789
|
case 'ML-DSA-65':
|
|
1452
1790
|
// Fall through
|
|
1453
1791
|
case 'ML-DSA-87':
|
|
1454
|
-
|
|
1455
|
-
// Export ML-DSA key in PKCS8 DER format
|
|
1456
|
-
return bufferLikeToArrayBuffer(
|
|
1457
|
-
key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
break;
|
|
1792
|
+
// Fall through
|
|
1461
1793
|
case 'ML-KEM-512':
|
|
1462
1794
|
// Fall through
|
|
1463
1795
|
case 'ML-KEM-768':
|
|
1464
1796
|
// Fall through
|
|
1465
1797
|
case 'ML-KEM-1024':
|
|
1798
|
+
if (key.type === 'private') {
|
|
1799
|
+
const ab = bufferLikeToArrayBuffer(
|
|
1800
|
+
key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
|
|
1801
|
+
);
|
|
1802
|
+
// 22 bytes of PKCS#8 ASN.1 + seed (32 ML-DSA, 64 ML-KEM). Guards
|
|
1803
|
+
// against a seedless KeyObject that was wrapped via toCryptoKey.
|
|
1804
|
+
const expected = key.algorithm.name.startsWith('ML-DSA') ? 54 : 86;
|
|
1805
|
+
if (ab.byteLength !== expected) {
|
|
1806
|
+
throw lazyDOMException(
|
|
1807
|
+
'The operation failed for an operation-specific reason',
|
|
1808
|
+
'OperationError',
|
|
1809
|
+
);
|
|
1810
|
+
}
|
|
1811
|
+
return ab;
|
|
1812
|
+
}
|
|
1813
|
+
break;
|
|
1814
|
+
case 'SLH-DSA-SHA2-128s':
|
|
1815
|
+
case 'SLH-DSA-SHA2-128f':
|
|
1816
|
+
case 'SLH-DSA-SHA2-192s':
|
|
1817
|
+
case 'SLH-DSA-SHA2-192f':
|
|
1818
|
+
case 'SLH-DSA-SHA2-256s':
|
|
1819
|
+
case 'SLH-DSA-SHA2-256f':
|
|
1820
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
1821
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
1822
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
1823
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
1824
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
1825
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
1466
1826
|
if (key.type === 'private') {
|
|
1467
1827
|
return bufferLikeToArrayBuffer(
|
|
1468
1828
|
key.keyObject.handle.exportKey(KFormatType.DER, KeyEncoding.PKCS8),
|
|
@@ -1476,79 +1836,101 @@ const exportKeyPkcs8 = async (
|
|
|
1476
1836
|
);
|
|
1477
1837
|
};
|
|
1478
1838
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1839
|
+
// Mirrors Node's export key matrix (lib/internal/crypto/webcrypto.js
|
|
1840
|
+
// exportKeyRawSecret / exportKeyRawPublic, lines 472-563):
|
|
1841
|
+
//
|
|
1842
|
+
// raw — AES-CTR/CBC/GCM/KW + HMAC (secret); ECDSA/ECDH/Ed/X (public)
|
|
1843
|
+
// raw-secret — AES-CTR/CBC/GCM/KW + HMAC + AES-OCB + KMAC + ChaCha20-Poly1305
|
|
1844
|
+
// raw-public — ECDSA/ECDH + Ed/X + ML-DSA + ML-KEM (public)
|
|
1845
|
+
const exportKeyRaw = (
|
|
1846
|
+
key: CryptoKey,
|
|
1847
|
+
format: 'raw' | 'raw-secret' | 'raw-public',
|
|
1848
|
+
): ArrayBuffer => {
|
|
1849
|
+
const name = key.algorithm.name;
|
|
1850
|
+
const isPublic = key.type === 'public';
|
|
1851
|
+
const isSecret = key.type === 'secret';
|
|
1852
|
+
|
|
1853
|
+
const exportSecret = (): ArrayBuffer => {
|
|
1854
|
+
const exported = key.keyObject.export();
|
|
1855
|
+
return exported.buffer.slice(
|
|
1856
|
+
exported.byteOffset,
|
|
1857
|
+
exported.byteOffset + exported.byteLength,
|
|
1858
|
+
) as ArrayBuffer;
|
|
1859
|
+
};
|
|
1860
|
+
const exportRawPublic = (): ArrayBuffer =>
|
|
1861
|
+
bufferLikeToArrayBuffer(key.keyObject.handle.exportKey());
|
|
1862
|
+
|
|
1863
|
+
const fail = (): never => {
|
|
1864
|
+
throw lazyDOMException(
|
|
1865
|
+
`Unable to export ${name} ${key.type} key using ${format} format`,
|
|
1866
|
+
'NotSupportedError',
|
|
1867
|
+
);
|
|
1868
|
+
};
|
|
1869
|
+
|
|
1870
|
+
// Symmetric: AES-CTR/CBC/GCM/KW and HMAC accept both 'raw' and 'raw-secret';
|
|
1871
|
+
// AES-OCB / KMAC* / ChaCha20-Poly1305 only 'raw-secret'.
|
|
1872
|
+
switch (name) {
|
|
1873
|
+
case 'AES-CTR':
|
|
1874
|
+
case 'AES-CBC':
|
|
1875
|
+
case 'AES-GCM':
|
|
1876
|
+
case 'AES-KW':
|
|
1877
|
+
case 'HMAC':
|
|
1878
|
+
if (!isSecret) return fail();
|
|
1879
|
+
if (format === 'raw' || format === 'raw-secret') return exportSecret();
|
|
1880
|
+
return fail();
|
|
1881
|
+
case 'AES-OCB':
|
|
1882
|
+
case 'KMAC128':
|
|
1883
|
+
case 'KMAC256':
|
|
1884
|
+
case 'ChaCha20-Poly1305':
|
|
1885
|
+
if (!isSecret) return fail();
|
|
1886
|
+
if (format === 'raw-secret') return exportSecret();
|
|
1887
|
+
return fail();
|
|
1481
1888
|
case 'ECDSA':
|
|
1482
|
-
// Fall through
|
|
1483
1889
|
case 'ECDH':
|
|
1484
|
-
if (
|
|
1890
|
+
if (!isPublic) return fail();
|
|
1891
|
+
if (format === 'raw' || format === 'raw-public') {
|
|
1485
1892
|
return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatRaw);
|
|
1486
1893
|
}
|
|
1487
|
-
|
|
1894
|
+
return fail();
|
|
1488
1895
|
case 'Ed25519':
|
|
1489
|
-
// Fall through
|
|
1490
1896
|
case 'Ed448':
|
|
1491
|
-
// Fall through
|
|
1492
1897
|
case 'X25519':
|
|
1493
|
-
// Fall through
|
|
1494
1898
|
case 'X448':
|
|
1495
|
-
if (
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
}
|
|
1499
|
-
break;
|
|
1500
|
-
case 'ML-KEM-512':
|
|
1501
|
-
// Fall through
|
|
1502
|
-
case 'ML-KEM-768':
|
|
1503
|
-
// Fall through
|
|
1504
|
-
case 'ML-KEM-1024':
|
|
1505
|
-
// Fall through
|
|
1899
|
+
if (!isPublic) return fail();
|
|
1900
|
+
if (format === 'raw' || format === 'raw-public') return exportRawPublic();
|
|
1901
|
+
return fail();
|
|
1506
1902
|
case 'ML-DSA-44':
|
|
1507
|
-
// Fall through
|
|
1508
1903
|
case 'ML-DSA-65':
|
|
1509
|
-
|
|
1510
|
-
case 'ML-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
case '
|
|
1517
|
-
|
|
1518
|
-
case '
|
|
1519
|
-
|
|
1520
|
-
case '
|
|
1521
|
-
|
|
1522
|
-
case '
|
|
1523
|
-
|
|
1524
|
-
case '
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
const exported = key.keyObject.export();
|
|
1534
|
-
// Convert Buffer to ArrayBuffer
|
|
1535
|
-
return exported.buffer.slice(
|
|
1536
|
-
exported.byteOffset,
|
|
1537
|
-
exported.byteOffset + exported.byteLength,
|
|
1538
|
-
);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
|
|
1542
|
-
throw lazyDOMException(
|
|
1543
|
-
`Unable to export a raw ${key.algorithm.name} ${key.type} key`,
|
|
1544
|
-
'InvalidAccessError',
|
|
1545
|
-
);
|
|
1904
|
+
case 'ML-DSA-87':
|
|
1905
|
+
case 'ML-KEM-512':
|
|
1906
|
+
case 'ML-KEM-768':
|
|
1907
|
+
case 'ML-KEM-1024':
|
|
1908
|
+
case 'SLH-DSA-SHA2-128s':
|
|
1909
|
+
case 'SLH-DSA-SHA2-128f':
|
|
1910
|
+
case 'SLH-DSA-SHA2-192s':
|
|
1911
|
+
case 'SLH-DSA-SHA2-192f':
|
|
1912
|
+
case 'SLH-DSA-SHA2-256s':
|
|
1913
|
+
case 'SLH-DSA-SHA2-256f':
|
|
1914
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
1915
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
1916
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
1917
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
1918
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
1919
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
1920
|
+
// ML-DSA / ML-KEM / SLH-DSA keys do not recognize plain 'raw' (Node
|
|
1921
|
+
// webcrypto.js lines 488-510).
|
|
1922
|
+
if (!isPublic) return fail();
|
|
1923
|
+
if (format === 'raw-public') return exportRawPublic();
|
|
1924
|
+
return fail();
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return fail();
|
|
1546
1928
|
};
|
|
1547
1929
|
|
|
1548
1930
|
const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => {
|
|
1549
1931
|
const jwk = key.keyObject.handle.exportJwk(
|
|
1550
1932
|
{
|
|
1551
|
-
key_ops: key.usages,
|
|
1933
|
+
key_ops: [...key.usages],
|
|
1552
1934
|
ext: key.extractable,
|
|
1553
1935
|
},
|
|
1554
1936
|
true,
|
|
@@ -1585,6 +1967,31 @@ const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => {
|
|
|
1585
1967
|
// Fall through
|
|
1586
1968
|
case 'X448':
|
|
1587
1969
|
return jwk;
|
|
1970
|
+
case 'ML-DSA-44':
|
|
1971
|
+
// Fall through
|
|
1972
|
+
case 'ML-DSA-65':
|
|
1973
|
+
// Fall through
|
|
1974
|
+
case 'ML-DSA-87':
|
|
1975
|
+
// Fall through
|
|
1976
|
+
case 'ML-KEM-512':
|
|
1977
|
+
// Fall through
|
|
1978
|
+
case 'ML-KEM-768':
|
|
1979
|
+
// Fall through
|
|
1980
|
+
case 'ML-KEM-1024':
|
|
1981
|
+
// Fall through
|
|
1982
|
+
case 'SLH-DSA-SHA2-128s':
|
|
1983
|
+
case 'SLH-DSA-SHA2-128f':
|
|
1984
|
+
case 'SLH-DSA-SHA2-192s':
|
|
1985
|
+
case 'SLH-DSA-SHA2-192f':
|
|
1986
|
+
case 'SLH-DSA-SHA2-256s':
|
|
1987
|
+
case 'SLH-DSA-SHA2-256f':
|
|
1988
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
1989
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
1990
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
1991
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
1992
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
1993
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
1994
|
+
return jwk;
|
|
1588
1995
|
case 'AES-CTR':
|
|
1589
1996
|
// Fall through
|
|
1590
1997
|
case 'AES-CBC':
|
|
@@ -1614,7 +2021,11 @@ const exportKeyJWK = (key: CryptoKey): ArrayBuffer | unknown => {
|
|
|
1614
2021
|
);
|
|
1615
2022
|
};
|
|
1616
2023
|
|
|
1617
|
-
|
|
2024
|
+
// PBKDF2 import. Mirrors Node's importGenericSecretKey ordering
|
|
2025
|
+
// (keys.js:945-971): extractable → usage → format → length. Callers pre-alias
|
|
2026
|
+
// 'raw-secret' / 'raw-public' to 'raw' via aliasKeyFormat
|
|
2027
|
+
// (webcrypto.js:798-808).
|
|
2028
|
+
const pbkdf2ImportKey = async (
|
|
1618
2029
|
{ name, length }: SubtleAlgorithm,
|
|
1619
2030
|
format: ImportFormat,
|
|
1620
2031
|
keyData: BufferLike | BinaryLike,
|
|
@@ -1622,33 +2033,70 @@ const importGenericSecretKey = async (
|
|
|
1622
2033
|
keyUsages: KeyUsage[],
|
|
1623
2034
|
): Promise<CryptoKey> => {
|
|
1624
2035
|
if (extractable) {
|
|
1625
|
-
throw
|
|
2036
|
+
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
|
|
1626
2037
|
}
|
|
1627
2038
|
if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
|
|
1628
|
-
throw
|
|
2039
|
+
throw lazyDOMException(
|
|
2040
|
+
`Unsupported key usage for a ${name} key`,
|
|
2041
|
+
'SyntaxError',
|
|
2042
|
+
);
|
|
2043
|
+
}
|
|
2044
|
+
if (format !== 'raw') {
|
|
2045
|
+
throw lazyDOMException(
|
|
2046
|
+
`Unable to import ${name} key with format ${format}`,
|
|
2047
|
+
'NotSupportedError',
|
|
2048
|
+
);
|
|
1629
2049
|
}
|
|
1630
2050
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
2051
|
+
const checkLength =
|
|
2052
|
+
typeof keyData === 'string' || SBuffer.isBuffer(keyData)
|
|
2053
|
+
? keyData.length * 8
|
|
2054
|
+
: keyData.byteLength * 8;
|
|
2055
|
+
if (length !== undefined && length !== checkLength) {
|
|
2056
|
+
throw lazyDOMException('Invalid key length', 'DataError');
|
|
2057
|
+
}
|
|
1636
2058
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
: keyData.byteLength * 8;
|
|
2059
|
+
const keyObject = createSecretKey(keyData as BinaryLike);
|
|
2060
|
+
return new CryptoKey(keyObject, { name }, keyUsages, false);
|
|
2061
|
+
};
|
|
1641
2062
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2063
|
+
// Argon2 import. Node gates the format at the dispatcher level — only
|
|
2064
|
+
// 'raw-secret' enters importGenericSecretKey (webcrypto.js:813-822). To match
|
|
2065
|
+
// that, format is the first check here; remaining ordering matches Node's
|
|
2066
|
+
// importGenericSecretKey.
|
|
2067
|
+
const argon2ImportKey = async (
|
|
2068
|
+
{ name, length }: SubtleAlgorithm,
|
|
2069
|
+
format: ImportFormat,
|
|
2070
|
+
keyData: BufferLike | BinaryLike,
|
|
2071
|
+
extractable: boolean,
|
|
2072
|
+
keyUsages: KeyUsage[],
|
|
2073
|
+
): Promise<CryptoKey> => {
|
|
2074
|
+
if (format !== 'raw-secret') {
|
|
2075
|
+
throw lazyDOMException(
|
|
2076
|
+
`Unable to import ${name} key with format ${format}`,
|
|
2077
|
+
'NotSupportedError',
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
if (extractable) {
|
|
2081
|
+
throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError');
|
|
2082
|
+
}
|
|
2083
|
+
if (hasAnyNotIn(keyUsages, ['deriveKey', 'deriveBits'])) {
|
|
2084
|
+
throw lazyDOMException(
|
|
2085
|
+
`Unsupported key usage for a ${name} key`,
|
|
2086
|
+
'SyntaxError',
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
1645
2089
|
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2090
|
+
const checkLength =
|
|
2091
|
+
typeof keyData === 'string' || SBuffer.isBuffer(keyData)
|
|
2092
|
+
? keyData.length * 8
|
|
2093
|
+
: keyData.byteLength * 8;
|
|
2094
|
+
if (length !== undefined && length !== checkLength) {
|
|
2095
|
+
throw lazyDOMException('Invalid key length', 'DataError');
|
|
1649
2096
|
}
|
|
1650
2097
|
|
|
1651
|
-
|
|
2098
|
+
const keyObject = createSecretKey(keyData as BinaryLike);
|
|
2099
|
+
return new CryptoKey(keyObject, { name }, keyUsages, false);
|
|
1652
2100
|
};
|
|
1653
2101
|
|
|
1654
2102
|
const hkdfImportKey = async (
|
|
@@ -1898,7 +2346,11 @@ const signVerify = (
|
|
|
1898
2346
|
const usage: Operation = signature === undefined ? 'sign' : 'verify';
|
|
1899
2347
|
algorithm = normalizeAlgorithm(algorithm, usage);
|
|
1900
2348
|
|
|
1901
|
-
if (
|
|
2349
|
+
if (algorithm.name !== key.algorithm.name) {
|
|
2350
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
if (!key.usages.includes(usage)) {
|
|
1902
2354
|
throw lazyDOMException(
|
|
1903
2355
|
`Unable to use this key to ${usage}`,
|
|
1904
2356
|
'InvalidAccessError',
|
|
@@ -1920,6 +2372,18 @@ const signVerify = (
|
|
|
1920
2372
|
case 'ML-DSA-44':
|
|
1921
2373
|
case 'ML-DSA-65':
|
|
1922
2374
|
case 'ML-DSA-87':
|
|
2375
|
+
case 'SLH-DSA-SHA2-128s':
|
|
2376
|
+
case 'SLH-DSA-SHA2-128f':
|
|
2377
|
+
case 'SLH-DSA-SHA2-192s':
|
|
2378
|
+
case 'SLH-DSA-SHA2-192f':
|
|
2379
|
+
case 'SLH-DSA-SHA2-256s':
|
|
2380
|
+
case 'SLH-DSA-SHA2-256f':
|
|
2381
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
2382
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
2383
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
2384
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
2385
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
2386
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
1923
2387
|
return mldsaSignVerify(key, data, signature);
|
|
1924
2388
|
case 'KMAC128':
|
|
1925
2389
|
case 'KMAC256':
|
|
@@ -1931,23 +2395,16 @@ const signVerify = (
|
|
|
1931
2395
|
);
|
|
1932
2396
|
};
|
|
1933
2397
|
|
|
2398
|
+
// Algorithm-mismatch and usage checks live at the public-method call sites
|
|
2399
|
+
// (encrypt / decrypt / wrapKey / unwrapKey) so spec-mandated message and
|
|
2400
|
+
// ordering — algorithm-mismatch first, then usage — is preserved
|
|
2401
|
+
// (Node webcrypto.js, commit 4cb1f284136).
|
|
1934
2402
|
const cipherOrWrap = async (
|
|
1935
2403
|
mode: CipherOrWrapMode,
|
|
1936
2404
|
algorithm: EncryptDecryptParams,
|
|
1937
2405
|
key: CryptoKey,
|
|
1938
2406
|
data: ArrayBuffer,
|
|
1939
|
-
op: Operation,
|
|
1940
2407
|
): Promise<ArrayBuffer> => {
|
|
1941
|
-
if (
|
|
1942
|
-
key.algorithm.name !== algorithm.name ||
|
|
1943
|
-
!key.usages.includes(op as KeyUsage)
|
|
1944
|
-
) {
|
|
1945
|
-
throw lazyDOMException(
|
|
1946
|
-
'The requested operation is not valid for the provided key',
|
|
1947
|
-
'InvalidAccessError',
|
|
1948
|
-
);
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
2408
|
validateMaxBufferLength(data, 'data');
|
|
1952
2409
|
|
|
1953
2410
|
switch (algorithm.name) {
|
|
@@ -2002,6 +2459,7 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2002
2459
|
'ML-DSA-44',
|
|
2003
2460
|
'ML-DSA-65',
|
|
2004
2461
|
'ML-DSA-87',
|
|
2462
|
+
...SLH_DSA_VARIANTS,
|
|
2005
2463
|
]),
|
|
2006
2464
|
verify: new Set([
|
|
2007
2465
|
'RSASSA-PKCS1-v1_5',
|
|
@@ -2015,6 +2473,7 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2015
2473
|
'ML-DSA-44',
|
|
2016
2474
|
'ML-DSA-65',
|
|
2017
2475
|
'ML-DSA-87',
|
|
2476
|
+
...SLH_DSA_VARIANTS,
|
|
2018
2477
|
]),
|
|
2019
2478
|
digest: new Set([
|
|
2020
2479
|
'SHA-1',
|
|
@@ -2026,6 +2485,10 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2026
2485
|
'SHA3-512',
|
|
2027
2486
|
'cSHAKE128',
|
|
2028
2487
|
'cSHAKE256',
|
|
2488
|
+
'TurboSHAKE128',
|
|
2489
|
+
'TurboSHAKE256',
|
|
2490
|
+
'KT128',
|
|
2491
|
+
'KT256',
|
|
2029
2492
|
]),
|
|
2030
2493
|
generateKey: new Set([
|
|
2031
2494
|
'RSASSA-PKCS1-v1_5',
|
|
@@ -2052,6 +2515,7 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2052
2515
|
'ML-KEM-512',
|
|
2053
2516
|
'ML-KEM-768',
|
|
2054
2517
|
'ML-KEM-1024',
|
|
2518
|
+
...SLH_DSA_VARIANTS,
|
|
2055
2519
|
]),
|
|
2056
2520
|
importKey: new Set([
|
|
2057
2521
|
'RSASSA-PKCS1-v1_5',
|
|
@@ -2083,6 +2547,7 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2083
2547
|
'ML-KEM-512',
|
|
2084
2548
|
'ML-KEM-768',
|
|
2085
2549
|
'ML-KEM-1024',
|
|
2550
|
+
...SLH_DSA_VARIANTS,
|
|
2086
2551
|
]),
|
|
2087
2552
|
exportKey: new Set([
|
|
2088
2553
|
'RSASSA-PKCS1-v1_5',
|
|
@@ -2109,6 +2574,7 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2109
2574
|
'ML-KEM-512',
|
|
2110
2575
|
'ML-KEM-768',
|
|
2111
2576
|
'ML-KEM-1024',
|
|
2577
|
+
...SLH_DSA_VARIANTS,
|
|
2112
2578
|
]),
|
|
2113
2579
|
deriveBits: new Set([
|
|
2114
2580
|
'PBKDF2',
|
|
@@ -2144,7 +2610,7 @@ const SUPPORTED_ALGORITHMS: Record<string, Set<string>> = {
|
|
|
2144
2610
|
decapsulateKey: new Set(['ML-KEM-512', 'ML-KEM-768', 'ML-KEM-1024']),
|
|
2145
2611
|
};
|
|
2146
2612
|
|
|
2147
|
-
const ASYMMETRIC_ALGORITHMS = new Set([
|
|
2613
|
+
const ASYMMETRIC_ALGORITHMS = new Set<string>([
|
|
2148
2614
|
'RSASSA-PKCS1-v1_5',
|
|
2149
2615
|
'RSA-PSS',
|
|
2150
2616
|
'RSA-OAEP',
|
|
@@ -2160,52 +2626,230 @@ const ASYMMETRIC_ALGORITHMS = new Set([
|
|
|
2160
2626
|
'ML-KEM-512',
|
|
2161
2627
|
'ML-KEM-768',
|
|
2162
2628
|
'ML-KEM-1024',
|
|
2629
|
+
...SLH_DSA_VARIANTS,
|
|
2163
2630
|
]);
|
|
2164
2631
|
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2632
|
+
// Per-algorithm validators for deriveBits (mirrors Node's hkdf.js:141-149,
|
|
2633
|
+
// pbkdf2.js:96-105, argon2.js:194-209). Used by Subtle.supports to reject
|
|
2634
|
+
// length values that the actual deriveBits implementation would reject.
|
|
2635
|
+
function validateKdfDeriveBitsLength(
|
|
2636
|
+
length: number | null | undefined,
|
|
2637
|
+
algName: string,
|
|
2638
|
+
): void {
|
|
2639
|
+
if (length === null || length === undefined) {
|
|
2640
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
2641
|
+
}
|
|
2642
|
+
if (length % 8) {
|
|
2643
|
+
throw lazyDOMException('length must be a multiple of 8', 'OperationError');
|
|
2644
|
+
}
|
|
2645
|
+
if (algName.startsWith('Argon2') && length < 32) {
|
|
2646
|
+
throw lazyDOMException('length must be >= 32', 'OperationError');
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
// Mirrors Node's webcrypto.js:1652-1731 `check`. Normalizes the algorithm,
|
|
2651
|
+
// looks it up in SUPPORTED_ALGORITHMS, and runs per-algorithm validation
|
|
2652
|
+
// (deriveBits length validators, HMAC+SHA3 generateKey rejection).
|
|
2653
|
+
// `op` is the operation key in SUPPORTED_ALGORITHMS — wrapKey/unwrapKey fall
|
|
2654
|
+
// back to encrypt/decrypt to mirror Node's normalize fallback.
|
|
2655
|
+
function supportsCheck(
|
|
2656
|
+
op: string,
|
|
2657
|
+
alg: SubtleAlgorithm | AnyAlgorithm,
|
|
2658
|
+
length?: number | null,
|
|
2659
|
+
): boolean {
|
|
2660
|
+
let normalizedAlgorithm: SubtleAlgorithm;
|
|
2661
|
+
try {
|
|
2662
|
+
normalizedAlgorithm = normalizeAlgorithm(alg, op as Operation);
|
|
2663
|
+
} catch {
|
|
2664
|
+
if (op === 'wrapKey') return supportsCheck('encrypt', alg);
|
|
2665
|
+
if (op === 'unwrapKey') return supportsCheck('decrypt', alg);
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
const supported = SUPPORTED_ALGORITHMS[op];
|
|
2670
|
+
if (!supported || !supported.has(normalizedAlgorithm.name)) {
|
|
2671
|
+
if (op === 'wrapKey') return supportsCheck('encrypt', alg);
|
|
2672
|
+
if (op === 'unwrapKey') return supportsCheck('decrypt', alg);
|
|
2673
|
+
return false;
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
if (op === 'deriveBits') {
|
|
2677
|
+
const name = normalizedAlgorithm.name;
|
|
2678
|
+
if (name === 'HKDF' || name === 'PBKDF2' || name.startsWith('Argon2')) {
|
|
2679
|
+
try {
|
|
2680
|
+
validateKdfDeriveBitsLength(length, name);
|
|
2681
|
+
} catch {
|
|
2682
|
+
return false;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
if (op === 'generateKey' && normalizedAlgorithm.name === 'HMAC') {
|
|
2688
|
+
const hashName =
|
|
2689
|
+
typeof normalizedAlgorithm.hash === 'string'
|
|
2690
|
+
? normalizedAlgorithm.hash
|
|
2691
|
+
: (normalizedAlgorithm.hash as { name?: string } | undefined)?.name;
|
|
2692
|
+
if (
|
|
2693
|
+
normalizedAlgorithm.length === undefined &&
|
|
2694
|
+
typeof hashName === 'string' &&
|
|
2695
|
+
hashName.startsWith('SHA3-')
|
|
2696
|
+
) {
|
|
2697
|
+
return false;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
function supportsImpl(
|
|
2705
|
+
operation: string,
|
|
2706
|
+
algorithm: SubtleAlgorithm | AnyAlgorithm,
|
|
2707
|
+
lengthOrAdditionalAlgorithm: unknown,
|
|
2708
|
+
): boolean {
|
|
2709
|
+
switch (operation) {
|
|
2710
|
+
case 'decapsulateBits':
|
|
2711
|
+
case 'decapsulateKey':
|
|
2712
|
+
case 'decrypt':
|
|
2713
|
+
case 'deriveBits':
|
|
2714
|
+
case 'deriveKey':
|
|
2715
|
+
case 'digest':
|
|
2716
|
+
case 'encapsulateBits':
|
|
2717
|
+
case 'encapsulateKey':
|
|
2718
|
+
case 'encrypt':
|
|
2719
|
+
case 'exportKey':
|
|
2720
|
+
case 'generateKey':
|
|
2721
|
+
case 'getPublicKey':
|
|
2722
|
+
case 'importKey':
|
|
2723
|
+
case 'sign':
|
|
2724
|
+
case 'unwrapKey':
|
|
2725
|
+
case 'verify':
|
|
2726
|
+
case 'wrapKey':
|
|
2727
|
+
break;
|
|
2728
|
+
default:
|
|
2178
2729
|
return false;
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
let length: number | null | undefined;
|
|
2733
|
+
|
|
2734
|
+
if (operation === 'deriveKey') {
|
|
2735
|
+
// deriveKey decomposes to importKey of derived alg + deriveBits with that
|
|
2736
|
+
// alg's key length. Node webcrypto.js:1547-1563.
|
|
2737
|
+
if (lengthOrAdditionalAlgorithm != null) {
|
|
2738
|
+
const additional = lengthOrAdditionalAlgorithm as
|
|
2739
|
+
| SubtleAlgorithm
|
|
2740
|
+
| AnyAlgorithm;
|
|
2741
|
+
if (!supportsCheck('importKey', additional)) return false;
|
|
2742
|
+
try {
|
|
2743
|
+
length = getKeyLength(normalizeAlgorithm(additional, 'get key length'));
|
|
2744
|
+
} catch {
|
|
2745
|
+
return false;
|
|
2746
|
+
}
|
|
2747
|
+
return supportsCheck('deriveBits', algorithm, length);
|
|
2748
|
+
}
|
|
2749
|
+
// No additional algorithm given — only check the deriveBits side.
|
|
2750
|
+
return supportsCheck('deriveBits', algorithm);
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
if (operation === 'wrapKey') {
|
|
2754
|
+
// wrapKey decomposes to encrypt of wrapping alg + exportKey of wrapped alg.
|
|
2755
|
+
// Node webcrypto.js:1564-1572.
|
|
2756
|
+
if (lengthOrAdditionalAlgorithm != null) {
|
|
2757
|
+
const additional = lengthOrAdditionalAlgorithm as
|
|
2758
|
+
| SubtleAlgorithm
|
|
2759
|
+
| AnyAlgorithm;
|
|
2760
|
+
if (!supportsCheck('exportKey', additional)) return false;
|
|
2179
2761
|
}
|
|
2762
|
+
return supportsCheck('wrapKey', algorithm);
|
|
2763
|
+
}
|
|
2180
2764
|
|
|
2181
|
-
|
|
2765
|
+
if (operation === 'unwrapKey') {
|
|
2766
|
+
// unwrapKey decomposes to decrypt of wrapping alg + importKey of wrapped
|
|
2767
|
+
// alg. Node webcrypto.js:1573-1581.
|
|
2768
|
+
if (lengthOrAdditionalAlgorithm != null) {
|
|
2769
|
+
const additional = lengthOrAdditionalAlgorithm as
|
|
2770
|
+
| SubtleAlgorithm
|
|
2771
|
+
| AnyAlgorithm;
|
|
2772
|
+
if (!supportsCheck('importKey', additional)) return false;
|
|
2773
|
+
}
|
|
2774
|
+
return supportsCheck('unwrapKey', algorithm);
|
|
2775
|
+
}
|
|
2182
2776
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2777
|
+
if (operation === 'deriveBits') {
|
|
2778
|
+
if (lengthOrAdditionalAlgorithm == null) {
|
|
2779
|
+
length = null;
|
|
2780
|
+
} else if (typeof lengthOrAdditionalAlgorithm === 'number') {
|
|
2781
|
+
length = lengthOrAdditionalAlgorithm;
|
|
2782
|
+
} else {
|
|
2783
|
+
return false;
|
|
2185
2784
|
}
|
|
2785
|
+
return supportsCheck('deriveBits', algorithm, length);
|
|
2786
|
+
}
|
|
2186
2787
|
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2788
|
+
if (operation === 'getPublicKey') {
|
|
2789
|
+
let normalized: SubtleAlgorithm;
|
|
2790
|
+
try {
|
|
2791
|
+
normalized = normalizeAlgorithm(algorithm, 'exportKey');
|
|
2792
|
+
} catch {
|
|
2793
|
+
return false;
|
|
2794
|
+
}
|
|
2795
|
+
return ASYMMETRIC_ALGORITHMS.has(normalized.name);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
if (operation === 'encapsulateKey' || operation === 'decapsulateKey') {
|
|
2799
|
+
// sharedKeyAlgorithm must support importKey, with HMAC/KMAC limited to
|
|
2800
|
+
// length === undefined or 256 (Node webcrypto.js:1610-1645).
|
|
2801
|
+
const additional = lengthOrAdditionalAlgorithm as
|
|
2802
|
+
| SubtleAlgorithm
|
|
2803
|
+
| AnyAlgorithm;
|
|
2804
|
+
let normalizedAdd: SubtleAlgorithm;
|
|
2805
|
+
try {
|
|
2806
|
+
normalizedAdd = normalizeAlgorithm(additional, 'importKey');
|
|
2807
|
+
} catch {
|
|
2808
|
+
return false;
|
|
2809
|
+
}
|
|
2810
|
+
switch (normalizedAdd.name) {
|
|
2811
|
+
case 'AES-OCB':
|
|
2812
|
+
case 'AES-KW':
|
|
2813
|
+
case 'AES-GCM':
|
|
2814
|
+
case 'AES-CTR':
|
|
2815
|
+
case 'AES-CBC':
|
|
2816
|
+
case 'ChaCha20-Poly1305':
|
|
2817
|
+
case 'HKDF':
|
|
2818
|
+
case 'PBKDF2':
|
|
2819
|
+
case 'Argon2i':
|
|
2820
|
+
case 'Argon2d':
|
|
2821
|
+
case 'Argon2id':
|
|
2822
|
+
break;
|
|
2823
|
+
case 'HMAC':
|
|
2824
|
+
case 'KMAC128':
|
|
2825
|
+
case 'KMAC256': {
|
|
2826
|
+
const addLen = normalizedAdd.length;
|
|
2827
|
+
if (addLen !== undefined && addLen !== 256) return false;
|
|
2828
|
+
break;
|
|
2202
2829
|
}
|
|
2203
|
-
|
|
2830
|
+
default:
|
|
2831
|
+
return false;
|
|
2204
2832
|
}
|
|
2833
|
+
return supportsCheck(operation, algorithm);
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
return supportsCheck(operation, algorithm);
|
|
2837
|
+
}
|
|
2205
2838
|
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2839
|
+
export class Subtle {
|
|
2840
|
+
// Spec-compliant capability detector. Mirrors Node's webcrypto.js:1506-1649
|
|
2841
|
+
// `SubtleCrypto.supports`, including:
|
|
2842
|
+
// • composed-operation decomposition (deriveKey, wrapKey, unwrapKey,
|
|
2843
|
+
// encapsulateKey, decapsulateKey, getPublicKey)
|
|
2844
|
+
// • per-algorithm length validators for deriveBits (HKDF/PBKDF2/Argon2)
|
|
2845
|
+
// • HMAC + SHA3 generateKey with no length → false
|
|
2846
|
+
// Static-only per the WICG spec.
|
|
2847
|
+
static supports(
|
|
2848
|
+
operation: string,
|
|
2849
|
+
algorithm: SubtleAlgorithm | AnyAlgorithm,
|
|
2850
|
+
lengthOrAdditionalAlgorithm: unknown = null,
|
|
2851
|
+
): boolean {
|
|
2852
|
+
return supportsImpl(operation, algorithm, lengthOrAdditionalAlgorithm);
|
|
2209
2853
|
}
|
|
2210
2854
|
|
|
2211
2855
|
async decrypt(
|
|
@@ -2213,13 +2857,25 @@ export class Subtle {
|
|
|
2213
2857
|
key: CryptoKey,
|
|
2214
2858
|
data: BufferLike,
|
|
2215
2859
|
): Promise<ArrayBuffer> {
|
|
2216
|
-
|
|
2860
|
+
requireArgs(arguments.length, 3, 'decrypt');
|
|
2861
|
+
const normalizedAlgorithm = normalizeAlgorithm(
|
|
2862
|
+
algorithm,
|
|
2863
|
+
'decrypt',
|
|
2864
|
+
) as EncryptDecryptParams;
|
|
2865
|
+
if (normalizedAlgorithm.name !== key.algorithm.name) {
|
|
2866
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
2867
|
+
}
|
|
2868
|
+
if (!key.usages.includes('decrypt')) {
|
|
2869
|
+
throw lazyDOMException(
|
|
2870
|
+
'Unable to use this key to decrypt',
|
|
2871
|
+
'InvalidAccessError',
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2217
2874
|
return cipherOrWrap(
|
|
2218
2875
|
CipherOrWrapMode.kWebCryptoCipherDecrypt,
|
|
2219
|
-
normalizedAlgorithm
|
|
2876
|
+
normalizedAlgorithm,
|
|
2220
2877
|
key,
|
|
2221
2878
|
bufferLikeToArrayBuffer(data),
|
|
2222
|
-
'decrypt',
|
|
2223
2879
|
);
|
|
2224
2880
|
}
|
|
2225
2881
|
|
|
@@ -2227,6 +2883,7 @@ export class Subtle {
|
|
|
2227
2883
|
algorithm: SubtleAlgorithm | AnyAlgorithm,
|
|
2228
2884
|
data: BufferLike,
|
|
2229
2885
|
): Promise<ArrayBuffer> {
|
|
2886
|
+
requireArgs(arguments.length, 2, 'digest');
|
|
2230
2887
|
const normalizedAlgorithm = normalizeAlgorithm(
|
|
2231
2888
|
algorithm,
|
|
2232
2889
|
'digest' as Operation,
|
|
@@ -2237,43 +2894,56 @@ export class Subtle {
|
|
|
2237
2894
|
async deriveBits(
|
|
2238
2895
|
algorithm: SubtleAlgorithm,
|
|
2239
2896
|
baseKey: CryptoKey,
|
|
2240
|
-
length: number,
|
|
2897
|
+
length: number | null = null,
|
|
2241
2898
|
): Promise<ArrayBuffer> {
|
|
2899
|
+
requireArgs(arguments.length, 2, 'deriveBits');
|
|
2900
|
+
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
|
|
2242
2901
|
// WebCrypto §SubtleCrypto.deriveBits step 11: throw InvalidAccessError
|
|
2243
2902
|
// unless `baseKey.[[usages]]` contains "deriveBits" specifically. The
|
|
2244
2903
|
// previous `deriveBits || deriveKey` accept-either branch silently
|
|
2245
2904
|
// promoted deriveKey-only keys into deriveBits use, contradicting the
|
|
2246
2905
|
// spec usage gate.
|
|
2247
|
-
if (!baseKey.
|
|
2906
|
+
if (!baseKey.usages.includes('deriveBits')) {
|
|
2248
2907
|
throw lazyDOMException(
|
|
2249
2908
|
'baseKey does not have deriveBits usage',
|
|
2250
2909
|
'InvalidAccessError',
|
|
2251
2910
|
);
|
|
2252
2911
|
}
|
|
2253
|
-
if (baseKey.algorithm.name !==
|
|
2254
|
-
throw
|
|
2255
|
-
|
|
2912
|
+
if (baseKey.algorithm.name !== normalizedAlgorithm.name) {
|
|
2913
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
2914
|
+
}
|
|
2915
|
+
switch (normalizedAlgorithm.name) {
|
|
2256
2916
|
case 'PBKDF2':
|
|
2257
|
-
|
|
2917
|
+
if (length === null) {
|
|
2918
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
2919
|
+
}
|
|
2920
|
+
return pbkdf2DeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2258
2921
|
case 'X25519':
|
|
2259
2922
|
// Fall through
|
|
2260
2923
|
case 'X448':
|
|
2261
|
-
return xDeriveBits(
|
|
2924
|
+
return xDeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2262
2925
|
case 'ECDH':
|
|
2263
|
-
return ecDeriveBits(
|
|
2926
|
+
return ecDeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2264
2927
|
case 'HKDF':
|
|
2928
|
+
if (length === null) {
|
|
2929
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
2930
|
+
}
|
|
2265
2931
|
return hkdfDeriveBits(
|
|
2266
|
-
|
|
2932
|
+
normalizedAlgorithm as unknown as HkdfAlgorithm,
|
|
2267
2933
|
baseKey,
|
|
2268
2934
|
length,
|
|
2269
2935
|
);
|
|
2270
2936
|
case 'Argon2d':
|
|
2271
2937
|
case 'Argon2i':
|
|
2272
2938
|
case 'Argon2id':
|
|
2273
|
-
|
|
2939
|
+
if (length === null) {
|
|
2940
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
2941
|
+
}
|
|
2942
|
+
return argon2DeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2274
2943
|
}
|
|
2275
|
-
throw
|
|
2276
|
-
`'subtle.deriveBits()' for ${
|
|
2944
|
+
throw lazyDOMException(
|
|
2945
|
+
`'subtle.deriveBits()' for ${normalizedAlgorithm.name} is not implemented.`,
|
|
2946
|
+
'NotSupportedError',
|
|
2277
2947
|
);
|
|
2278
2948
|
}
|
|
2279
2949
|
|
|
@@ -2284,40 +2954,55 @@ export class Subtle {
|
|
|
2284
2954
|
extractable: boolean,
|
|
2285
2955
|
keyUsages: KeyUsage[],
|
|
2286
2956
|
): Promise<CryptoKey> {
|
|
2957
|
+
requireArgs(arguments.length, 5, 'deriveKey');
|
|
2958
|
+
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'deriveBits');
|
|
2959
|
+
const normalizedDerivedKeyAlgorithm = normalizeAlgorithm(
|
|
2960
|
+
derivedKeyAlgorithm,
|
|
2961
|
+
'importKey',
|
|
2962
|
+
);
|
|
2963
|
+
|
|
2287
2964
|
// Validate baseKey usage
|
|
2288
|
-
if (
|
|
2289
|
-
!baseKey.usages.includes('deriveKey') &&
|
|
2290
|
-
!baseKey.usages.includes('deriveBits')
|
|
2291
|
-
) {
|
|
2965
|
+
if (!baseKey.usages.includes('deriveKey')) {
|
|
2292
2966
|
throw lazyDOMException(
|
|
2293
|
-
'baseKey does not have deriveKey
|
|
2967
|
+
'baseKey does not have deriveKey usage',
|
|
2294
2968
|
'InvalidAccessError',
|
|
2295
2969
|
);
|
|
2296
2970
|
}
|
|
2297
2971
|
|
|
2298
|
-
|
|
2299
|
-
|
|
2972
|
+
if (baseKey.algorithm.name !== normalizedAlgorithm.name) {
|
|
2973
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// Calculate required key length (may be null for KDF-derived material).
|
|
2977
|
+
const length = getKeyLength(normalizedDerivedKeyAlgorithm);
|
|
2300
2978
|
|
|
2301
2979
|
// Step 1: Derive bits
|
|
2302
2980
|
let derivedBits: ArrayBuffer;
|
|
2303
|
-
|
|
2304
|
-
throw new Error('Key algorithm mismatch');
|
|
2305
|
-
|
|
2306
|
-
switch (algorithm.name) {
|
|
2981
|
+
switch (normalizedAlgorithm.name) {
|
|
2307
2982
|
case 'PBKDF2':
|
|
2308
|
-
|
|
2983
|
+
if (length === null) {
|
|
2984
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
2985
|
+
}
|
|
2986
|
+
derivedBits = await pbkdf2DeriveBits(
|
|
2987
|
+
normalizedAlgorithm,
|
|
2988
|
+
baseKey,
|
|
2989
|
+
length,
|
|
2990
|
+
);
|
|
2309
2991
|
break;
|
|
2310
2992
|
case 'X25519':
|
|
2311
2993
|
// Fall through
|
|
2312
2994
|
case 'X448':
|
|
2313
|
-
derivedBits = await xDeriveBits(
|
|
2995
|
+
derivedBits = await xDeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2314
2996
|
break;
|
|
2315
2997
|
case 'ECDH':
|
|
2316
|
-
derivedBits = await ecDeriveBits(
|
|
2998
|
+
derivedBits = await ecDeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2317
2999
|
break;
|
|
2318
3000
|
case 'HKDF':
|
|
3001
|
+
if (length === null) {
|
|
3002
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
3003
|
+
}
|
|
2319
3004
|
derivedBits = hkdfDeriveBits(
|
|
2320
|
-
|
|
3005
|
+
normalizedAlgorithm as unknown as HkdfAlgorithm,
|
|
2321
3006
|
baseKey,
|
|
2322
3007
|
length,
|
|
2323
3008
|
);
|
|
@@ -2325,17 +3010,22 @@ export class Subtle {
|
|
|
2325
3010
|
case 'Argon2d':
|
|
2326
3011
|
case 'Argon2i':
|
|
2327
3012
|
case 'Argon2id':
|
|
2328
|
-
|
|
3013
|
+
if (length === null) {
|
|
3014
|
+
throw lazyDOMException('length cannot be null', 'OperationError');
|
|
3015
|
+
}
|
|
3016
|
+
derivedBits = argon2DeriveBits(normalizedAlgorithm, baseKey, length);
|
|
2329
3017
|
break;
|
|
2330
3018
|
default:
|
|
2331
|
-
throw
|
|
2332
|
-
`'subtle.deriveKey()' for ${
|
|
3019
|
+
throw lazyDOMException(
|
|
3020
|
+
`'subtle.deriveKey()' for ${normalizedAlgorithm.name} is not implemented.`,
|
|
3021
|
+
'NotSupportedError',
|
|
2333
3022
|
);
|
|
2334
3023
|
}
|
|
2335
3024
|
|
|
2336
|
-
// Step 2: Import as key
|
|
3025
|
+
// Step 2: Import as key. Use 'raw-secret' so derived material flows into
|
|
3026
|
+
// AEADs / KMAC correctly — they reject plain 'raw' (Node webcrypto.js:381-385).
|
|
2337
3027
|
return this.importKey(
|
|
2338
|
-
'raw',
|
|
3028
|
+
'raw-secret',
|
|
2339
3029
|
derivedBits,
|
|
2340
3030
|
derivedKeyAlgorithm,
|
|
2341
3031
|
extractable,
|
|
@@ -2348,13 +3038,25 @@ export class Subtle {
|
|
|
2348
3038
|
key: CryptoKey,
|
|
2349
3039
|
data: BufferLike,
|
|
2350
3040
|
): Promise<ArrayBuffer> {
|
|
2351
|
-
|
|
3041
|
+
requireArgs(arguments.length, 3, 'encrypt');
|
|
3042
|
+
const normalizedAlgorithm = normalizeAlgorithm(
|
|
3043
|
+
algorithm,
|
|
3044
|
+
'encrypt',
|
|
3045
|
+
) as EncryptDecryptParams;
|
|
3046
|
+
if (normalizedAlgorithm.name !== key.algorithm.name) {
|
|
3047
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
3048
|
+
}
|
|
3049
|
+
if (!key.usages.includes('encrypt')) {
|
|
3050
|
+
throw lazyDOMException(
|
|
3051
|
+
'Unable to use this key to encrypt',
|
|
3052
|
+
'InvalidAccessError',
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
2352
3055
|
return cipherOrWrap(
|
|
2353
3056
|
CipherOrWrapMode.kWebCryptoCipherEncrypt,
|
|
2354
|
-
normalizedAlgorithm
|
|
3057
|
+
normalizedAlgorithm,
|
|
2355
3058
|
key,
|
|
2356
3059
|
bufferLikeToArrayBuffer(data),
|
|
2357
|
-
'encrypt',
|
|
2358
3060
|
);
|
|
2359
3061
|
}
|
|
2360
3062
|
|
|
@@ -2362,16 +3064,19 @@ export class Subtle {
|
|
|
2362
3064
|
format: ImportFormat,
|
|
2363
3065
|
key: CryptoKey,
|
|
2364
3066
|
): Promise<ArrayBuffer | JWK> {
|
|
2365
|
-
|
|
3067
|
+
requireArgs(arguments.length, 2, 'exportKey');
|
|
3068
|
+
if (!key.extractable)
|
|
3069
|
+
throw lazyDOMException('key is not extractable', 'InvalidAccessError');
|
|
2366
3070
|
|
|
2367
3071
|
if (format === 'raw-seed') {
|
|
2368
|
-
const pqcAlgos = [
|
|
3072
|
+
const pqcAlgos: string[] = [
|
|
2369
3073
|
'ML-KEM-512',
|
|
2370
3074
|
'ML-KEM-768',
|
|
2371
3075
|
'ML-KEM-1024',
|
|
2372
3076
|
'ML-DSA-44',
|
|
2373
3077
|
'ML-DSA-65',
|
|
2374
3078
|
'ML-DSA-87',
|
|
3079
|
+
...SLH_DSA_VARIANTS,
|
|
2375
3080
|
];
|
|
2376
3081
|
if (!pqcAlgos.includes(key.algorithm.name)) {
|
|
2377
3082
|
throw lazyDOMException(
|
|
@@ -2388,9 +3093,6 @@ export class Subtle {
|
|
|
2388
3093
|
return bufferLikeToArrayBuffer(key.keyObject.handle.exportKey());
|
|
2389
3094
|
}
|
|
2390
3095
|
|
|
2391
|
-
// Note: 'raw-seed' is handled above; do NOT normalize it here
|
|
2392
|
-
if (format === 'raw-secret' || format === 'raw-public') format = 'raw';
|
|
2393
|
-
|
|
2394
3096
|
switch (format) {
|
|
2395
3097
|
case 'spki':
|
|
2396
3098
|
return (await exportKeySpki(key)) as ArrayBuffer;
|
|
@@ -2399,7 +3101,9 @@ export class Subtle {
|
|
|
2399
3101
|
case 'jwk':
|
|
2400
3102
|
return exportKeyJWK(key) as JWK;
|
|
2401
3103
|
case 'raw':
|
|
2402
|
-
|
|
3104
|
+
case 'raw-secret':
|
|
3105
|
+
case 'raw-public':
|
|
3106
|
+
return exportKeyRaw(key, format) as ArrayBuffer;
|
|
2403
3107
|
}
|
|
2404
3108
|
}
|
|
2405
3109
|
|
|
@@ -2409,10 +3113,18 @@ export class Subtle {
|
|
|
2409
3113
|
wrappingKey: CryptoKey,
|
|
2410
3114
|
wrapAlgorithm: EncryptDecryptParams,
|
|
2411
3115
|
): Promise<ArrayBuffer> {
|
|
2412
|
-
|
|
3116
|
+
requireArgs(arguments.length, 4, 'wrapKey');
|
|
3117
|
+
const normalizedWrapAlgorithm = normalizeAlgorithm(
|
|
3118
|
+
wrapAlgorithm,
|
|
3119
|
+
'wrapKey',
|
|
3120
|
+
) as EncryptDecryptParams;
|
|
3121
|
+
|
|
3122
|
+
if (normalizedWrapAlgorithm.name !== wrappingKey.algorithm.name) {
|
|
3123
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
3124
|
+
}
|
|
2413
3125
|
if (!wrappingKey.usages.includes('wrapKey')) {
|
|
2414
3126
|
throw lazyDOMException(
|
|
2415
|
-
'
|
|
3127
|
+
'Unable to use this key to wrapKey',
|
|
2416
3128
|
'InvalidAccessError',
|
|
2417
3129
|
);
|
|
2418
3130
|
}
|
|
@@ -2427,7 +3139,7 @@ export class Subtle {
|
|
|
2427
3139
|
const buffer = SBuffer.from(jwkString, 'utf8');
|
|
2428
3140
|
|
|
2429
3141
|
// For AES-KW, pad to multiple of 8 bytes (accounting for null terminator)
|
|
2430
|
-
if (
|
|
3142
|
+
if (normalizedWrapAlgorithm.name === 'AES-KW') {
|
|
2431
3143
|
const length = buffer.length;
|
|
2432
3144
|
// Add 1 for null terminator, then pad to multiple of 8
|
|
2433
3145
|
const paddedLength = Math.ceil((length + 1) / 8) * 8;
|
|
@@ -2446,10 +3158,9 @@ export class Subtle {
|
|
|
2446
3158
|
// Step 3: Encrypt the exported key
|
|
2447
3159
|
return cipherOrWrap(
|
|
2448
3160
|
CipherOrWrapMode.kWebCryptoCipherEncrypt,
|
|
2449
|
-
|
|
3161
|
+
normalizedWrapAlgorithm,
|
|
2450
3162
|
wrappingKey,
|
|
2451
3163
|
keyData,
|
|
2452
|
-
'wrapKey',
|
|
2453
3164
|
);
|
|
2454
3165
|
}
|
|
2455
3166
|
|
|
@@ -2462,10 +3173,18 @@ export class Subtle {
|
|
|
2462
3173
|
extractable: boolean,
|
|
2463
3174
|
keyUsages: KeyUsage[],
|
|
2464
3175
|
): Promise<CryptoKey> {
|
|
2465
|
-
|
|
3176
|
+
requireArgs(arguments.length, 7, 'unwrapKey');
|
|
3177
|
+
const normalizedUnwrapAlgorithm = normalizeAlgorithm(
|
|
3178
|
+
unwrapAlgorithm,
|
|
3179
|
+
'unwrapKey',
|
|
3180
|
+
) as EncryptDecryptParams;
|
|
3181
|
+
|
|
3182
|
+
if (normalizedUnwrapAlgorithm.name !== unwrappingKey.algorithm.name) {
|
|
3183
|
+
throw lazyDOMException('Key algorithm mismatch', 'InvalidAccessError');
|
|
3184
|
+
}
|
|
2466
3185
|
if (!unwrappingKey.usages.includes('unwrapKey')) {
|
|
2467
3186
|
throw lazyDOMException(
|
|
2468
|
-
'
|
|
3187
|
+
'Unable to use this key to unwrapKey',
|
|
2469
3188
|
'InvalidAccessError',
|
|
2470
3189
|
);
|
|
2471
3190
|
}
|
|
@@ -2473,10 +3192,9 @@ export class Subtle {
|
|
|
2473
3192
|
// Step 1: Decrypt the wrapped key
|
|
2474
3193
|
const decrypted = await cipherOrWrap(
|
|
2475
3194
|
CipherOrWrapMode.kWebCryptoCipherDecrypt,
|
|
2476
|
-
|
|
3195
|
+
normalizedUnwrapAlgorithm,
|
|
2477
3196
|
unwrappingKey,
|
|
2478
3197
|
bufferLikeToArrayBuffer(wrappedKey),
|
|
2479
|
-
'unwrapKey',
|
|
2480
3198
|
);
|
|
2481
3199
|
|
|
2482
3200
|
// Step 2: Convert to appropriate format
|
|
@@ -2485,7 +3203,7 @@ export class Subtle {
|
|
|
2485
3203
|
const buffer = SBuffer.from(decrypted);
|
|
2486
3204
|
// For AES-KW, the data may be padded - find the null terminator
|
|
2487
3205
|
let jwkString: string;
|
|
2488
|
-
if (
|
|
3206
|
+
if (normalizedUnwrapAlgorithm.name === 'AES-KW') {
|
|
2489
3207
|
// Find the null terminator (if present) to get the original string
|
|
2490
3208
|
const nullIndex = buffer.indexOf(0);
|
|
2491
3209
|
if (nullIndex !== -1) {
|
|
@@ -2517,6 +3235,7 @@ export class Subtle {
|
|
|
2517
3235
|
extractable: boolean,
|
|
2518
3236
|
keyUsages: KeyUsage[],
|
|
2519
3237
|
): Promise<CryptoKey | CryptoKeyPair> {
|
|
3238
|
+
requireArgs(arguments.length, 3, 'generateKey');
|
|
2520
3239
|
algorithm = normalizeAlgorithm(algorithm, 'generateKey');
|
|
2521
3240
|
let result: CryptoKey | CryptoKeyPair;
|
|
2522
3241
|
switch (algorithm.name) {
|
|
@@ -2603,6 +3322,25 @@ export class Subtle {
|
|
|
2603
3322
|
);
|
|
2604
3323
|
checkCryptoKeyPairUsages(result as CryptoKeyPair);
|
|
2605
3324
|
break;
|
|
3325
|
+
case 'SLH-DSA-SHA2-128s':
|
|
3326
|
+
case 'SLH-DSA-SHA2-128f':
|
|
3327
|
+
case 'SLH-DSA-SHA2-192s':
|
|
3328
|
+
case 'SLH-DSA-SHA2-192f':
|
|
3329
|
+
case 'SLH-DSA-SHA2-256s':
|
|
3330
|
+
case 'SLH-DSA-SHA2-256f':
|
|
3331
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
3332
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
3333
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
3334
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
3335
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
3336
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
3337
|
+
result = await slhdsa_generateKeyPairWebCrypto(
|
|
3338
|
+
algorithm.name as SlhDsaVariant,
|
|
3339
|
+
extractable,
|
|
3340
|
+
keyUsages,
|
|
3341
|
+
);
|
|
3342
|
+
checkCryptoKeyPairUsages(result as CryptoKeyPair);
|
|
3343
|
+
break;
|
|
2606
3344
|
case 'X25519':
|
|
2607
3345
|
// Fall through
|
|
2608
3346
|
case 'X448':
|
|
@@ -2639,6 +3377,7 @@ export class Subtle {
|
|
|
2639
3377
|
key: CryptoKey,
|
|
2640
3378
|
keyUsages: KeyUsage[],
|
|
2641
3379
|
): Promise<CryptoKey> {
|
|
3380
|
+
requireArgs(arguments.length, 2, 'getPublicKey');
|
|
2642
3381
|
if (key.type === 'secret') {
|
|
2643
3382
|
throw lazyDOMException('key must be a private key', 'NotSupportedError');
|
|
2644
3383
|
}
|
|
@@ -2657,8 +3396,11 @@ export class Subtle {
|
|
|
2657
3396
|
extractable: boolean,
|
|
2658
3397
|
keyUsages: KeyUsage[],
|
|
2659
3398
|
): Promise<CryptoKey> {
|
|
2660
|
-
|
|
2661
|
-
|
|
3399
|
+
requireArgs(arguments.length, 5, 'importKey');
|
|
3400
|
+
// Per-algorithm format handling. Some algorithms alias raw-secret/raw-public
|
|
3401
|
+
// to 'raw' (RSA, EC, Ed/X, HMAC, HKDF, PBKDF2); others demand the
|
|
3402
|
+
// disambiguated form (KMAC, AES-OCB, ChaCha20-Poly1305, Argon2, ML-DSA,
|
|
3403
|
+
// ML-KEM). 'raw-seed' is never normalized — PQC import handles it directly.
|
|
2662
3404
|
const normalizedAlgorithm = normalizeAlgorithm(algorithm, 'importKey');
|
|
2663
3405
|
let result: CryptoKey;
|
|
2664
3406
|
switch (normalizedAlgorithm.name) {
|
|
@@ -2668,7 +3410,7 @@ export class Subtle {
|
|
|
2668
3410
|
// Fall through
|
|
2669
3411
|
case 'RSA-OAEP':
|
|
2670
3412
|
result = rsaImportKey(
|
|
2671
|
-
format,
|
|
3413
|
+
aliasKeyFormat(format),
|
|
2672
3414
|
data as BufferLike | JWK,
|
|
2673
3415
|
normalizedAlgorithm,
|
|
2674
3416
|
extractable,
|
|
@@ -2679,7 +3421,7 @@ export class Subtle {
|
|
|
2679
3421
|
// Fall through
|
|
2680
3422
|
case 'ECDH':
|
|
2681
3423
|
result = ecImportKey(
|
|
2682
|
-
format,
|
|
3424
|
+
aliasKeyFormat(format),
|
|
2683
3425
|
data,
|
|
2684
3426
|
normalizedAlgorithm,
|
|
2685
3427
|
extractable,
|
|
@@ -2687,6 +3429,9 @@ export class Subtle {
|
|
|
2687
3429
|
);
|
|
2688
3430
|
break;
|
|
2689
3431
|
case 'HMAC':
|
|
3432
|
+
// No aliasing — Node routes HMAC straight into mac.js, which accepts
|
|
3433
|
+
// 'raw' / 'raw-secret' / 'jwk' and rejects everything else
|
|
3434
|
+
// (webcrypto.js:774-781, mac.js:136-174).
|
|
2690
3435
|
result = await hmacImportKey(
|
|
2691
3436
|
normalizedAlgorithm,
|
|
2692
3437
|
format,
|
|
@@ -2726,10 +3471,18 @@ export class Subtle {
|
|
|
2726
3471
|
);
|
|
2727
3472
|
break;
|
|
2728
3473
|
case 'PBKDF2':
|
|
3474
|
+
result = await pbkdf2ImportKey(
|
|
3475
|
+
normalizedAlgorithm,
|
|
3476
|
+
aliasKeyFormat(format),
|
|
3477
|
+
data as BufferLike | BinaryLike,
|
|
3478
|
+
extractable,
|
|
3479
|
+
keyUsages,
|
|
3480
|
+
);
|
|
3481
|
+
break;
|
|
2729
3482
|
case 'Argon2d':
|
|
2730
3483
|
case 'Argon2i':
|
|
2731
3484
|
case 'Argon2id':
|
|
2732
|
-
result = await
|
|
3485
|
+
result = await argon2ImportKey(
|
|
2733
3486
|
normalizedAlgorithm,
|
|
2734
3487
|
format,
|
|
2735
3488
|
data as BufferLike | BinaryLike,
|
|
@@ -2739,7 +3492,7 @@ export class Subtle {
|
|
|
2739
3492
|
break;
|
|
2740
3493
|
case 'HKDF':
|
|
2741
3494
|
result = await hkdfImportKey(
|
|
2742
|
-
format,
|
|
3495
|
+
aliasKeyFormat(format),
|
|
2743
3496
|
data as BufferLike | BinaryLike,
|
|
2744
3497
|
normalizedAlgorithm,
|
|
2745
3498
|
extractable,
|
|
@@ -2754,6 +3507,26 @@ export class Subtle {
|
|
|
2754
3507
|
// Fall through
|
|
2755
3508
|
case 'Ed448':
|
|
2756
3509
|
result = edImportKey(
|
|
3510
|
+
aliasKeyFormat(format),
|
|
3511
|
+
data as BufferLike | JWK,
|
|
3512
|
+
normalizedAlgorithm,
|
|
3513
|
+
extractable,
|
|
3514
|
+
keyUsages,
|
|
3515
|
+
);
|
|
3516
|
+
break;
|
|
3517
|
+
case 'SLH-DSA-SHA2-128s':
|
|
3518
|
+
case 'SLH-DSA-SHA2-128f':
|
|
3519
|
+
case 'SLH-DSA-SHA2-192s':
|
|
3520
|
+
case 'SLH-DSA-SHA2-192f':
|
|
3521
|
+
case 'SLH-DSA-SHA2-256s':
|
|
3522
|
+
case 'SLH-DSA-SHA2-256f':
|
|
3523
|
+
case 'SLH-DSA-SHAKE-128s':
|
|
3524
|
+
case 'SLH-DSA-SHAKE-128f':
|
|
3525
|
+
case 'SLH-DSA-SHAKE-192s':
|
|
3526
|
+
case 'SLH-DSA-SHAKE-192f':
|
|
3527
|
+
case 'SLH-DSA-SHAKE-256s':
|
|
3528
|
+
case 'SLH-DSA-SHAKE-256f':
|
|
3529
|
+
result = slhdsaImportKey(
|
|
2757
3530
|
format,
|
|
2758
3531
|
data as BufferLike | JWK,
|
|
2759
3532
|
normalizedAlgorithm,
|
|
@@ -2768,7 +3541,7 @@ export class Subtle {
|
|
|
2768
3541
|
case 'ML-DSA-87':
|
|
2769
3542
|
result = mldsaImportKey(
|
|
2770
3543
|
format,
|
|
2771
|
-
data as BufferLike,
|
|
3544
|
+
data as BufferLike | JWK,
|
|
2772
3545
|
normalizedAlgorithm,
|
|
2773
3546
|
extractable,
|
|
2774
3547
|
keyUsages,
|
|
@@ -2781,7 +3554,7 @@ export class Subtle {
|
|
|
2781
3554
|
case 'ML-KEM-1024':
|
|
2782
3555
|
result = mlkemImportKey(
|
|
2783
3556
|
format,
|
|
2784
|
-
data as BufferLike,
|
|
3557
|
+
data as BufferLike | JWK,
|
|
2785
3558
|
normalizedAlgorithm,
|
|
2786
3559
|
extractable,
|
|
2787
3560
|
keyUsages,
|
|
@@ -2810,6 +3583,7 @@ export class Subtle {
|
|
|
2810
3583
|
key: CryptoKey,
|
|
2811
3584
|
data: BufferLike,
|
|
2812
3585
|
): Promise<ArrayBuffer> {
|
|
3586
|
+
requireArgs(arguments.length, 3, 'sign');
|
|
2813
3587
|
return signVerify(
|
|
2814
3588
|
normalizeAlgorithm(algorithm, 'sign'),
|
|
2815
3589
|
key,
|
|
@@ -2823,6 +3597,7 @@ export class Subtle {
|
|
|
2823
3597
|
signature: BufferLike,
|
|
2824
3598
|
data: BufferLike,
|
|
2825
3599
|
): Promise<boolean> {
|
|
3600
|
+
requireArgs(arguments.length, 4, 'verify');
|
|
2826
3601
|
return signVerify(
|
|
2827
3602
|
normalizeAlgorithm(algorithm, 'verify'),
|
|
2828
3603
|
key,
|
|
@@ -2894,6 +3669,7 @@ export class Subtle {
|
|
|
2894
3669
|
algorithm: SubtleAlgorithm,
|
|
2895
3670
|
key: CryptoKey,
|
|
2896
3671
|
): Promise<EncapsulateResult> {
|
|
3672
|
+
requireArgs(arguments.length, 2, 'encapsulateBits');
|
|
2897
3673
|
if (!key.usages.includes('encapsulateBits')) {
|
|
2898
3674
|
throw lazyDOMException(
|
|
2899
3675
|
'Key does not have encapsulateBits usage',
|
|
@@ -2911,6 +3687,7 @@ export class Subtle {
|
|
|
2911
3687
|
extractable: boolean,
|
|
2912
3688
|
usages: KeyUsage[],
|
|
2913
3689
|
): Promise<{ key: CryptoKey; ciphertext: ArrayBuffer }> {
|
|
3690
|
+
requireArgs(arguments.length, 5, 'encapsulateKey');
|
|
2914
3691
|
if (!key.usages.includes('encapsulateKey')) {
|
|
2915
3692
|
throw lazyDOMException(
|
|
2916
3693
|
'Key does not have encapsulateKey usage',
|
|
@@ -2919,8 +3696,10 @@ export class Subtle {
|
|
|
2919
3696
|
}
|
|
2920
3697
|
|
|
2921
3698
|
const { sharedKey, ciphertext } = this._encapsulateCore(algorithm, key);
|
|
3699
|
+
// Node imports the encapsulated shared bits as 'raw-secret'
|
|
3700
|
+
// (webcrypto.js:1370-1374) so AEADs / KMAC accept the result.
|
|
2922
3701
|
const importedKey = await this.importKey(
|
|
2923
|
-
'raw',
|
|
3702
|
+
'raw-secret',
|
|
2924
3703
|
sharedKey,
|
|
2925
3704
|
sharedKeyAlgorithm,
|
|
2926
3705
|
extractable,
|
|
@@ -2935,6 +3714,7 @@ export class Subtle {
|
|
|
2935
3714
|
key: CryptoKey,
|
|
2936
3715
|
ciphertext: BufferLike,
|
|
2937
3716
|
): Promise<ArrayBuffer> {
|
|
3717
|
+
requireArgs(arguments.length, 3, 'decapsulateBits');
|
|
2938
3718
|
if (!key.usages.includes('decapsulateBits')) {
|
|
2939
3719
|
throw lazyDOMException(
|
|
2940
3720
|
'Key does not have decapsulateBits usage',
|
|
@@ -2953,6 +3733,7 @@ export class Subtle {
|
|
|
2953
3733
|
extractable: boolean,
|
|
2954
3734
|
usages: KeyUsage[],
|
|
2955
3735
|
): Promise<CryptoKey> {
|
|
3736
|
+
requireArgs(arguments.length, 6, 'decapsulateKey');
|
|
2956
3737
|
if (!key.usages.includes('decapsulateKey')) {
|
|
2957
3738
|
throw lazyDOMException(
|
|
2958
3739
|
'Key does not have decapsulateKey usage',
|
|
@@ -2961,8 +3742,10 @@ export class Subtle {
|
|
|
2961
3742
|
}
|
|
2962
3743
|
|
|
2963
3744
|
const sharedKey = this._decapsulateCore(algorithm, key, ciphertext);
|
|
3745
|
+
// Node imports the decapsulated shared bits as 'raw-secret'
|
|
3746
|
+
// (webcrypto.js:1490-1494).
|
|
2964
3747
|
return this.importKey(
|
|
2965
|
-
'raw',
|
|
3748
|
+
'raw-secret',
|
|
2966
3749
|
sharedKey,
|
|
2967
3750
|
sharedKeyAlgorithm,
|
|
2968
3751
|
extractable,
|
|
@@ -2973,8 +3756,14 @@ export class Subtle {
|
|
|
2973
3756
|
|
|
2974
3757
|
export const subtle = new Subtle();
|
|
2975
3758
|
|
|
2976
|
-
|
|
3759
|
+
// Returns the number of bits to derive for an `importKey` algorithm, mirroring
|
|
3760
|
+
// Node's webcrypto.js:269-306 `getKeyLength`. Returns null for KDF algorithms
|
|
3761
|
+
// (HKDF / PBKDF2 / Argon2) — those carry their full derived secret without a
|
|
3762
|
+
// fixed key length. Throws OperationError on invalid AES / HMAC inputs rather
|
|
3763
|
+
// than silently coercing to a default (Node commit 4cb1f284136 behavior).
|
|
3764
|
+
function getKeyLength(algorithm: SubtleAlgorithm): number | null {
|
|
2977
3765
|
const name = algorithm.name;
|
|
3766
|
+
const length = (algorithm as { length?: number }).length;
|
|
2978
3767
|
|
|
2979
3768
|
switch (name) {
|
|
2980
3769
|
case 'AES-CTR':
|
|
@@ -2982,18 +3771,38 @@ function getKeyLength(algorithm: SubtleAlgorithm): number {
|
|
|
2982
3771
|
case 'AES-GCM':
|
|
2983
3772
|
case 'AES-KW':
|
|
2984
3773
|
case 'AES-OCB':
|
|
2985
|
-
|
|
2986
|
-
|
|
3774
|
+
if (length !== 128 && length !== 192 && length !== 256) {
|
|
3775
|
+
throw lazyDOMException('Invalid key length', 'OperationError');
|
|
3776
|
+
}
|
|
3777
|
+
return length;
|
|
2987
3778
|
|
|
2988
3779
|
case 'HMAC': {
|
|
2989
|
-
|
|
2990
|
-
|
|
3780
|
+
if (length === undefined) {
|
|
3781
|
+
return getHmacBlockSize(
|
|
3782
|
+
(algorithm.hash as { name?: string } | undefined)?.name ??
|
|
3783
|
+
(algorithm.hash as string | undefined),
|
|
3784
|
+
);
|
|
3785
|
+
}
|
|
3786
|
+
if (typeof length === 'number' && length !== 0) {
|
|
3787
|
+
return length;
|
|
3788
|
+
}
|
|
3789
|
+
throw lazyDOMException('Invalid key length', 'OperationError');
|
|
2991
3790
|
}
|
|
2992
3791
|
|
|
2993
3792
|
case 'KMAC128':
|
|
2994
|
-
return
|
|
3793
|
+
return typeof length === 'number' ? length : 128;
|
|
2995
3794
|
case 'KMAC256':
|
|
2996
|
-
return
|
|
3795
|
+
return typeof length === 'number' ? length : 256;
|
|
3796
|
+
|
|
3797
|
+
case 'ChaCha20-Poly1305':
|
|
3798
|
+
return 256;
|
|
3799
|
+
|
|
3800
|
+
case 'HKDF':
|
|
3801
|
+
case 'PBKDF2':
|
|
3802
|
+
case 'Argon2d':
|
|
3803
|
+
case 'Argon2i':
|
|
3804
|
+
case 'Argon2id':
|
|
3805
|
+
return null;
|
|
2997
3806
|
|
|
2998
3807
|
default:
|
|
2999
3808
|
throw lazyDOMException(
|
|
@@ -3002,3 +3811,25 @@ function getKeyLength(algorithm: SubtleAlgorithm): number {
|
|
|
3002
3811
|
);
|
|
3003
3812
|
}
|
|
3004
3813
|
}
|
|
3814
|
+
|
|
3815
|
+
function getHmacBlockSize(name: string | undefined): number {
|
|
3816
|
+
switch (name) {
|
|
3817
|
+
case 'SHA-1':
|
|
3818
|
+
case 'SHA-256':
|
|
3819
|
+
return 512;
|
|
3820
|
+
case 'SHA-384':
|
|
3821
|
+
case 'SHA-512':
|
|
3822
|
+
return 1024;
|
|
3823
|
+
case 'SHA3-256':
|
|
3824
|
+
case 'SHA3-384':
|
|
3825
|
+
case 'SHA3-512':
|
|
3826
|
+
// SHA-3 / HMAC interaction undefined — Node throws here too
|
|
3827
|
+
// (webcrypto-modern-algos issue #23).
|
|
3828
|
+
throw lazyDOMException(
|
|
3829
|
+
'Explicit algorithm length member is required',
|
|
3830
|
+
'NotSupportedError',
|
|
3831
|
+
);
|
|
3832
|
+
default:
|
|
3833
|
+
throw lazyDOMException('Invalid key length', 'OperationError');
|
|
3834
|
+
}
|
|
3835
|
+
}
|