@ccheever/exact-ibex-runtime 0.1.0
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/package.json +63 -0
- package/src/abort/AbortController.ts +23 -0
- package/src/abort/AbortSignal.ts +152 -0
- package/src/abort/index.ts +2 -0
- package/src/accessibility.ts +12 -0
- package/src/arraybuffer-detach.ts +109 -0
- package/src/base64/base64.ts +168 -0
- package/src/base64/index.ts +1 -0
- package/src/blob/Blob.ts +259 -0
- package/src/blob/File.ts +59 -0
- package/src/blob/FormData.ts +323 -0
- package/src/blob/index.ts +3 -0
- package/src/bootstrap.ts +1946 -0
- package/src/broadcast/BroadcastChannel.ts +280 -0
- package/src/broadcast/index.ts +5 -0
- package/src/cache/Cache.ts +349 -0
- package/src/cache/CacheStorage.ts +89 -0
- package/src/cache/index.ts +27 -0
- package/src/camera/index.ts +6202 -0
- package/src/camera/processor.worker.ts +194 -0
- package/src/camera/scene.ts +195 -0
- package/src/clipboard/Clipboard.ts +129 -0
- package/src/clipboard/ClipboardItem.ts +97 -0
- package/src/clipboard/index.ts +6 -0
- package/src/clone/index.ts +1 -0
- package/src/clone/structuredClone.ts +389 -0
- package/src/clone/transferableSymbols.ts +2 -0
- package/src/compression/CompressionStream.ts +146 -0
- package/src/compression/DecompressionStream.ts +342 -0
- package/src/compression/index.ts +4 -0
- package/src/console/Console.ts +341 -0
- package/src/console/index.ts +2 -0
- package/src/core/accessibility-state.ts +263 -0
- package/src/core/accessibility.ts +184 -0
- package/src/core/agent-state.ts +37 -0
- package/src/core/diagnostics-logs.ts +144 -0
- package/src/core/host-call-bridge.ts +16 -0
- package/src/core/i18n-helpers.ts +189 -0
- package/src/core/locale-state.ts +253 -0
- package/src/core/locale.ts +95 -0
- package/src/crypto/Crypto.ts +2743 -0
- package/src/crypto/index.ts +1 -0
- package/src/diagnostics/logs.ts +7 -0
- package/src/encoding/TextDecoder.ts +1181 -0
- package/src/encoding/TextDecoderStream.ts +58 -0
- package/src/encoding/TextEncoder.ts +180 -0
- package/src/encoding/TextEncoderStream.ts +39 -0
- package/src/encoding/index.ts +8 -0
- package/src/events/CloseEvent.ts +91 -0
- package/src/events/DOMException.ts +409 -0
- package/src/events/ErrorEvent.ts +39 -0
- package/src/events/Event.ts +151 -0
- package/src/events/EventTarget.ts +280 -0
- package/src/events/FocusEvent.ts +27 -0
- package/src/events/KeyboardEvent.ts +46 -0
- package/src/events/MessageEvent.ts +61 -0
- package/src/events/ProgressEvent.ts +33 -0
- package/src/events/PromiseRejectionEvent.ts +31 -0
- package/src/events/index.ts +52 -0
- package/src/eventsource/EventSource.ts +371 -0
- package/src/eventsource/index.ts +2 -0
- package/src/fetch/Headers.ts +642 -0
- package/src/fetch/Request.ts +760 -0
- package/src/fetch/Response.ts +543 -0
- package/src/fetch/body.ts +1256 -0
- package/src/fetch/cookie-jar.ts +566 -0
- package/src/fetch/demo.ts +207 -0
- package/src/fetch/errors.ts +101 -0
- package/src/fetch/fetch.ts +2610 -0
- package/src/fetch/index.ts +101 -0
- package/src/fetch/native-bridge.ts +65 -0
- package/src/fetch/types.ts +258 -0
- package/src/filereader/FileReader.ts +236 -0
- package/src/filereader/index.ts +1 -0
- package/src/fs/Dirent.ts +39 -0
- package/src/fs/ExactFile.ts +450 -0
- package/src/fs/Stats.ts +80 -0
- package/src/fs/index.ts +944 -0
- package/src/fs/promises.ts +386 -0
- package/src/fs/shared.ts +328 -0
- package/src/http-server/index.js +697 -0
- package/src/http-server/index.ts +27 -0
- package/src/identity.generated.ts +14 -0
- package/src/index.ts +283 -0
- package/src/indexeddb/IDBCursor.ts +188 -0
- package/src/indexeddb/IDBDatabase.ts +343 -0
- package/src/indexeddb/IDBFactory.ts +269 -0
- package/src/indexeddb/IDBIndex.ts +194 -0
- package/src/indexeddb/IDBKeyRange.ts +109 -0
- package/src/indexeddb/IDBObjectStore.ts +468 -0
- package/src/indexeddb/IDBRequest.ts +163 -0
- package/src/indexeddb/IDBTransaction.ts +207 -0
- package/src/indexeddb/index.ts +34 -0
- package/src/indexeddb/utils.ts +52 -0
- package/src/inspect/index.ts +1 -0
- package/src/inspect/inspect.ts +465 -0
- package/src/internal/detect.ts +104 -0
- package/src/locale.ts +10 -0
- package/src/location/index.ts +1059 -0
- package/src/locks/LockManager.ts +460 -0
- package/src/locks/index.ts +12 -0
- package/src/media/VideoFrame.ts +58 -0
- package/src/messaging/MessageChannel.ts +31 -0
- package/src/messaging/MessagePort.ts +180 -0
- package/src/messaging/index.ts +2 -0
- package/src/messaging.ts +247 -0
- package/src/native/NativeModules.ts +354 -0
- package/src/native/index.ts +1 -0
- package/src/navigator/Navigator.ts +351 -0
- package/src/navigator/index.ts +1 -0
- package/src/node/Buffer.ts +1786 -0
- package/src/node/index.ts +4 -0
- package/src/node/path.ts +495 -0
- package/src/node/process.ts +2528 -0
- package/src/performance/Performance.ts +532 -0
- package/src/performance/index.ts +21 -0
- package/src/polyfills/array.ts +236 -0
- package/src/polyfills/arraybuffer.ts +172 -0
- package/src/polyfills/groupby.ts +85 -0
- package/src/polyfills/index.ts +85 -0
- package/src/polyfills/intl.ts +1956 -0
- package/src/polyfills/iterator.ts +479 -0
- package/src/polyfills/promise.ts +37 -0
- package/src/polyfills/set.ts +245 -0
- package/src/polyfills/string.ts +85 -0
- package/src/polyfills/typedarray.ts +110 -0
- package/src/promise-rejection-tracking.ts +464 -0
- package/src/react-native/index.ts +388 -0
- package/src/runtime-entry.ts +55 -0
- package/src/scheduling/AnimationFrame.ts +105 -0
- package/src/scheduling/IdleCallback.ts +167 -0
- package/src/scheduling/index.ts +13 -0
- package/src/security/Capabilities.ts +1146 -0
- package/src/security/Permissions.ts +392 -0
- package/src/security/capability-bits.generated.ts +63 -0
- package/src/security/index.ts +16 -0
- package/src/sqlite/Database.ts +456 -0
- package/src/sqlite/Statement.ts +206 -0
- package/src/sqlite/constants.ts +79 -0
- package/src/sqlite/errors.ts +25 -0
- package/src/sqlite/index.ts +34 -0
- package/src/sqlite/module.js +438 -0
- package/src/storage/Storage.ts +291 -0
- package/src/storage/StorageManager.ts +91 -0
- package/src/storage/index.ts +3 -0
- package/src/stream-compat.ts +47 -0
- package/src/streams/ReadableStream.ts +4131 -0
- package/src/streams/TransformStream.ts +375 -0
- package/src/streams/WritableStream.ts +866 -0
- package/src/streams/index.ts +41 -0
- package/src/timers/Timers.ts +296 -0
- package/src/timers/index.ts +11 -0
- package/src/url/URL.ts +656 -0
- package/src/url/URLPattern.ts +850 -0
- package/src/url/URLSearchParams.ts +244 -0
- package/src/url/index.ts +9 -0
- package/src/websocket/WebSocket.ts +770 -0
- package/src/websocket/WebSocketError.ts +52 -0
- package/src/websocket/WebSocketStream.ts +628 -0
- package/src/websocket/index.ts +7 -0
- package/src/window/index.ts +872 -0
|
@@ -0,0 +1,2743 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Crypto - Web Crypto API Implementation
|
|
4
|
+
*
|
|
5
|
+
* @see https://www.w3.org/TR/WebCryptoAPI/
|
|
6
|
+
*
|
|
7
|
+
* Supported algorithms:
|
|
8
|
+
* - Digest: SHA-1, SHA-256, SHA-384, SHA-512
|
|
9
|
+
* - Encrypt/Decrypt: AES-GCM, AES-CBC, AES-CTR, RSA-OAEP
|
|
10
|
+
* - Sign/Verify: HMAC, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, Ed25519
|
|
11
|
+
* - Key generation: AES-GCM, AES-CBC, AES-CTR, HMAC, RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, ECDH, Ed25519, X25519
|
|
12
|
+
* - Key derivation: PBKDF2, HKDF, X25519
|
|
13
|
+
* - Key formats: raw, jwk, spki, pkcs8
|
|
14
|
+
*
|
|
15
|
+
* Note: This uses native crypto when available, with JS fallbacks for testing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { getNativeCryptoModule } from "../native/NativeModules";
|
|
19
|
+
import { requireCapability, Capabilities, isSilentMode } from "../security/Capabilities";
|
|
20
|
+
|
|
21
|
+
// Declare native crypto bridge functions
|
|
22
|
+
declare global {
|
|
23
|
+
// AES operations (algorithm-specific native bridges)
|
|
24
|
+
var __exactAesCbcEncrypt: ((
|
|
25
|
+
key: Uint8Array,
|
|
26
|
+
iv: Uint8Array,
|
|
27
|
+
data: Uint8Array
|
|
28
|
+
) => Uint8Array) | undefined;
|
|
29
|
+
|
|
30
|
+
var __exactAesCbcDecrypt: ((
|
|
31
|
+
key: Uint8Array,
|
|
32
|
+
iv: Uint8Array,
|
|
33
|
+
data: Uint8Array
|
|
34
|
+
) => Uint8Array) | undefined;
|
|
35
|
+
|
|
36
|
+
var __exactAesCtrEncrypt: ((
|
|
37
|
+
key: Uint8Array,
|
|
38
|
+
counter: Uint8Array,
|
|
39
|
+
data: Uint8Array
|
|
40
|
+
) => Uint8Array) | undefined;
|
|
41
|
+
|
|
42
|
+
var __exactAesGcmEncrypt: ((
|
|
43
|
+
key: Uint8Array,
|
|
44
|
+
iv: Uint8Array,
|
|
45
|
+
data: Uint8Array,
|
|
46
|
+
aad?: Uint8Array,
|
|
47
|
+
tagLength?: number
|
|
48
|
+
) => Uint8Array) | undefined;
|
|
49
|
+
|
|
50
|
+
var __exactAesGcmDecrypt: ((
|
|
51
|
+
key: Uint8Array,
|
|
52
|
+
iv: Uint8Array,
|
|
53
|
+
data: Uint8Array,
|
|
54
|
+
aad?: Uint8Array,
|
|
55
|
+
tagLength?: number
|
|
56
|
+
) => Uint8Array) | undefined;
|
|
57
|
+
|
|
58
|
+
// HMAC operation (returns hex string, not bytes)
|
|
59
|
+
var __exactHmacSync: ((
|
|
60
|
+
algorithm: string,
|
|
61
|
+
key: string,
|
|
62
|
+
data: string
|
|
63
|
+
) => string) | undefined;
|
|
64
|
+
|
|
65
|
+
// RSA sign/verify (expects PEM key strings, data as string)
|
|
66
|
+
var __exactSignSync: ((
|
|
67
|
+
algorithm: string,
|
|
68
|
+
data: string,
|
|
69
|
+
key: string
|
|
70
|
+
) => Uint8Array) | undefined;
|
|
71
|
+
|
|
72
|
+
var __exactVerifySync: ((
|
|
73
|
+
algorithm: string,
|
|
74
|
+
signature: Uint8Array,
|
|
75
|
+
data: string,
|
|
76
|
+
key: string
|
|
77
|
+
) => boolean) | undefined;
|
|
78
|
+
|
|
79
|
+
// Key generation (returns PEM strings for asymmetric keys)
|
|
80
|
+
var __exactGenerateKeyPairSync: ((
|
|
81
|
+
keyType: string,
|
|
82
|
+
options?: Record<string, any>
|
|
83
|
+
) => { publicKey: string; privateKey: string }) | undefined;
|
|
84
|
+
|
|
85
|
+
// Key derivation
|
|
86
|
+
var __exactPbkdf2: ((
|
|
87
|
+
password: Uint8Array,
|
|
88
|
+
salt: Uint8Array,
|
|
89
|
+
iterations: number,
|
|
90
|
+
keyLength: number,
|
|
91
|
+
hashAlgo: string
|
|
92
|
+
) => Uint8Array) | undefined;
|
|
93
|
+
|
|
94
|
+
var __exactHkdf: ((
|
|
95
|
+
hashAlgo: string,
|
|
96
|
+
ikm: Uint8Array,
|
|
97
|
+
salt: Uint8Array,
|
|
98
|
+
info: Uint8Array,
|
|
99
|
+
length: number
|
|
100
|
+
) => Uint8Array) | undefined;
|
|
101
|
+
|
|
102
|
+
// Ed25519 operations
|
|
103
|
+
var __exactEd25519Sign: ((
|
|
104
|
+
privateKey: Uint8Array,
|
|
105
|
+
data: Uint8Array
|
|
106
|
+
) => Uint8Array) | undefined;
|
|
107
|
+
|
|
108
|
+
var __exactEd25519Verify: ((
|
|
109
|
+
publicKey: Uint8Array,
|
|
110
|
+
signature: Uint8Array,
|
|
111
|
+
data: Uint8Array
|
|
112
|
+
) => boolean) | undefined;
|
|
113
|
+
|
|
114
|
+
// ECDSA operations
|
|
115
|
+
var __exactEcdsaSign: ((
|
|
116
|
+
curve: string,
|
|
117
|
+
hash: string,
|
|
118
|
+
privateKey: Uint8Array,
|
|
119
|
+
data: Uint8Array
|
|
120
|
+
) => Uint8Array) | undefined;
|
|
121
|
+
|
|
122
|
+
var __exactEcdsaVerify: ((
|
|
123
|
+
curve: string,
|
|
124
|
+
hash: string,
|
|
125
|
+
publicKey: Uint8Array,
|
|
126
|
+
signature: Uint8Array,
|
|
127
|
+
data: Uint8Array
|
|
128
|
+
) => boolean) | undefined;
|
|
129
|
+
|
|
130
|
+
// X25519 key agreement
|
|
131
|
+
var __exactX25519DeriveBits: ((
|
|
132
|
+
privateKey: Uint8Array,
|
|
133
|
+
publicKey: Uint8Array
|
|
134
|
+
) => Uint8Array) | undefined;
|
|
135
|
+
|
|
136
|
+
// ECDH key agreement for NIST curves
|
|
137
|
+
var __exactEcdhDeriveBits: ((
|
|
138
|
+
curve: string,
|
|
139
|
+
privateKey: Uint8Array,
|
|
140
|
+
publicKey: Uint8Array
|
|
141
|
+
) => Uint8Array) | undefined;
|
|
142
|
+
|
|
143
|
+
// RSA-OAEP encrypt/decrypt
|
|
144
|
+
var __exactRsaOaepEncrypt: ((
|
|
145
|
+
publicKeyData: Uint8Array,
|
|
146
|
+
hashAlgorithm: string,
|
|
147
|
+
label: Uint8Array,
|
|
148
|
+
plaintext: Uint8Array
|
|
149
|
+
) => Uint8Array) | undefined;
|
|
150
|
+
|
|
151
|
+
var __exactRsaOaepDecrypt: ((
|
|
152
|
+
privateKeyData: Uint8Array,
|
|
153
|
+
hashAlgorithm: string,
|
|
154
|
+
label: Uint8Array,
|
|
155
|
+
ciphertext: Uint8Array
|
|
156
|
+
) => Uint8Array) | undefined;
|
|
157
|
+
|
|
158
|
+
// SPKI/PKCS8 key format support
|
|
159
|
+
var __exactExportKeySpki: ((
|
|
160
|
+
keyType: string,
|
|
161
|
+
keyData: Uint8Array
|
|
162
|
+
) => Uint8Array) | undefined;
|
|
163
|
+
|
|
164
|
+
var __exactExportKeyPkcs8: ((
|
|
165
|
+
keyType: string,
|
|
166
|
+
keyData: Uint8Array
|
|
167
|
+
) => Uint8Array) | undefined;
|
|
168
|
+
|
|
169
|
+
var __exactImportKeySpki: ((
|
|
170
|
+
keyData: Uint8Array
|
|
171
|
+
) => { keyType: string; algorithm: string; rawKeyData: Uint8Array }) | undefined;
|
|
172
|
+
|
|
173
|
+
var __exactImportKeyPkcs8: ((
|
|
174
|
+
keyData: Uint8Array
|
|
175
|
+
) => { keyType: string; algorithm: string; rawKeyData: Uint8Array }) | undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Wrap native bridge errors as DOMException with the appropriate name.
|
|
180
|
+
* Native __exact* functions throw generic Error; WPT expects DOMException.
|
|
181
|
+
*/
|
|
182
|
+
function wrapNativeError(e: unknown, defaultName: string): DOMException {
|
|
183
|
+
if (e instanceof DOMException) return e;
|
|
184
|
+
const msg = (e instanceof Error) ? e.message : String(e);
|
|
185
|
+
if (/not supported|unsupported|not available|unrecognized/i.test(msg)) {
|
|
186
|
+
return new DOMException(msg, 'NotSupportedError');
|
|
187
|
+
}
|
|
188
|
+
if (/invalid key|bad key/i.test(msg)) {
|
|
189
|
+
return new DOMException(msg, 'DataError');
|
|
190
|
+
}
|
|
191
|
+
if (/not extractable|usage/i.test(msg)) {
|
|
192
|
+
return new DOMException(msg, 'InvalidAccessError');
|
|
193
|
+
}
|
|
194
|
+
return new DOMException(msg, defaultName);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class SubtleCrypto {
|
|
198
|
+
/**
|
|
199
|
+
* Check capability before any subtle crypto operation.
|
|
200
|
+
* @throws NotAllowedError if capability is not granted
|
|
201
|
+
*/
|
|
202
|
+
#checkCapability(): void {
|
|
203
|
+
requireCapability(Capabilities.CRYPTO_SUBTLE);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Generate a cryptographic digest (hash)
|
|
208
|
+
*/
|
|
209
|
+
async digest(
|
|
210
|
+
algorithm: AlgorithmIdentifier,
|
|
211
|
+
data: BufferSource
|
|
212
|
+
): Promise<ArrayBuffer> {
|
|
213
|
+
this.#checkCapability();
|
|
214
|
+
const alg = typeof algorithm === "string" ? algorithm : algorithm.name;
|
|
215
|
+
const normalizedAlg = normalizeHashName(alg);
|
|
216
|
+
const bytes = toUint8Array(data);
|
|
217
|
+
|
|
218
|
+
const native = getNativeCryptoModule();
|
|
219
|
+
if (native) {
|
|
220
|
+
try {
|
|
221
|
+
switch (normalizedAlg) {
|
|
222
|
+
case "SHA-1": {
|
|
223
|
+
if (native.digest) return await native.digest(normalizedAlg, bytes);
|
|
224
|
+
return await native.sha1(bytes);
|
|
225
|
+
}
|
|
226
|
+
case "SHA-256": {
|
|
227
|
+
if (native.digest) return await native.digest(normalizedAlg, bytes);
|
|
228
|
+
return await native.sha256(bytes);
|
|
229
|
+
}
|
|
230
|
+
case "SHA-384": {
|
|
231
|
+
if (native.digest) return await native.digest(normalizedAlg, bytes);
|
|
232
|
+
return await native.sha384(bytes);
|
|
233
|
+
}
|
|
234
|
+
case "SHA-512": {
|
|
235
|
+
if (native.digest) return await native.digest(normalizedAlg, bytes);
|
|
236
|
+
return await native.sha512(bytes);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch (_nativeErr) {
|
|
240
|
+
// Native threw (e.g., stub not implemented), fall through to fallback path
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Try platform crypto (Bun/Node/browser globalThis.crypto.subtle) if available
|
|
245
|
+
const platformSubtle = getPlatformSubtle();
|
|
246
|
+
if (platformSubtle) {
|
|
247
|
+
try {
|
|
248
|
+
return await platformSubtle.digest(algorithm, bytes);
|
|
249
|
+
} catch (_platformErr) {
|
|
250
|
+
// Platform crypto failed, fall through to pure JS
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Pure JS fallback for supported hashes
|
|
255
|
+
switch (normalizedAlg) {
|
|
256
|
+
case "SHA-1":
|
|
257
|
+
return jsSHA1(bytes);
|
|
258
|
+
case "SHA-256":
|
|
259
|
+
return jsSHA256(bytes);
|
|
260
|
+
case "SHA-384":
|
|
261
|
+
return jsSHA384(bytes);
|
|
262
|
+
case "SHA-512":
|
|
263
|
+
return jsSHA512(bytes);
|
|
264
|
+
default:
|
|
265
|
+
throw new DOMException(
|
|
266
|
+
`Unrecognized algorithm name: ${alg}`,
|
|
267
|
+
"NotSupportedError"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Encrypt data using the specified algorithm and key
|
|
274
|
+
*/
|
|
275
|
+
async encrypt(
|
|
276
|
+
algorithm: AlgorithmIdentifier | AesCbcParams | AesCtrParams | AesGcmParams | RsaOaepParams,
|
|
277
|
+
key: CryptoKey,
|
|
278
|
+
data: BufferSource
|
|
279
|
+
): Promise<ArrayBuffer> {
|
|
280
|
+
this.#checkCapability();
|
|
281
|
+
validateKey(key, 'encrypt');
|
|
282
|
+
const bytes = toUint8Array(data);
|
|
283
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
284
|
+
|
|
285
|
+
switch (alg.name.toUpperCase()) {
|
|
286
|
+
case 'AES-GCM': {
|
|
287
|
+
const params = algorithm as AesGcmParams;
|
|
288
|
+
const iv = toUint8Array(params.iv);
|
|
289
|
+
const additionalData = params.additionalData ? toUint8Array(params.additionalData) : undefined;
|
|
290
|
+
const tagLength = params.tagLength ?? 128;
|
|
291
|
+
|
|
292
|
+
if (typeof __exactAesGcmEncrypt === 'function') {
|
|
293
|
+
try {
|
|
294
|
+
const result = __exactAesGcmEncrypt((key as ExactCryptoKey)._keyData, iv, bytes, additionalData, tagLength);
|
|
295
|
+
return uint8ArrayToArrayBuffer(result);
|
|
296
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
297
|
+
}
|
|
298
|
+
throw new DOMException('Native crypto not available for AES-GCM', 'NotSupportedError');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case 'AES-CBC': {
|
|
302
|
+
const params = algorithm as AesCbcParams;
|
|
303
|
+
const iv = toUint8Array(params.iv);
|
|
304
|
+
|
|
305
|
+
if (typeof __exactAesCbcEncrypt === 'function') {
|
|
306
|
+
try {
|
|
307
|
+
const result = __exactAesCbcEncrypt((key as ExactCryptoKey)._keyData, iv, bytes);
|
|
308
|
+
return uint8ArrayToArrayBuffer(result);
|
|
309
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
310
|
+
}
|
|
311
|
+
throw new DOMException('Native crypto not available for AES-CBC', 'NotSupportedError');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
case 'AES-CTR': {
|
|
315
|
+
const params = algorithm as AesCtrParams;
|
|
316
|
+
const counter = toUint8Array(params.counter);
|
|
317
|
+
|
|
318
|
+
if (typeof __exactAesCtrEncrypt === 'function') {
|
|
319
|
+
try {
|
|
320
|
+
const result = __exactAesCtrEncrypt((key as ExactCryptoKey)._keyData, counter, bytes);
|
|
321
|
+
return uint8ArrayToArrayBuffer(result);
|
|
322
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
323
|
+
}
|
|
324
|
+
throw new DOMException('Native crypto not available for AES-CTR', 'NotSupportedError');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
case 'RSA-OAEP': {
|
|
328
|
+
const params = algorithm as RsaOaepParams;
|
|
329
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash?.name || 'SHA-256';
|
|
330
|
+
const label = params.label ? toUint8Array(params.label) : new Uint8Array(0);
|
|
331
|
+
|
|
332
|
+
if (typeof __exactRsaOaepEncrypt === 'function') {
|
|
333
|
+
try {
|
|
334
|
+
const result = __exactRsaOaepEncrypt((key as ExactCryptoKey)._keyData, hash, label, bytes);
|
|
335
|
+
return uint8ArrayToArrayBuffer(result);
|
|
336
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
337
|
+
}
|
|
338
|
+
throw new DOMException('Native crypto not available for RSA-OAEP', 'NotSupportedError');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
default:
|
|
342
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Decrypt data using the specified algorithm and key
|
|
348
|
+
*/
|
|
349
|
+
async decrypt(
|
|
350
|
+
algorithm: AlgorithmIdentifier | AesCbcParams | AesCtrParams | AesGcmParams | RsaOaepParams,
|
|
351
|
+
key: CryptoKey,
|
|
352
|
+
data: BufferSource
|
|
353
|
+
): Promise<ArrayBuffer> {
|
|
354
|
+
this.#checkCapability();
|
|
355
|
+
validateKey(key, 'decrypt');
|
|
356
|
+
const bytes = toUint8Array(data);
|
|
357
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
358
|
+
|
|
359
|
+
switch (alg.name.toUpperCase()) {
|
|
360
|
+
case 'AES-GCM': {
|
|
361
|
+
const params = algorithm as AesGcmParams;
|
|
362
|
+
const iv = toUint8Array(params.iv);
|
|
363
|
+
const additionalData = params.additionalData ? toUint8Array(params.additionalData) : undefined;
|
|
364
|
+
const tagLength = params.tagLength ?? 128;
|
|
365
|
+
|
|
366
|
+
if (typeof __exactAesGcmDecrypt === 'function') {
|
|
367
|
+
try {
|
|
368
|
+
const result = __exactAesGcmDecrypt((key as ExactCryptoKey)._keyData, iv, bytes, additionalData, tagLength);
|
|
369
|
+
return uint8ArrayToArrayBuffer(result);
|
|
370
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
371
|
+
}
|
|
372
|
+
throw new DOMException('Native crypto not available for AES-GCM', 'NotSupportedError');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
case 'AES-CBC': {
|
|
376
|
+
const params = algorithm as AesCbcParams;
|
|
377
|
+
const iv = toUint8Array(params.iv);
|
|
378
|
+
|
|
379
|
+
if (typeof __exactAesCbcDecrypt === 'function') {
|
|
380
|
+
try {
|
|
381
|
+
const result = __exactAesCbcDecrypt((key as ExactCryptoKey)._keyData, iv, bytes);
|
|
382
|
+
return uint8ArrayToArrayBuffer(result);
|
|
383
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
384
|
+
}
|
|
385
|
+
throw new DOMException('Native crypto not available for AES-CBC', 'NotSupportedError');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
case 'AES-CTR': {
|
|
389
|
+
const params = algorithm as AesCtrParams;
|
|
390
|
+
const counter = toUint8Array(params.counter);
|
|
391
|
+
|
|
392
|
+
// CTR mode: encrypt and decrypt are the same operation
|
|
393
|
+
if (typeof __exactAesCtrEncrypt === 'function') {
|
|
394
|
+
try {
|
|
395
|
+
const result = __exactAesCtrEncrypt((key as ExactCryptoKey)._keyData, counter, bytes);
|
|
396
|
+
return uint8ArrayToArrayBuffer(result);
|
|
397
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
398
|
+
}
|
|
399
|
+
throw new DOMException('Native crypto not available for AES-CTR', 'NotSupportedError');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
case 'RSA-OAEP': {
|
|
403
|
+
const params = algorithm as RsaOaepParams;
|
|
404
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash?.name || 'SHA-256';
|
|
405
|
+
const label = params.label ? toUint8Array(params.label) : new Uint8Array(0);
|
|
406
|
+
|
|
407
|
+
if (typeof __exactRsaOaepDecrypt === 'function') {
|
|
408
|
+
try {
|
|
409
|
+
const result = __exactRsaOaepDecrypt((key as ExactCryptoKey)._keyData, hash, label, bytes);
|
|
410
|
+
return uint8ArrayToArrayBuffer(result);
|
|
411
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
412
|
+
}
|
|
413
|
+
throw new DOMException('Native crypto not available for RSA-OAEP', 'NotSupportedError');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
default:
|
|
417
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Sign data using the specified algorithm and key
|
|
423
|
+
*/
|
|
424
|
+
async sign(
|
|
425
|
+
algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams,
|
|
426
|
+
key: CryptoKey,
|
|
427
|
+
data: BufferSource
|
|
428
|
+
): Promise<ArrayBuffer> {
|
|
429
|
+
this.#checkCapability();
|
|
430
|
+
validateKey(key, 'sign');
|
|
431
|
+
const bytes = toUint8Array(data);
|
|
432
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
433
|
+
const native = getNativeCryptoModule();
|
|
434
|
+
|
|
435
|
+
switch (alg.name.toUpperCase()) {
|
|
436
|
+
case 'HMAC': {
|
|
437
|
+
const hash = normalizeHashName((key.algorithm as HmacKeyAlgorithm).hash.name);
|
|
438
|
+
|
|
439
|
+
if (native?.hmacSign) {
|
|
440
|
+
return native.hmacSign(hash, (key as ExactCryptoKey)._keyData, bytes);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (typeof __exactHmacSync === 'function') {
|
|
444
|
+
// __exactHmacSync takes (algorithm, keyString, dataString) and returns a hex string
|
|
445
|
+
const keyData = (key as ExactCryptoKey)._keyData;
|
|
446
|
+
const keyStr = uint8ArrayToString(keyData);
|
|
447
|
+
const dataStr = uint8ArrayToString(bytes);
|
|
448
|
+
const hashAlgo = hash.toLowerCase().replace('-', '');
|
|
449
|
+
const hex = __exactHmacSync(hashAlgo, keyStr, dataStr);
|
|
450
|
+
return hexStringToArrayBuffer(hex);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return jsHmac((key as ExactCryptoKey)._keyData, bytes, hash);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
case 'RSASSA-PKCS1-V1_5': {
|
|
457
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
458
|
+
|
|
459
|
+
if (typeof __exactSignSync === 'function') {
|
|
460
|
+
try {
|
|
461
|
+
const keyStr = uint8ArrayToString((key as ExactCryptoKey)._keyData);
|
|
462
|
+
const dataStr = uint8ArrayToString(bytes);
|
|
463
|
+
const result = __exactSignSync(hash, dataStr, keyStr);
|
|
464
|
+
return uint8ArrayToArrayBuffer(result);
|
|
465
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
466
|
+
}
|
|
467
|
+
throw new DOMException('Native crypto not available for RSASSA-PKCS1-v1_5', 'NotSupportedError');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
case 'RSA-PSS': {
|
|
471
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
472
|
+
|
|
473
|
+
if (typeof __exactSignSync === 'function') {
|
|
474
|
+
try {
|
|
475
|
+
const keyStr = uint8ArrayToString((key as ExactCryptoKey)._keyData);
|
|
476
|
+
const dataStr = uint8ArrayToString(bytes);
|
|
477
|
+
const result = __exactSignSync(hash, dataStr, keyStr);
|
|
478
|
+
return uint8ArrayToArrayBuffer(result);
|
|
479
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
480
|
+
}
|
|
481
|
+
throw new DOMException('Native crypto not available for RSA-PSS', 'NotSupportedError');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
case 'ECDSA': {
|
|
485
|
+
const params = algorithm as EcdsaParams;
|
|
486
|
+
const hash = typeof params.hash === 'string' ? params.hash : params.hash.name;
|
|
487
|
+
const curve = (key.algorithm as EcKeyAlgorithm).namedCurve;
|
|
488
|
+
|
|
489
|
+
if (typeof __exactEcdsaSign === 'function') {
|
|
490
|
+
try {
|
|
491
|
+
const result = __exactEcdsaSign(curve, hash, (key as ExactCryptoKey)._keyData, bytes);
|
|
492
|
+
return uint8ArrayToArrayBuffer(result);
|
|
493
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
494
|
+
}
|
|
495
|
+
throw new DOMException('Native crypto not available for ECDSA', 'NotSupportedError');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
case 'ED25519':
|
|
499
|
+
case 'EDDSA': {
|
|
500
|
+
if (typeof __exactEd25519Sign === 'function') {
|
|
501
|
+
try {
|
|
502
|
+
// If key data is 64 bytes (d[32] || x[32] from JWK import), pass only d portion
|
|
503
|
+
const keyData = (key as ExactCryptoKey)._keyData;
|
|
504
|
+
const privKeyData = keyData.length === 64 ? keyData.slice(0, 32) : keyData;
|
|
505
|
+
const result = __exactEd25519Sign(privKeyData, bytes);
|
|
506
|
+
return uint8ArrayToArrayBuffer(result);
|
|
507
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
508
|
+
}
|
|
509
|
+
throw new DOMException('Native crypto not available for Ed25519', 'NotSupportedError');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
default:
|
|
513
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Verify a signature using the specified algorithm and key
|
|
519
|
+
*/
|
|
520
|
+
async verify(
|
|
521
|
+
algorithm: AlgorithmIdentifier | RsaPssParams | EcdsaParams,
|
|
522
|
+
key: CryptoKey,
|
|
523
|
+
signature: BufferSource,
|
|
524
|
+
data: BufferSource
|
|
525
|
+
): Promise<boolean> {
|
|
526
|
+
this.#checkCapability();
|
|
527
|
+
validateKey(key, 'verify');
|
|
528
|
+
const signatureBytes = toUint8Array(signature);
|
|
529
|
+
const dataBytes = toUint8Array(data);
|
|
530
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
531
|
+
const native = getNativeCryptoModule();
|
|
532
|
+
|
|
533
|
+
switch (alg.name.toUpperCase()) {
|
|
534
|
+
case 'HMAC': {
|
|
535
|
+
const hash = normalizeHashName((key.algorithm as HmacKeyAlgorithm).hash.name);
|
|
536
|
+
|
|
537
|
+
if (native?.hmacVerify) {
|
|
538
|
+
return native.hmacVerify(hash, (key as ExactCryptoKey)._keyData, signatureBytes, dataBytes);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (typeof __exactHmacSync === 'function') {
|
|
542
|
+
// Compute HMAC and compare with provided signature
|
|
543
|
+
const keyData = (key as ExactCryptoKey)._keyData;
|
|
544
|
+
const keyStr = uint8ArrayToString(keyData);
|
|
545
|
+
const dataStr = uint8ArrayToString(dataBytes);
|
|
546
|
+
const hashAlgo = hash.toLowerCase().replace('-', '');
|
|
547
|
+
const hex = __exactHmacSync(hashAlgo, keyStr, dataStr);
|
|
548
|
+
const expectedSig = new Uint8Array(hex.length / 2);
|
|
549
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
550
|
+
expectedSig[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
551
|
+
}
|
|
552
|
+
return constantTimeCompare(expectedSig, signatureBytes);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const expectedSig = await jsHmac((key as ExactCryptoKey)._keyData, dataBytes, hash);
|
|
556
|
+
return constantTimeCompare(new Uint8Array(expectedSig), signatureBytes);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
case 'RSASSA-PKCS1-V1_5': {
|
|
560
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
561
|
+
|
|
562
|
+
if (typeof __exactVerifySync === 'function') {
|
|
563
|
+
try {
|
|
564
|
+
const keyStr = uint8ArrayToString((key as ExactCryptoKey)._keyData);
|
|
565
|
+
const dataStr = uint8ArrayToString(dataBytes);
|
|
566
|
+
return __exactVerifySync(hash, signatureBytes, dataStr, keyStr);
|
|
567
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
568
|
+
}
|
|
569
|
+
throw new DOMException('Native crypto not available for RSASSA-PKCS1-v1_5', 'NotSupportedError');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
case 'RSA-PSS': {
|
|
573
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash.name;
|
|
574
|
+
|
|
575
|
+
if (typeof __exactVerifySync === 'function') {
|
|
576
|
+
try {
|
|
577
|
+
const keyStr = uint8ArrayToString((key as ExactCryptoKey)._keyData);
|
|
578
|
+
const dataStr = uint8ArrayToString(dataBytes);
|
|
579
|
+
return __exactVerifySync(hash, signatureBytes, dataStr, keyStr);
|
|
580
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
581
|
+
}
|
|
582
|
+
throw new DOMException('Native crypto not available for RSA-PSS', 'NotSupportedError');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
case 'ECDSA': {
|
|
586
|
+
const params = algorithm as EcdsaParams;
|
|
587
|
+
const hash = typeof params.hash === 'string' ? params.hash : params.hash.name;
|
|
588
|
+
const curve = (key.algorithm as EcKeyAlgorithm).namedCurve;
|
|
589
|
+
|
|
590
|
+
if (typeof __exactEcdsaVerify === 'function') {
|
|
591
|
+
try {
|
|
592
|
+
return __exactEcdsaVerify(curve, hash, (key as ExactCryptoKey)._keyData, signatureBytes, dataBytes);
|
|
593
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
594
|
+
}
|
|
595
|
+
throw new DOMException('Native crypto not available for ECDSA', 'NotSupportedError');
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
case 'ED25519':
|
|
599
|
+
case 'EDDSA': {
|
|
600
|
+
if (typeof __exactEd25519Verify === 'function') {
|
|
601
|
+
try {
|
|
602
|
+
return __exactEd25519Verify((key as ExactCryptoKey)._keyData, signatureBytes, dataBytes);
|
|
603
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
604
|
+
}
|
|
605
|
+
throw new DOMException('Native crypto not available for Ed25519', 'NotSupportedError');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
default:
|
|
609
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Generate a new key or key pair
|
|
615
|
+
*/
|
|
616
|
+
async generateKey(
|
|
617
|
+
algorithm: RsaHashedKeyGenParams | EcKeyGenParams | AesKeyGenParams | HmacKeyGenParams | KmacKeyGenParams,
|
|
618
|
+
extractable: boolean,
|
|
619
|
+
keyUsages: KeyUsage[]
|
|
620
|
+
): Promise<CryptoKeyPair | CryptoKey> {
|
|
621
|
+
this.#checkCapability();
|
|
622
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
623
|
+
if (typeof alg.name !== 'string') {
|
|
624
|
+
throw new DOMException('Invalid algorithm', 'TypeError');
|
|
625
|
+
}
|
|
626
|
+
const algName = alg.name.toUpperCase();
|
|
627
|
+
if (keyUsages.length === 0) {
|
|
628
|
+
throw new DOMException('Key usages cannot be empty', 'SyntaxError');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
switch (algName) {
|
|
632
|
+
case 'AES-GCM':
|
|
633
|
+
case 'AES-CBC':
|
|
634
|
+
case 'AES-CTR':
|
|
635
|
+
case 'AES-KW': {
|
|
636
|
+
const params = algorithm as AesKeyGenParams;
|
|
637
|
+
const length = params.length;
|
|
638
|
+
|
|
639
|
+
if (![128, 192, 256].includes(length)) {
|
|
640
|
+
throw new DOMException('Invalid key length', 'DataError');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Generate random key bytes
|
|
644
|
+
const keyData = new Uint8Array(length / 8);
|
|
645
|
+
crypto.getRandomValues(keyData);
|
|
646
|
+
|
|
647
|
+
return new ExactCryptoKey(
|
|
648
|
+
'secret',
|
|
649
|
+
extractable,
|
|
650
|
+
{ name: alg.name, length },
|
|
651
|
+
keyUsages,
|
|
652
|
+
keyData
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
case 'HMAC': {
|
|
657
|
+
const params = algorithm as HmacKeyGenParams;
|
|
658
|
+
const hash = normalizeHashName(typeof params.hash === 'string' ? params.hash : params.hash.name);
|
|
659
|
+
const length = params.length ?? getHashLength(hash);
|
|
660
|
+
|
|
661
|
+
const keyData = new Uint8Array(length / 8);
|
|
662
|
+
crypto.getRandomValues(keyData);
|
|
663
|
+
|
|
664
|
+
return new ExactCryptoKey(
|
|
665
|
+
'secret',
|
|
666
|
+
extractable,
|
|
667
|
+
{ name: 'HMAC', hash: { name: hash }, length },
|
|
668
|
+
keyUsages,
|
|
669
|
+
keyData
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
case 'RSA-OAEP':
|
|
674
|
+
case 'RSASSA-PKCS1-V1_5':
|
|
675
|
+
case 'RSA-PSS': {
|
|
676
|
+
const params = algorithm as RsaHashedKeyGenParams;
|
|
677
|
+
const hash = normalizeRequiredHash(params.hash);
|
|
678
|
+
|
|
679
|
+
if (typeof __exactGenerateKeyPairSync === 'function') {
|
|
680
|
+
try {
|
|
681
|
+
const result = __exactGenerateKeyPairSync('rsa', {
|
|
682
|
+
modulusLength: params.modulusLength,
|
|
683
|
+
publicExponent: params.publicExponent instanceof Uint8Array
|
|
684
|
+
? new DataView(params.publicExponent.buffer, params.publicExponent.byteOffset, params.publicExponent.byteLength).getUint32(params.publicExponent.byteLength - 4, false)
|
|
685
|
+
: 65537,
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
const algorithmInfo = {
|
|
689
|
+
name: alg.name,
|
|
690
|
+
modulusLength: params.modulusLength,
|
|
691
|
+
publicExponent: params.publicExponent,
|
|
692
|
+
hash: { name: hash },
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// Keys are PEM strings - store as UTF-8 bytes
|
|
696
|
+
const pubKeyData = new TextEncoder().encode(result.publicKey);
|
|
697
|
+
const privKeyData = new TextEncoder().encode(result.privateKey);
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
publicKey: new ExactCryptoKey(
|
|
701
|
+
'public',
|
|
702
|
+
true,
|
|
703
|
+
algorithmInfo,
|
|
704
|
+
keyUsages.filter(u => ['encrypt', 'verify', 'wrapKey'].includes(u)),
|
|
705
|
+
pubKeyData
|
|
706
|
+
),
|
|
707
|
+
privateKey: new ExactCryptoKey(
|
|
708
|
+
'private',
|
|
709
|
+
extractable,
|
|
710
|
+
algorithmInfo,
|
|
711
|
+
keyUsages.filter(u => ['decrypt', 'sign', 'unwrapKey'].includes(u)),
|
|
712
|
+
privKeyData
|
|
713
|
+
),
|
|
714
|
+
};
|
|
715
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
716
|
+
}
|
|
717
|
+
throw new DOMException('Native crypto not available for RSA key generation', 'NotSupportedError');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
case 'ECDSA':
|
|
721
|
+
case 'ECDH': {
|
|
722
|
+
const params = algorithm as EcKeyGenParams;
|
|
723
|
+
|
|
724
|
+
if (typeof __exactGenerateKeyPairSync === 'function') {
|
|
725
|
+
const result = __exactGenerateKeyPairSync('ec', {
|
|
726
|
+
namedCurve: params.namedCurve,
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
const algorithmInfo = {
|
|
730
|
+
name: alg.name,
|
|
731
|
+
namedCurve: params.namedCurve,
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
// Keys are PEM strings - store as UTF-8 bytes
|
|
735
|
+
const pubKeyData = new TextEncoder().encode(result.publicKey);
|
|
736
|
+
const privKeyData = new TextEncoder().encode(result.privateKey);
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
publicKey: new ExactCryptoKey(
|
|
740
|
+
'public',
|
|
741
|
+
true,
|
|
742
|
+
algorithmInfo,
|
|
743
|
+
alg.name === 'ECDSA'
|
|
744
|
+
? keyUsages.filter(u => u === 'verify')
|
|
745
|
+
: keyUsages.filter(u => u === 'deriveKey' || u === 'deriveBits'),
|
|
746
|
+
pubKeyData
|
|
747
|
+
),
|
|
748
|
+
privateKey: new ExactCryptoKey(
|
|
749
|
+
'private',
|
|
750
|
+
extractable,
|
|
751
|
+
algorithmInfo,
|
|
752
|
+
alg.name === 'ECDSA'
|
|
753
|
+
? keyUsages.filter(u => u === 'sign')
|
|
754
|
+
: keyUsages.filter(u => u === 'deriveKey' || u === 'deriveBits'),
|
|
755
|
+
privKeyData
|
|
756
|
+
),
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
throw new DOMException('Native crypto not available for EC key generation', 'NotSupportedError');
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
case 'ED25519':
|
|
763
|
+
case 'EDDSA': {
|
|
764
|
+
if (typeof __exactGenerateKeyPairSync === 'function') {
|
|
765
|
+
const result = __exactGenerateKeyPairSync('ed25519', {});
|
|
766
|
+
|
|
767
|
+
const algorithmInfo = { name: 'Ed25519' };
|
|
768
|
+
|
|
769
|
+
// Keys are PEM strings - store as UTF-8 bytes
|
|
770
|
+
const pubKeyData = new TextEncoder().encode(result.publicKey);
|
|
771
|
+
const privKeyData = new TextEncoder().encode(result.privateKey);
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
publicKey: new ExactCryptoKey(
|
|
775
|
+
'public',
|
|
776
|
+
true,
|
|
777
|
+
algorithmInfo,
|
|
778
|
+
keyUsages.filter(u => u === 'verify'),
|
|
779
|
+
pubKeyData
|
|
780
|
+
),
|
|
781
|
+
privateKey: new ExactCryptoKey(
|
|
782
|
+
'private',
|
|
783
|
+
extractable,
|
|
784
|
+
algorithmInfo,
|
|
785
|
+
keyUsages.filter(u => u === 'sign'),
|
|
786
|
+
privKeyData
|
|
787
|
+
),
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
throw new DOMException('Native crypto not available for Ed25519 key generation', 'NotSupportedError');
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
case 'X25519': {
|
|
794
|
+
if (typeof __exactGenerateKeyPairSync === 'function') {
|
|
795
|
+
const result = __exactGenerateKeyPairSync('x25519', {});
|
|
796
|
+
|
|
797
|
+
const algorithmInfo = { name: 'X25519' };
|
|
798
|
+
|
|
799
|
+
// Keys are PEM strings - store as UTF-8 bytes
|
|
800
|
+
const pubKeyData = new TextEncoder().encode(result.publicKey);
|
|
801
|
+
const privKeyData = new TextEncoder().encode(result.privateKey);
|
|
802
|
+
|
|
803
|
+
return {
|
|
804
|
+
publicKey: new ExactCryptoKey(
|
|
805
|
+
'public',
|
|
806
|
+
true,
|
|
807
|
+
algorithmInfo,
|
|
808
|
+
keyUsages.filter(u => u === 'deriveBits' || u === 'deriveKey'),
|
|
809
|
+
pubKeyData
|
|
810
|
+
),
|
|
811
|
+
privateKey: new ExactCryptoKey(
|
|
812
|
+
'private',
|
|
813
|
+
extractable,
|
|
814
|
+
algorithmInfo,
|
|
815
|
+
keyUsages.filter(u => u === 'deriveBits' || u === 'deriveKey'),
|
|
816
|
+
privKeyData
|
|
817
|
+
),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
throw new DOMException('Native crypto not available for X25519 key generation', 'NotSupportedError');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
case 'KMAC128':
|
|
824
|
+
case 'KMAC256': {
|
|
825
|
+
const params = algorithm as KmacKeyGenParams;
|
|
826
|
+
const length = params.length;
|
|
827
|
+
if (length !== 128 && length !== 160 && length !== 256) {
|
|
828
|
+
throw new DOMException('Invalid key length', 'DataError');
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
for (const usage of keyUsages) {
|
|
832
|
+
if (usage !== 'sign' && usage !== 'verify') {
|
|
833
|
+
throw new DOMException(
|
|
834
|
+
`Invalid key usage '${usage}' for ${algName}`,
|
|
835
|
+
'SyntaxError'
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const keyData = new Uint8Array(length / 8);
|
|
841
|
+
crypto.getRandomValues(keyData);
|
|
842
|
+
|
|
843
|
+
return new ExactCryptoKey(
|
|
844
|
+
'secret',
|
|
845
|
+
extractable,
|
|
846
|
+
{ name: alg.name, length },
|
|
847
|
+
keyUsages,
|
|
848
|
+
keyData
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
default:
|
|
853
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Import a key from external data
|
|
859
|
+
*/
|
|
860
|
+
async importKey(
|
|
861
|
+
format: KeyFormat,
|
|
862
|
+
keyData: BufferSource | JsonWebKey,
|
|
863
|
+
algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm,
|
|
864
|
+
extractable: boolean,
|
|
865
|
+
keyUsages: KeyUsage[]
|
|
866
|
+
): Promise<CryptoKey> {
|
|
867
|
+
this.#checkCapability();
|
|
868
|
+
// Normalize tentative "raw-secret" format to "raw"
|
|
869
|
+
if (format === ('raw-secret' as any)) format = 'raw';
|
|
870
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
871
|
+
|
|
872
|
+
// Handle raw format (for symmetric keys)
|
|
873
|
+
if (format === 'raw') {
|
|
874
|
+
const rawData = toUint8Array(keyData as BufferSource);
|
|
875
|
+
|
|
876
|
+
switch (alg.name.toUpperCase()) {
|
|
877
|
+
case 'AES-GCM':
|
|
878
|
+
case 'AES-CBC':
|
|
879
|
+
case 'AES-CTR':
|
|
880
|
+
case 'AES-KW': {
|
|
881
|
+
if (![16, 24, 32].includes(rawData.length)) {
|
|
882
|
+
throw new DOMException('Invalid key length', 'DataError');
|
|
883
|
+
}
|
|
884
|
+
return new ExactCryptoKey(
|
|
885
|
+
'secret',
|
|
886
|
+
extractable,
|
|
887
|
+
{ name: alg.name, length: rawData.length * 8 },
|
|
888
|
+
keyUsages,
|
|
889
|
+
rawData
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
case 'HMAC': {
|
|
894
|
+
const params = algorithm as HmacImportParams;
|
|
895
|
+
const hash = normalizeHashName(typeof params.hash === 'string' ? params.hash : params.hash.name);
|
|
896
|
+
return new ExactCryptoKey(
|
|
897
|
+
'secret',
|
|
898
|
+
extractable,
|
|
899
|
+
{ name: 'HMAC', hash: { name: hash }, length: rawData.length * 8 },
|
|
900
|
+
keyUsages,
|
|
901
|
+
rawData
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
case 'PBKDF2':
|
|
906
|
+
case 'HKDF': {
|
|
907
|
+
return new ExactCryptoKey(
|
|
908
|
+
'secret',
|
|
909
|
+
false, // PBKDF2/HKDF keys are never extractable
|
|
910
|
+
{ name: alg.name },
|
|
911
|
+
keyUsages,
|
|
912
|
+
rawData
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
case 'ED25519':
|
|
917
|
+
case 'EDDSA': {
|
|
918
|
+
if (rawData.length !== 32) {
|
|
919
|
+
throw new DOMException('Ed25519 key must be 32 bytes', 'DataError');
|
|
920
|
+
}
|
|
921
|
+
return new ExactCryptoKey(
|
|
922
|
+
'public',
|
|
923
|
+
extractable,
|
|
924
|
+
{ name: 'Ed25519' },
|
|
925
|
+
keyUsages,
|
|
926
|
+
rawData
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
case 'X25519': {
|
|
931
|
+
if (rawData.length !== 32) {
|
|
932
|
+
throw new DOMException('X25519 key must be 32 bytes', 'DataError');
|
|
933
|
+
}
|
|
934
|
+
return new ExactCryptoKey(
|
|
935
|
+
'public',
|
|
936
|
+
extractable,
|
|
937
|
+
{ name: 'X25519' },
|
|
938
|
+
keyUsages,
|
|
939
|
+
rawData
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
default:
|
|
944
|
+
throw new DOMException(`Unsupported algorithm for raw import: ${alg.name}`, 'NotSupportedError');
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Handle JWK format
|
|
949
|
+
if (format === 'jwk') {
|
|
950
|
+
const jwk = keyData as JsonWebKey;
|
|
951
|
+
return this._importJwk(jwk, algorithm, extractable, keyUsages);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Handle SPKI format (for public keys)
|
|
955
|
+
if (format === 'spki') {
|
|
956
|
+
const rawData = toUint8Array(keyData as BufferSource);
|
|
957
|
+
|
|
958
|
+
if (typeof __exactImportKeySpki === 'function') {
|
|
959
|
+
try {
|
|
960
|
+
const result = __exactImportKeySpki(rawData);
|
|
961
|
+
const rawKeyData = result.rawKeyData instanceof Uint8Array
|
|
962
|
+
? result.rawKeyData
|
|
963
|
+
: new Uint8Array(result.rawKeyData as any);
|
|
964
|
+
|
|
965
|
+
// Determine algorithm info based on key type and provided algorithm
|
|
966
|
+
const algName = alg.name.toUpperCase();
|
|
967
|
+
let algorithmInfo: KeyAlgorithm;
|
|
968
|
+
|
|
969
|
+
if (algName === 'ED25519' || algName === 'EDDSA') {
|
|
970
|
+
algorithmInfo = { name: 'Ed25519' };
|
|
971
|
+
} else if (algName === 'X25519') {
|
|
972
|
+
algorithmInfo = { name: 'X25519' };
|
|
973
|
+
} else if (algName.startsWith('RSA')) {
|
|
974
|
+
algorithmInfo = {
|
|
975
|
+
name: alg.name,
|
|
976
|
+
hash: { name: normalizeHashName(typeof (algorithm as RsaHashedImportParams).hash === 'string' ? (algorithm as RsaHashedImportParams).hash : ((algorithm as RsaHashedImportParams).hash as any).name) },
|
|
977
|
+
} as any;
|
|
978
|
+
} else if (algName === 'ECDSA' || algName === 'ECDH') {
|
|
979
|
+
algorithmInfo = {
|
|
980
|
+
name: alg.name,
|
|
981
|
+
namedCurve: (algorithm as EcKeyImportParams).namedCurve,
|
|
982
|
+
};
|
|
983
|
+
} else {
|
|
984
|
+
algorithmInfo = { name: alg.name };
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
return new ExactCryptoKey('public', extractable, algorithmInfo, keyUsages, rawKeyData);
|
|
988
|
+
} catch (e) {
|
|
989
|
+
throw wrapNativeError(e, 'NotSupportedError');
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
throw new DOMException('SPKI import requires native implementation', 'NotSupportedError');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Handle PKCS8 format (for private keys)
|
|
996
|
+
if (format === 'pkcs8') {
|
|
997
|
+
const rawData = toUint8Array(keyData as BufferSource);
|
|
998
|
+
|
|
999
|
+
if (typeof __exactImportKeyPkcs8 === 'function') {
|
|
1000
|
+
try {
|
|
1001
|
+
const result = __exactImportKeyPkcs8(rawData);
|
|
1002
|
+
const rawKeyData = result.rawKeyData instanceof Uint8Array
|
|
1003
|
+
? result.rawKeyData
|
|
1004
|
+
: new Uint8Array(result.rawKeyData as any);
|
|
1005
|
+
|
|
1006
|
+
const algName = alg.name.toUpperCase();
|
|
1007
|
+
let algorithmInfo: KeyAlgorithm;
|
|
1008
|
+
|
|
1009
|
+
if (algName === 'ED25519' || algName === 'EDDSA') {
|
|
1010
|
+
algorithmInfo = { name: 'Ed25519' };
|
|
1011
|
+
} else if (algName === 'X25519') {
|
|
1012
|
+
algorithmInfo = { name: 'X25519' };
|
|
1013
|
+
} else if (algName.startsWith('RSA')) {
|
|
1014
|
+
algorithmInfo = {
|
|
1015
|
+
name: alg.name,
|
|
1016
|
+
hash: { name: normalizeHashName(typeof (algorithm as RsaHashedImportParams).hash === 'string' ? (algorithm as RsaHashedImportParams).hash : ((algorithm as RsaHashedImportParams).hash as any).name) },
|
|
1017
|
+
} as any;
|
|
1018
|
+
} else if (algName === 'ECDSA' || algName === 'ECDH') {
|
|
1019
|
+
algorithmInfo = {
|
|
1020
|
+
name: alg.name,
|
|
1021
|
+
namedCurve: (algorithm as EcKeyImportParams).namedCurve,
|
|
1022
|
+
};
|
|
1023
|
+
} else {
|
|
1024
|
+
algorithmInfo = { name: alg.name };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return new ExactCryptoKey('private', extractable, algorithmInfo, keyUsages, rawKeyData);
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
throw wrapNativeError(e, 'NotSupportedError');
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
throw new DOMException('PKCS8 import requires native implementation', 'NotSupportedError');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
throw new DOMException(`Unsupported format: ${format}`, 'NotSupportedError');
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Import a JWK key
|
|
1040
|
+
*/
|
|
1041
|
+
private async _importJwk(
|
|
1042
|
+
jwk: JsonWebKey,
|
|
1043
|
+
algorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm,
|
|
1044
|
+
extractable: boolean,
|
|
1045
|
+
keyUsages: KeyUsage[]
|
|
1046
|
+
): Promise<CryptoKey> {
|
|
1047
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
1048
|
+
|
|
1049
|
+
switch (jwk.kty) {
|
|
1050
|
+
case 'oct': {
|
|
1051
|
+
// Symmetric key
|
|
1052
|
+
if (!jwk.k) {
|
|
1053
|
+
throw new DOMException('Missing k parameter in JWK', 'DataError');
|
|
1054
|
+
}
|
|
1055
|
+
const rawData = base64UrlDecode(jwk.k);
|
|
1056
|
+
|
|
1057
|
+
if (alg.name.toUpperCase().startsWith('AES')) {
|
|
1058
|
+
return new ExactCryptoKey(
|
|
1059
|
+
'secret',
|
|
1060
|
+
extractable,
|
|
1061
|
+
{ name: alg.name, length: rawData.length * 8 },
|
|
1062
|
+
keyUsages,
|
|
1063
|
+
rawData
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (alg.name.toUpperCase() === 'HMAC') {
|
|
1068
|
+
const params = algorithm as HmacImportParams;
|
|
1069
|
+
const hash = normalizeHashName(typeof params.hash === 'string' ? params.hash : params.hash.name);
|
|
1070
|
+
return new ExactCryptoKey(
|
|
1071
|
+
'secret',
|
|
1072
|
+
extractable,
|
|
1073
|
+
{ name: 'HMAC', hash: { name: hash }, length: rawData.length * 8 },
|
|
1074
|
+
keyUsages,
|
|
1075
|
+
rawData
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
throw new DOMException(`Unsupported algorithm for oct key: ${alg.name}`, 'NotSupportedError');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
case 'RSA': {
|
|
1083
|
+
// Import RSA JWK - extract modulus and exponent
|
|
1084
|
+
if (!jwk.n || !jwk.e) {
|
|
1085
|
+
throw new DOMException('Missing n or e parameter in RSA JWK', 'DataError');
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
const isPrivate = !!jwk.d;
|
|
1089
|
+
const hash = (algorithm as RsaHashedImportParams).hash;
|
|
1090
|
+
const hashName = normalizeHashName(typeof hash === 'string' ? hash : hash.name);
|
|
1091
|
+
|
|
1092
|
+
// Convert JWK RSA parameters to PEM format for the native sign/verify bridges
|
|
1093
|
+
const pemStr = rsaJwkToPem(jwk, isPrivate);
|
|
1094
|
+
const pemBytes = new TextEncoder().encode(pemStr);
|
|
1095
|
+
|
|
1096
|
+
const algorithmInfo = {
|
|
1097
|
+
name: alg.name,
|
|
1098
|
+
hash: { name: hashName },
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
return new ExactCryptoKey(
|
|
1102
|
+
isPrivate ? 'private' : 'public',
|
|
1103
|
+
extractable,
|
|
1104
|
+
algorithmInfo,
|
|
1105
|
+
keyUsages,
|
|
1106
|
+
pemBytes
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
case 'EC': {
|
|
1111
|
+
if (!jwk.x || !jwk.y) {
|
|
1112
|
+
throw new DOMException('Missing x or y parameter in EC JWK', 'DataError');
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const isPrivate = !!jwk.d;
|
|
1116
|
+
const curve = jwk.crv || (algorithm as EcKeyImportParams).namedCurve;
|
|
1117
|
+
|
|
1118
|
+
// Concatenate x, y (and d for private) coordinates as raw key data
|
|
1119
|
+
const x = base64UrlDecode(jwk.x);
|
|
1120
|
+
const y = base64UrlDecode(jwk.y);
|
|
1121
|
+
let keyDataBytes: Uint8Array;
|
|
1122
|
+
|
|
1123
|
+
if (isPrivate && jwk.d) {
|
|
1124
|
+
const d = base64UrlDecode(jwk.d);
|
|
1125
|
+
keyDataBytes = new Uint8Array(1 + x.length + y.length + d.length);
|
|
1126
|
+
keyDataBytes[0] = 0x04; // uncompressed point
|
|
1127
|
+
keyDataBytes.set(x, 1);
|
|
1128
|
+
keyDataBytes.set(y, 1 + x.length);
|
|
1129
|
+
keyDataBytes.set(d, 1 + x.length + y.length);
|
|
1130
|
+
} else {
|
|
1131
|
+
keyDataBytes = new Uint8Array(1 + x.length + y.length);
|
|
1132
|
+
keyDataBytes[0] = 0x04; // uncompressed point
|
|
1133
|
+
keyDataBytes.set(x, 1);
|
|
1134
|
+
keyDataBytes.set(y, 1 + x.length);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const ecAlgorithmInfo = {
|
|
1138
|
+
name: alg.name,
|
|
1139
|
+
namedCurve: curve,
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
return new ExactCryptoKey(
|
|
1143
|
+
isPrivate ? 'private' : 'public',
|
|
1144
|
+
extractable,
|
|
1145
|
+
ecAlgorithmInfo,
|
|
1146
|
+
keyUsages,
|
|
1147
|
+
keyDataBytes
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
case 'OKP': {
|
|
1152
|
+
// Octet Key Pair - Ed25519, X25519
|
|
1153
|
+
if (!jwk.x) {
|
|
1154
|
+
throw new DOMException('Missing x parameter in OKP JWK', 'DataError');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const isPrivate = !!jwk.d;
|
|
1158
|
+
const curve = jwk.crv;
|
|
1159
|
+
|
|
1160
|
+
let keyDataBytes: Uint8Array;
|
|
1161
|
+
if (isPrivate && jwk.d) {
|
|
1162
|
+
// Store both d (private) and x (public) so JWK export can include both.
|
|
1163
|
+
// Format: d[32] || x[32] = 64 bytes total
|
|
1164
|
+
const dBytes = base64UrlDecode(jwk.d);
|
|
1165
|
+
const xBytes = base64UrlDecode(jwk.x);
|
|
1166
|
+
keyDataBytes = new Uint8Array(dBytes.length + xBytes.length);
|
|
1167
|
+
keyDataBytes.set(dBytes, 0);
|
|
1168
|
+
keyDataBytes.set(xBytes, dBytes.length);
|
|
1169
|
+
} else {
|
|
1170
|
+
keyDataBytes = base64UrlDecode(jwk.x);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
let okpAlgName: string;
|
|
1174
|
+
if (curve === 'Ed25519') {
|
|
1175
|
+
okpAlgName = 'Ed25519';
|
|
1176
|
+
} else if (curve === 'X25519') {
|
|
1177
|
+
okpAlgName = 'X25519';
|
|
1178
|
+
} else {
|
|
1179
|
+
throw new DOMException(`Unsupported OKP curve: ${curve}`, 'NotSupportedError');
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return new ExactCryptoKey(
|
|
1183
|
+
isPrivate ? 'private' : 'public',
|
|
1184
|
+
extractable,
|
|
1185
|
+
{ name: okpAlgName },
|
|
1186
|
+
keyUsages,
|
|
1187
|
+
keyDataBytes
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
default:
|
|
1192
|
+
throw new DOMException(`JWK import for ${jwk.kty} keys is not supported`, 'NotSupportedError');
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Export a key to the specified format
|
|
1198
|
+
*/
|
|
1199
|
+
async exportKey(format: KeyFormat, key: CryptoKey): Promise<ArrayBuffer | JsonWebKey> {
|
|
1200
|
+
this.#checkCapability();
|
|
1201
|
+
if (!key.extractable) {
|
|
1202
|
+
throw new DOMException('Key is not extractable', 'InvalidAccessError');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
const exactKey = key as ExactCryptoKey;
|
|
1206
|
+
|
|
1207
|
+
if (format === 'raw') {
|
|
1208
|
+
if (key.type !== 'secret') {
|
|
1209
|
+
throw new DOMException('Raw export only supported for secret keys', 'InvalidAccessError');
|
|
1210
|
+
}
|
|
1211
|
+
return exactKey._keyData.buffer.slice(
|
|
1212
|
+
exactKey._keyData.byteOffset,
|
|
1213
|
+
exactKey._keyData.byteOffset + exactKey._keyData.byteLength
|
|
1214
|
+
);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (format === 'jwk') {
|
|
1218
|
+
return this._exportJwk(exactKey);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (format === 'spki') {
|
|
1222
|
+
if (key.type !== 'public') {
|
|
1223
|
+
throw new DOMException('SPKI export only supported for public keys', 'InvalidAccessError');
|
|
1224
|
+
}
|
|
1225
|
+
if (typeof __exactExportKeySpki === 'function') {
|
|
1226
|
+
const algName = key.algorithm.name.toUpperCase();
|
|
1227
|
+
const result = __exactExportKeySpki(algName, exactKey._keyData);
|
|
1228
|
+
return uint8ArrayToArrayBuffer(result);
|
|
1229
|
+
}
|
|
1230
|
+
throw new DOMException('SPKI export requires native implementation', 'NotSupportedError');
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (format === 'pkcs8') {
|
|
1234
|
+
if (key.type !== 'private') {
|
|
1235
|
+
throw new DOMException('PKCS8 export only supported for private keys', 'InvalidAccessError');
|
|
1236
|
+
}
|
|
1237
|
+
if (typeof __exactExportKeyPkcs8 === 'function') {
|
|
1238
|
+
const algName = key.algorithm.name.toUpperCase();
|
|
1239
|
+
const result = __exactExportKeyPkcs8(algName, exactKey._keyData);
|
|
1240
|
+
return uint8ArrayToArrayBuffer(result);
|
|
1241
|
+
}
|
|
1242
|
+
throw new DOMException('PKCS8 export requires native implementation', 'NotSupportedError');
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
throw new DOMException(`Export format ${format} is not supported`, 'NotSupportedError');
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Export a key as JWK
|
|
1250
|
+
*/
|
|
1251
|
+
private _exportJwk(key: ExactCryptoKey): JsonWebKey {
|
|
1252
|
+
if (key.type === 'secret') {
|
|
1253
|
+
const algName = key.algorithm.name.toUpperCase();
|
|
1254
|
+
|
|
1255
|
+
const jwk: JsonWebKey = {
|
|
1256
|
+
kty: 'oct',
|
|
1257
|
+
k: base64UrlEncode(key._keyData),
|
|
1258
|
+
ext: key.extractable,
|
|
1259
|
+
key_ops: [...key.usages],
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
if (algName.startsWith('AES')) {
|
|
1263
|
+
jwk.alg = `A${(key.algorithm as AesKeyAlgorithm).length}${algName.slice(4)}`;
|
|
1264
|
+
} else if (algName === 'HMAC') {
|
|
1265
|
+
const hash = (key.algorithm as HmacKeyAlgorithm).hash.name;
|
|
1266
|
+
jwk.alg = `HS${hash.replace('SHA-', '')}`;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return jwk;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// Asymmetric key export
|
|
1273
|
+
const algName = key.algorithm.name.toUpperCase();
|
|
1274
|
+
|
|
1275
|
+
if (algName === 'ED25519' || algName === 'EDDSA') {
|
|
1276
|
+
const jwk: JsonWebKey = {
|
|
1277
|
+
kty: 'OKP',
|
|
1278
|
+
crv: 'Ed25519',
|
|
1279
|
+
ext: key.extractable,
|
|
1280
|
+
key_ops: [...key.usages],
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
if (key.type === 'public') {
|
|
1284
|
+
jwk.x = base64UrlEncode(key._keyData);
|
|
1285
|
+
} else {
|
|
1286
|
+
// Private key: key data may be d[32] || x[32] (64 bytes from JWK import)
|
|
1287
|
+
// or a PEM string (from generateKey). Per Web Crypto spec, both x and d
|
|
1288
|
+
// must be present in a private OKP JWK.
|
|
1289
|
+
if (key._keyData.length === 64) {
|
|
1290
|
+
// Raw key data: d[32] || x[32]
|
|
1291
|
+
jwk.d = base64UrlEncode(key._keyData.slice(0, 32));
|
|
1292
|
+
jwk.x = base64UrlEncode(key._keyData.slice(32));
|
|
1293
|
+
} else {
|
|
1294
|
+
// PEM or other format -- export d only (x not available without native bridge)
|
|
1295
|
+
jwk.d = base64UrlEncode(key._keyData);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
return jwk;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (algName === 'X25519') {
|
|
1303
|
+
const jwk: JsonWebKey = {
|
|
1304
|
+
kty: 'OKP',
|
|
1305
|
+
crv: 'X25519',
|
|
1306
|
+
ext: key.extractable,
|
|
1307
|
+
key_ops: [...key.usages],
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
if (key.type === 'public') {
|
|
1311
|
+
jwk.x = base64UrlEncode(key._keyData);
|
|
1312
|
+
} else {
|
|
1313
|
+
// Private key: key data may be d[32] || x[32] (64 bytes from JWK import)
|
|
1314
|
+
// or a PEM string (from generateKey). Per Web Crypto spec, both x and d
|
|
1315
|
+
// must be present in a private OKP JWK.
|
|
1316
|
+
if (key._keyData.length === 64) {
|
|
1317
|
+
// Raw key data: d[32] || x[32]
|
|
1318
|
+
jwk.d = base64UrlEncode(key._keyData.slice(0, 32));
|
|
1319
|
+
jwk.x = base64UrlEncode(key._keyData.slice(32));
|
|
1320
|
+
} else {
|
|
1321
|
+
// PEM or other format -- export d only (x not available without native bridge)
|
|
1322
|
+
jwk.d = base64UrlEncode(key._keyData);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
return jwk;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
if (algName === 'ECDSA' || algName === 'ECDH') {
|
|
1330
|
+
const curve = (key.algorithm as EcKeyAlgorithm).namedCurve;
|
|
1331
|
+
const data = key._keyData;
|
|
1332
|
+
|
|
1333
|
+
// Key data format: 0x04 || x || y [|| d]
|
|
1334
|
+
const coordSize = (data.length - 1) / (key.type === 'private' ? 3 : 2);
|
|
1335
|
+
|
|
1336
|
+
const jwk: JsonWebKey = {
|
|
1337
|
+
kty: 'EC',
|
|
1338
|
+
crv: curve,
|
|
1339
|
+
x: base64UrlEncode(data.slice(1, 1 + coordSize)),
|
|
1340
|
+
y: base64UrlEncode(data.slice(1 + coordSize, 1 + 2 * coordSize)),
|
|
1341
|
+
ext: key.extractable,
|
|
1342
|
+
key_ops: [...key.usages],
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
if (key.type === 'private') {
|
|
1346
|
+
jwk.d = base64UrlEncode(data.slice(1 + 2 * coordSize));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
return jwk;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
if (algName.startsWith('RSA') || algName === 'RSASSA-PKCS1-V1_5') {
|
|
1353
|
+
// RSA keys are stored as PEM strings encoded as UTF-8 bytes
|
|
1354
|
+
const pemStr = uint8ArrayToString(key._keyData);
|
|
1355
|
+
const rsaComponents = parseRsaPemToJwkComponents(pemStr, key.type === 'private');
|
|
1356
|
+
|
|
1357
|
+
const hash = (key.algorithm as RsaHashedKeyAlgorithm).hash?.name || 'SHA-256';
|
|
1358
|
+
let jwkAlg: string;
|
|
1359
|
+
if (algName === 'RSA-OAEP') {
|
|
1360
|
+
jwkAlg = hash === 'SHA-256' ? 'RSA-OAEP-256' : hash === 'SHA-384' ? 'RSA-OAEP-384' : hash === 'SHA-512' ? 'RSA-OAEP-512' : 'RSA-OAEP';
|
|
1361
|
+
} else if (algName === 'RSA-PSS') {
|
|
1362
|
+
jwkAlg = hash === 'SHA-256' ? 'PS256' : hash === 'SHA-384' ? 'PS384' : 'PS512';
|
|
1363
|
+
} else {
|
|
1364
|
+
jwkAlg = hash === 'SHA-256' ? 'RS256' : hash === 'SHA-384' ? 'RS384' : 'RS512';
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const jwk: JsonWebKey = {
|
|
1368
|
+
kty: 'RSA',
|
|
1369
|
+
alg: jwkAlg,
|
|
1370
|
+
ext: key.extractable,
|
|
1371
|
+
key_ops: [...key.usages],
|
|
1372
|
+
...rsaComponents,
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
return jwk;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
throw new DOMException('JWK export for this key type is not supported', 'NotSupportedError');
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Derive bits from a base key
|
|
1383
|
+
*/
|
|
1384
|
+
async deriveBits(
|
|
1385
|
+
algorithm: AlgorithmIdentifier | Pbkdf2Params | HkdfParams | EcdhKeyDeriveParams,
|
|
1386
|
+
baseKey: CryptoKey,
|
|
1387
|
+
length: number
|
|
1388
|
+
): Promise<ArrayBuffer> {
|
|
1389
|
+
this.#checkCapability();
|
|
1390
|
+
validateKey(baseKey, 'deriveBits');
|
|
1391
|
+
const alg = normalizeAlgorithm(algorithm);
|
|
1392
|
+
|
|
1393
|
+
switch (alg.name.toUpperCase()) {
|
|
1394
|
+
case 'PBKDF2': {
|
|
1395
|
+
const params = algorithm as Pbkdf2Params;
|
|
1396
|
+
const salt = toUint8Array(params.salt);
|
|
1397
|
+
const hash = typeof params.hash === 'string' ? params.hash : params.hash.name;
|
|
1398
|
+
const normalizedHash = normalizeHashName(hash);
|
|
1399
|
+
|
|
1400
|
+
if (typeof __exactPbkdf2 === 'function') {
|
|
1401
|
+
// __exactPbkdf2(password, salt, iterations, keyLength, hashAlgo)
|
|
1402
|
+
const result = __exactPbkdf2(
|
|
1403
|
+
(baseKey as ExactCryptoKey)._keyData,
|
|
1404
|
+
salt,
|
|
1405
|
+
params.iterations,
|
|
1406
|
+
length / 8,
|
|
1407
|
+
normalizedHash
|
|
1408
|
+
);
|
|
1409
|
+
return uint8ArrayToArrayBuffer(result);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// JS fallback for PBKDF2-SHA256
|
|
1413
|
+
if (normalizedHash === 'SHA-256') {
|
|
1414
|
+
return jsPbkdf2Sha256(
|
|
1415
|
+
(baseKey as ExactCryptoKey)._keyData,
|
|
1416
|
+
salt,
|
|
1417
|
+
params.iterations,
|
|
1418
|
+
length / 8
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
throw new DOMException('Native crypto not available for PBKDF2', 'NotSupportedError');
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
case 'HKDF': {
|
|
1425
|
+
const params = algorithm as HkdfParams;
|
|
1426
|
+
const salt = toUint8Array(params.salt);
|
|
1427
|
+
const info = toUint8Array(params.info);
|
|
1428
|
+
const hash = typeof params.hash === 'string' ? params.hash : params.hash.name;
|
|
1429
|
+
const normalizedHash = normalizeHashName(hash);
|
|
1430
|
+
|
|
1431
|
+
if (typeof __exactHkdf === 'function') {
|
|
1432
|
+
const result = __exactHkdf(
|
|
1433
|
+
normalizedHash,
|
|
1434
|
+
(baseKey as ExactCryptoKey)._keyData,
|
|
1435
|
+
salt,
|
|
1436
|
+
info,
|
|
1437
|
+
length / 8
|
|
1438
|
+
);
|
|
1439
|
+
return uint8ArrayToArrayBuffer(result);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// JS fallback for HKDF
|
|
1443
|
+
return jsHkdf(
|
|
1444
|
+
(baseKey as ExactCryptoKey)._keyData,
|
|
1445
|
+
salt,
|
|
1446
|
+
info,
|
|
1447
|
+
normalizedHash,
|
|
1448
|
+
length / 8
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
case 'X25519': {
|
|
1453
|
+
const params = algorithm as EcdhKeyDeriveParams;
|
|
1454
|
+
const publicKey = params.public as ExactCryptoKey;
|
|
1455
|
+
|
|
1456
|
+
if (typeof __exactX25519DeriveBits === 'function') {
|
|
1457
|
+
try {
|
|
1458
|
+
// If base key data is 64 bytes (d[32] || x[32] from JWK import), pass only d portion
|
|
1459
|
+
const baseKeyData = (baseKey as ExactCryptoKey)._keyData;
|
|
1460
|
+
const privKeyData = baseKeyData.length === 64 ? baseKeyData.slice(0, 32) : baseKeyData;
|
|
1461
|
+
const result = __exactX25519DeriveBits(
|
|
1462
|
+
privKeyData,
|
|
1463
|
+
publicKey._keyData
|
|
1464
|
+
);
|
|
1465
|
+
return uint8ArrayToArrayBuffer(result);
|
|
1466
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
1467
|
+
}
|
|
1468
|
+
throw new DOMException('Native crypto not available for X25519', 'NotSupportedError');
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
case 'ECDH': {
|
|
1472
|
+
const params = algorithm as EcdhKeyDeriveParams;
|
|
1473
|
+
const publicKey = params.public as ExactCryptoKey;
|
|
1474
|
+
|
|
1475
|
+
if (typeof __exactEcdhDeriveBits === 'function') {
|
|
1476
|
+
try {
|
|
1477
|
+
const curve = (baseKey.algorithm as EcKeyAlgorithm).namedCurve;
|
|
1478
|
+
const result = __exactEcdhDeriveBits(
|
|
1479
|
+
curve,
|
|
1480
|
+
(baseKey as ExactCryptoKey)._keyData,
|
|
1481
|
+
publicKey._keyData
|
|
1482
|
+
);
|
|
1483
|
+
return uint8ArrayToArrayBuffer(result);
|
|
1484
|
+
} catch (e) { throw wrapNativeError(e, 'OperationError'); }
|
|
1485
|
+
}
|
|
1486
|
+
throw new DOMException('Native crypto not available for ECDH', 'NotSupportedError');
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
default:
|
|
1490
|
+
throw new DOMException(`Unsupported algorithm: ${alg.name}`, 'NotSupportedError');
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
/**
|
|
1495
|
+
* Derive a key from a base key
|
|
1496
|
+
*/
|
|
1497
|
+
async deriveKey(
|
|
1498
|
+
algorithm: AlgorithmIdentifier | Pbkdf2Params | HkdfParams | EcdhKeyDeriveParams,
|
|
1499
|
+
baseKey: CryptoKey,
|
|
1500
|
+
derivedKeyAlgorithm: AlgorithmIdentifier | AesKeyGenParams | HmacKeyGenParams,
|
|
1501
|
+
extractable: boolean,
|
|
1502
|
+
keyUsages: KeyUsage[]
|
|
1503
|
+
): Promise<CryptoKey> {
|
|
1504
|
+
this.#checkCapability();
|
|
1505
|
+
validateKey(baseKey, 'deriveKey');
|
|
1506
|
+
const derivedAlg = normalizeAlgorithm(derivedKeyAlgorithm);
|
|
1507
|
+
|
|
1508
|
+
// Determine the length needed
|
|
1509
|
+
let length: number;
|
|
1510
|
+
if (derivedAlg.name.toUpperCase().startsWith('AES')) {
|
|
1511
|
+
length = (derivedKeyAlgorithm as AesKeyGenParams).length;
|
|
1512
|
+
} else if (derivedAlg.name.toUpperCase() === 'HMAC') {
|
|
1513
|
+
const params = derivedKeyAlgorithm as HmacKeyGenParams;
|
|
1514
|
+
const hash = normalizeHashName(typeof params.hash === 'string' ? params.hash : params.hash.name);
|
|
1515
|
+
length = params.length ?? getHashLength(hash);
|
|
1516
|
+
} else {
|
|
1517
|
+
throw new DOMException(`Unsupported derived key algorithm: ${derivedAlg.name}`, 'NotSupportedError');
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Create a proxy key with 'deriveBits' usage for the internal deriveBits call
|
|
1521
|
+
const proxyKey = new ExactCryptoKey(
|
|
1522
|
+
(baseKey as ExactCryptoKey).type,
|
|
1523
|
+
(baseKey as ExactCryptoKey).extractable,
|
|
1524
|
+
(baseKey as ExactCryptoKey).algorithm,
|
|
1525
|
+
[...(baseKey as ExactCryptoKey).usages, 'deriveBits'],
|
|
1526
|
+
(baseKey as ExactCryptoKey)._keyData
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
// Derive the bits
|
|
1530
|
+
const bits = await this.deriveBits(algorithm, proxyKey, length);
|
|
1531
|
+
|
|
1532
|
+
// Import as a key
|
|
1533
|
+
return this.importKey('raw', bits, derivedKeyAlgorithm, extractable, keyUsages);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Wrap a key with another key
|
|
1538
|
+
*/
|
|
1539
|
+
async wrapKey(
|
|
1540
|
+
format: KeyFormat,
|
|
1541
|
+
key: CryptoKey,
|
|
1542
|
+
wrappingKey: CryptoKey,
|
|
1543
|
+
wrapAlgorithm: AlgorithmIdentifier | AesCbcParams | AesCtrParams | AesGcmParams | RsaOaepParams
|
|
1544
|
+
): Promise<ArrayBuffer> {
|
|
1545
|
+
this.#checkCapability();
|
|
1546
|
+
// Export the key
|
|
1547
|
+
const exported = await this.exportKey(format, key);
|
|
1548
|
+
|
|
1549
|
+
// Get bytes from export
|
|
1550
|
+
let bytes: Uint8Array;
|
|
1551
|
+
if (exported instanceof ArrayBuffer) {
|
|
1552
|
+
bytes = new Uint8Array(exported);
|
|
1553
|
+
} else {
|
|
1554
|
+
// JWK - convert to JSON string
|
|
1555
|
+
bytes = new TextEncoder().encode(JSON.stringify(exported));
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Validate wrapping key has 'wrapKey' usage
|
|
1559
|
+
validateKey(wrappingKey, 'wrapKey');
|
|
1560
|
+
|
|
1561
|
+
// Create a proxy key with 'encrypt' usage for the internal encrypt call
|
|
1562
|
+
const proxyKey = new ExactCryptoKey(
|
|
1563
|
+
(wrappingKey as any).type,
|
|
1564
|
+
(wrappingKey as any).extractable,
|
|
1565
|
+
(wrappingKey as any).algorithm,
|
|
1566
|
+
[...(wrappingKey as any).usages, 'encrypt'],
|
|
1567
|
+
(wrappingKey as any)._keyData
|
|
1568
|
+
);
|
|
1569
|
+
|
|
1570
|
+
// Encrypt with wrapping key
|
|
1571
|
+
return this.encrypt(wrapAlgorithm, proxyKey, bytes);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Unwrap a key
|
|
1576
|
+
*/
|
|
1577
|
+
async unwrapKey(
|
|
1578
|
+
format: KeyFormat,
|
|
1579
|
+
wrappedKey: BufferSource,
|
|
1580
|
+
unwrappingKey: CryptoKey,
|
|
1581
|
+
unwrapAlgorithm: AlgorithmIdentifier | AesCbcParams | AesCtrParams | AesGcmParams | RsaOaepParams,
|
|
1582
|
+
unwrappedKeyAlgorithm: AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm,
|
|
1583
|
+
extractable: boolean,
|
|
1584
|
+
keyUsages: KeyUsage[]
|
|
1585
|
+
): Promise<CryptoKey> {
|
|
1586
|
+
this.#checkCapability();
|
|
1587
|
+
// Validate unwrapping key has 'unwrapKey' usage
|
|
1588
|
+
validateKey(unwrappingKey, 'unwrapKey');
|
|
1589
|
+
|
|
1590
|
+
// Create a proxy key with 'decrypt' usage for the internal decrypt call
|
|
1591
|
+
const proxyKey = new ExactCryptoKey(
|
|
1592
|
+
(unwrappingKey as any).type,
|
|
1593
|
+
(unwrappingKey as any).extractable,
|
|
1594
|
+
(unwrappingKey as any).algorithm,
|
|
1595
|
+
[...(unwrappingKey as any).usages, 'decrypt'],
|
|
1596
|
+
(unwrappingKey as any)._keyData
|
|
1597
|
+
);
|
|
1598
|
+
|
|
1599
|
+
// Decrypt the wrapped key
|
|
1600
|
+
const decrypted = await this.decrypt(unwrapAlgorithm, proxyKey, wrappedKey);
|
|
1601
|
+
|
|
1602
|
+
// Import the key
|
|
1603
|
+
if (format === 'jwk') {
|
|
1604
|
+
const jwkString = new TextDecoder().decode(decrypted);
|
|
1605
|
+
const jwk = JSON.parse(jwkString);
|
|
1606
|
+
return this.importKey(format, jwk, unwrappedKeyAlgorithm, extractable, keyUsages);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
return this.importKey(format, decrypted, unwrappedKeyAlgorithm, extractable, keyUsages);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
export class Crypto {
|
|
1614
|
+
private _subtle = new SubtleCrypto();
|
|
1615
|
+
|
|
1616
|
+
get subtle(): SubtleCrypto {
|
|
1617
|
+
return this._subtle;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Fill array with cryptographically secure random bytes
|
|
1622
|
+
*/
|
|
1623
|
+
getRandomValues<T extends ArrayBufferView>(array: T): T {
|
|
1624
|
+
// Check capability before proceeding
|
|
1625
|
+
requireCapability(Capabilities.CRYPTO_RANDOM);
|
|
1626
|
+
|
|
1627
|
+
if (
|
|
1628
|
+
!(
|
|
1629
|
+
array instanceof Int8Array ||
|
|
1630
|
+
array instanceof Uint8Array ||
|
|
1631
|
+
array instanceof Uint8ClampedArray ||
|
|
1632
|
+
array instanceof Int16Array ||
|
|
1633
|
+
array instanceof Uint16Array ||
|
|
1634
|
+
array instanceof Int32Array ||
|
|
1635
|
+
array instanceof Uint32Array ||
|
|
1636
|
+
array instanceof BigInt64Array ||
|
|
1637
|
+
array instanceof BigUint64Array
|
|
1638
|
+
)
|
|
1639
|
+
) {
|
|
1640
|
+
throw new TypeError("Argument must be an integer-typed TypedArray");
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (array.byteLength > 65536) {
|
|
1644
|
+
throw new DOMException(
|
|
1645
|
+
"The ArrayBufferView's byte length exceeds the limit (65536)",
|
|
1646
|
+
"QuotaExceededError"
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const native = getNativeCryptoModule();
|
|
1651
|
+
if (native) {
|
|
1652
|
+
const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
1653
|
+
native.getRandomValues(bytes);
|
|
1654
|
+
return array;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
// Check if we're in test/debug mode via environment or global flag
|
|
1658
|
+
const isTestMode = (
|
|
1659
|
+
typeof process !== 'undefined' &&
|
|
1660
|
+
(process.env?.NODE_ENV === 'test' || process.env?.EXACT_ALLOW_INSECURE_CRYPTO === 'true')
|
|
1661
|
+
) || (typeof (globalThis as any).__EXACT_TEST_MODE__ !== 'undefined');
|
|
1662
|
+
|
|
1663
|
+
if (!isTestMode) {
|
|
1664
|
+
// In production, throw an error instead of using insecure fallback
|
|
1665
|
+
throw new DOMException(
|
|
1666
|
+
'crypto.getRandomValues requires native crypto module. ' +
|
|
1667
|
+
'The native module is not available and insecure fallbacks are disabled in production. ' +
|
|
1668
|
+
'Set EXACT_ALLOW_INSECURE_CRYPTO=true or __EXACT_TEST_MODE__=true for testing.',
|
|
1669
|
+
'NotSupportedError'
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// JS fallback for testing only (NOT cryptographically secure!)
|
|
1674
|
+
if (!isSilentMode()) {
|
|
1675
|
+
console.warn(
|
|
1676
|
+
"crypto.getRandomValues: Using INSECURE Math.random() fallback! " +
|
|
1677
|
+
"This is only acceptable in test environments."
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
1681
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1682
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
1683
|
+
}
|
|
1684
|
+
return array;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Generate a random UUID v4
|
|
1689
|
+
*/
|
|
1690
|
+
randomUUID(): `${string}-${string}-${string}-${string}-${string}` {
|
|
1691
|
+
// Check capability before proceeding
|
|
1692
|
+
requireCapability(Capabilities.CRYPTO_RANDOM);
|
|
1693
|
+
|
|
1694
|
+
const native = getNativeCryptoModule();
|
|
1695
|
+
if (native) {
|
|
1696
|
+
return native.randomUUID() as `${string}-${string}-${string}-${string}-${string}`;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
// JS fallback
|
|
1700
|
+
const bytes = new Uint8Array(16);
|
|
1701
|
+
this.getRandomValues(bytes);
|
|
1702
|
+
|
|
1703
|
+
// Set version (4) and variant (10xx)
|
|
1704
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
1705
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
1706
|
+
|
|
1707
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1708
|
+
|
|
1709
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}` as `${string}-${string}-${string}-${string}-${string}`;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// Helper to convert BufferSource to Uint8Array
|
|
1714
|
+
function toUint8Array(data: BufferSource): Uint8Array {
|
|
1715
|
+
if (data instanceof ArrayBuffer) {
|
|
1716
|
+
return new Uint8Array(data);
|
|
1717
|
+
}
|
|
1718
|
+
if (ArrayBuffer.isView(data)) {
|
|
1719
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
1720
|
+
}
|
|
1721
|
+
throw new TypeError("Data must be an ArrayBuffer or ArrayBufferView");
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// Simple SHA-256 implementation for testing (not optimized)
|
|
1725
|
+
async function jsSHA256(data: Uint8Array): Promise<ArrayBuffer> {
|
|
1726
|
+
// Constants
|
|
1727
|
+
const K = [
|
|
1728
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
1729
|
+
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
1730
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
1731
|
+
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
1732
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
1733
|
+
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
1734
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
1735
|
+
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
1736
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
1737
|
+
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
1738
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
1739
|
+
];
|
|
1740
|
+
|
|
1741
|
+
// Initial hash values
|
|
1742
|
+
let h0 = 0x6a09e667;
|
|
1743
|
+
let h1 = 0xbb67ae85;
|
|
1744
|
+
let h2 = 0x3c6ef372;
|
|
1745
|
+
let h3 = 0xa54ff53a;
|
|
1746
|
+
let h4 = 0x510e527f;
|
|
1747
|
+
let h5 = 0x9b05688c;
|
|
1748
|
+
let h6 = 0x1f83d9ab;
|
|
1749
|
+
let h7 = 0x5be0cd19;
|
|
1750
|
+
|
|
1751
|
+
// Pre-processing: add padding
|
|
1752
|
+
const msgLen = data.length;
|
|
1753
|
+
const bitLen = msgLen * 8;
|
|
1754
|
+
|
|
1755
|
+
// Calculate padded length: message + 1 byte (0x80) + padding + 8 bytes (length)
|
|
1756
|
+
// Total must be multiple of 64
|
|
1757
|
+
let paddedLen = msgLen + 1 + 8; // minimum: msg + 0x80 + 64-bit length
|
|
1758
|
+
const remainder = paddedLen % 64;
|
|
1759
|
+
if (remainder > 0) {
|
|
1760
|
+
paddedLen += 64 - remainder;
|
|
1761
|
+
}
|
|
1762
|
+
if (paddedLen - msgLen - 1 < 8) {
|
|
1763
|
+
paddedLen += 64; // Need more space for length
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
const padded = new Uint8Array(paddedLen);
|
|
1767
|
+
padded.set(data);
|
|
1768
|
+
padded[msgLen] = 0x80;
|
|
1769
|
+
|
|
1770
|
+
// Append length as 64-bit big-endian (we only use 32 bits for length)
|
|
1771
|
+
const view = new DataView(padded.buffer);
|
|
1772
|
+
view.setUint32(paddedLen - 4, bitLen, false);
|
|
1773
|
+
|
|
1774
|
+
// Process each 512-bit (64-byte) chunk
|
|
1775
|
+
for (let offset = 0; offset < paddedLen; offset += 64) {
|
|
1776
|
+
const W = new Uint32Array(64);
|
|
1777
|
+
|
|
1778
|
+
// Copy chunk into first 16 words
|
|
1779
|
+
for (let i = 0; i < 16; i++) {
|
|
1780
|
+
W[i] = view.getUint32(offset + i * 4, false);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// Extend to 64 words
|
|
1784
|
+
for (let i = 16; i < 64; i++) {
|
|
1785
|
+
const s0 = rotr(W[i - 15], 7) ^ rotr(W[i - 15], 18) ^ (W[i - 15] >>> 3);
|
|
1786
|
+
const s1 = rotr(W[i - 2], 17) ^ rotr(W[i - 2], 19) ^ (W[i - 2] >>> 10);
|
|
1787
|
+
W[i] = (W[i - 16] + s0 + W[i - 7] + s1) >>> 0;
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
// Initialize working variables
|
|
1791
|
+
let a = h0,
|
|
1792
|
+
b = h1,
|
|
1793
|
+
c = h2,
|
|
1794
|
+
d = h3,
|
|
1795
|
+
e = h4,
|
|
1796
|
+
f = h5,
|
|
1797
|
+
g = h6,
|
|
1798
|
+
h = h7;
|
|
1799
|
+
|
|
1800
|
+
// Main loop
|
|
1801
|
+
for (let i = 0; i < 64; i++) {
|
|
1802
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
1803
|
+
const ch = (e & f) ^ (~e & g);
|
|
1804
|
+
const temp1 = (h + S1 + ch + K[i] + W[i]) >>> 0;
|
|
1805
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
1806
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
1807
|
+
const temp2 = (S0 + maj) >>> 0;
|
|
1808
|
+
|
|
1809
|
+
h = g;
|
|
1810
|
+
g = f;
|
|
1811
|
+
f = e;
|
|
1812
|
+
e = (d + temp1) >>> 0;
|
|
1813
|
+
d = c;
|
|
1814
|
+
c = b;
|
|
1815
|
+
b = a;
|
|
1816
|
+
a = (temp1 + temp2) >>> 0;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// Add to hash
|
|
1820
|
+
h0 = (h0 + a) >>> 0;
|
|
1821
|
+
h1 = (h1 + b) >>> 0;
|
|
1822
|
+
h2 = (h2 + c) >>> 0;
|
|
1823
|
+
h3 = (h3 + d) >>> 0;
|
|
1824
|
+
h4 = (h4 + e) >>> 0;
|
|
1825
|
+
h5 = (h5 + f) >>> 0;
|
|
1826
|
+
h6 = (h6 + g) >>> 0;
|
|
1827
|
+
h7 = (h7 + h) >>> 0;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Produce final hash
|
|
1831
|
+
const result = new ArrayBuffer(32);
|
|
1832
|
+
const resultView = new DataView(result);
|
|
1833
|
+
resultView.setUint32(0, h0, false);
|
|
1834
|
+
resultView.setUint32(4, h1, false);
|
|
1835
|
+
resultView.setUint32(8, h2, false);
|
|
1836
|
+
resultView.setUint32(12, h3, false);
|
|
1837
|
+
resultView.setUint32(16, h4, false);
|
|
1838
|
+
resultView.setUint32(20, h5, false);
|
|
1839
|
+
resultView.setUint32(24, h6, false);
|
|
1840
|
+
resultView.setUint32(28, h7, false);
|
|
1841
|
+
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function rotr(x: number, n: number): number {
|
|
1846
|
+
return ((x >>> n) | (x << (32 - n))) >>> 0;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
// DOMException for this module
|
|
1850
|
+
class DOMException extends Error {
|
|
1851
|
+
readonly code: number = 0;
|
|
1852
|
+
constructor(message: string, name: string) {
|
|
1853
|
+
super(message);
|
|
1854
|
+
this.name = name;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
// =============================================================================
|
|
1859
|
+
// Type Definitions
|
|
1860
|
+
// =============================================================================
|
|
1861
|
+
|
|
1862
|
+
type AlgorithmIdentifier = string | { name: string };
|
|
1863
|
+
type KeyFormat = "raw" | "spki" | "pkcs8" | "jwk";
|
|
1864
|
+
type KeyUsage = "encrypt" | "decrypt" | "sign" | "verify" | "deriveKey" | "deriveBits" | "wrapKey" | "unwrapKey";
|
|
1865
|
+
|
|
1866
|
+
interface CryptoKey {
|
|
1867
|
+
readonly type: string;
|
|
1868
|
+
readonly extractable: boolean;
|
|
1869
|
+
readonly algorithm: KeyAlgorithm;
|
|
1870
|
+
readonly usages: KeyUsage[];
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
interface KeyAlgorithm {
|
|
1874
|
+
name: string;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
interface AesKeyAlgorithm extends KeyAlgorithm {
|
|
1878
|
+
length: number;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
interface HmacKeyAlgorithm extends KeyAlgorithm {
|
|
1882
|
+
hash: { name: string };
|
|
1883
|
+
length: number;
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
interface RsaHashedKeyAlgorithm extends KeyAlgorithm {
|
|
1887
|
+
modulusLength: number;
|
|
1888
|
+
publicExponent: Uint8Array;
|
|
1889
|
+
hash: { name: string };
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
interface EcKeyAlgorithm extends KeyAlgorithm {
|
|
1893
|
+
namedCurve: string;
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
interface CryptoKeyPair {
|
|
1897
|
+
readonly publicKey: CryptoKey;
|
|
1898
|
+
readonly privateKey: CryptoKey;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
interface JsonWebKey {
|
|
1902
|
+
kty?: string;
|
|
1903
|
+
k?: string;
|
|
1904
|
+
alg?: string;
|
|
1905
|
+
ext?: boolean;
|
|
1906
|
+
key_ops?: string[];
|
|
1907
|
+
[key: string]: any;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// Algorithm parameter interfaces
|
|
1911
|
+
interface AesCbcParams { name: string; iv: BufferSource; }
|
|
1912
|
+
interface AesCtrParams { name: string; counter: BufferSource; length: number; }
|
|
1913
|
+
interface AesGcmParams { name: string; iv: BufferSource; additionalData?: BufferSource; tagLength?: number; }
|
|
1914
|
+
interface RsaOaepParams { name: string; label?: BufferSource; }
|
|
1915
|
+
interface RsaPssParams { name: string; saltLength: number; }
|
|
1916
|
+
interface EcdsaParams { name: string; hash: string | { name: string }; }
|
|
1917
|
+
interface Pbkdf2Params { name: string; salt: BufferSource; iterations: number; hash: string | { name: string }; }
|
|
1918
|
+
interface HkdfParams { name: string; salt: BufferSource; info: BufferSource; hash: string | { name: string }; }
|
|
1919
|
+
interface EcdhKeyDeriveParams { name: string; public: CryptoKey; }
|
|
1920
|
+
|
|
1921
|
+
// Key generation parameter interfaces
|
|
1922
|
+
interface AesKeyGenParams { name: string; length: number; }
|
|
1923
|
+
interface HmacKeyGenParams { name: string; hash: string | { name: string }; length?: number; }
|
|
1924
|
+
interface RsaHashedKeyGenParams { name: string; modulusLength: number; publicExponent: Uint8Array; hash: string | { name: string }; }
|
|
1925
|
+
interface EcKeyGenParams { name: string; namedCurve: string; }
|
|
1926
|
+
interface KmacKeyGenParams { name: string; length: number; }
|
|
1927
|
+
|
|
1928
|
+
// Import parameter interfaces
|
|
1929
|
+
interface HmacImportParams { name: string; hash: string | { name: string }; length?: number; }
|
|
1930
|
+
interface RsaHashedImportParams { name: string; hash: string | { name: string }; }
|
|
1931
|
+
interface EcKeyImportParams { name: string; namedCurve: string; }
|
|
1932
|
+
|
|
1933
|
+
// =============================================================================
|
|
1934
|
+
// ExactCryptoKey - Internal key implementation
|
|
1935
|
+
// =============================================================================
|
|
1936
|
+
|
|
1937
|
+
export class ExactCryptoKey implements CryptoKey {
|
|
1938
|
+
readonly type: 'public' | 'private' | 'secret';
|
|
1939
|
+
readonly extractable: boolean;
|
|
1940
|
+
readonly algorithm: KeyAlgorithm;
|
|
1941
|
+
readonly usages: KeyUsage[];
|
|
1942
|
+
readonly _keyData: Uint8Array;
|
|
1943
|
+
|
|
1944
|
+
constructor(
|
|
1945
|
+
type: 'public' | 'private' | 'secret',
|
|
1946
|
+
extractable: boolean,
|
|
1947
|
+
algorithm: KeyAlgorithm,
|
|
1948
|
+
usages: KeyUsage[],
|
|
1949
|
+
keyData: Uint8Array
|
|
1950
|
+
) {
|
|
1951
|
+
this.type = type;
|
|
1952
|
+
this.extractable = extractable;
|
|
1953
|
+
this.algorithm = algorithm;
|
|
1954
|
+
this.usages = usages;
|
|
1955
|
+
this._keyData = keyData;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
get [Symbol.toStringTag](): 'CryptoKey' {
|
|
1959
|
+
return 'CryptoKey';
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
export { ExactCryptoKey as CryptoKey };
|
|
1964
|
+
|
|
1965
|
+
// =============================================================================
|
|
1966
|
+
// Helper Functions
|
|
1967
|
+
// =============================================================================
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Normalize algorithm identifier to object form
|
|
1971
|
+
*/
|
|
1972
|
+
function normalizeAlgorithm(algorithm: AlgorithmIdentifier | any): { name: string; [key: string]: any } {
|
|
1973
|
+
if (typeof algorithm === 'string') {
|
|
1974
|
+
return { name: algorithm };
|
|
1975
|
+
}
|
|
1976
|
+
return algorithm;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Validate that a key can be used for the specified operation
|
|
1981
|
+
*/
|
|
1982
|
+
function validateKey(key: CryptoKey, operation: KeyUsage): void {
|
|
1983
|
+
if (!key.usages.includes(operation)) {
|
|
1984
|
+
throw new DOMException(
|
|
1985
|
+
`Key does not support the '${operation}' operation`,
|
|
1986
|
+
'InvalidAccessError'
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* Normalize hash names like `sha256`, `SHA-256`, and `sha-256`.
|
|
1993
|
+
*/
|
|
1994
|
+
function normalizeHashName(hash: string): string {
|
|
1995
|
+
const normalized = hash.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
1996
|
+
switch (normalized) {
|
|
1997
|
+
case "SHA1":
|
|
1998
|
+
return "SHA-1";
|
|
1999
|
+
case "SHA224":
|
|
2000
|
+
return "SHA-224";
|
|
2001
|
+
case "SHA256":
|
|
2002
|
+
return "SHA-256";
|
|
2003
|
+
case "SHA384":
|
|
2004
|
+
return "SHA-384";
|
|
2005
|
+
case "SHA512":
|
|
2006
|
+
return "SHA-512";
|
|
2007
|
+
default:
|
|
2008
|
+
throw new DOMException(`Unrecognized hash algorithm: ${hash}`, "NotSupportedError");
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
/**
|
|
2013
|
+
* Return a platform subtle implementation for digest and HMAC fallbacks.
|
|
2014
|
+
* This avoids recursive calls into the same Exact crypto implementation.
|
|
2015
|
+
*/
|
|
2016
|
+
function getPlatformSubtle(): any | null {
|
|
2017
|
+
const g = globalThis as any;
|
|
2018
|
+
const candidate = g.crypto?.subtle;
|
|
2019
|
+
if (candidate && candidate !== crypto.subtle) {
|
|
2020
|
+
return candidate;
|
|
2021
|
+
}
|
|
2022
|
+
if (g.webcrypto?.subtle) {
|
|
2023
|
+
return g.webcrypto.subtle;
|
|
2024
|
+
}
|
|
2025
|
+
return null;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
/**
|
|
2029
|
+
* Get hash output length in bits
|
|
2030
|
+
*/
|
|
2031
|
+
function getHashLength(hash: string): number {
|
|
2032
|
+
const normalized = normalizeHashName(hash);
|
|
2033
|
+
switch (normalized) {
|
|
2034
|
+
case "SHA-1":
|
|
2035
|
+
return 160;
|
|
2036
|
+
case "SHA-256":
|
|
2037
|
+
return 256;
|
|
2038
|
+
case "SHA-384":
|
|
2039
|
+
return 384;
|
|
2040
|
+
case "SHA-512":
|
|
2041
|
+
return 512;
|
|
2042
|
+
default:
|
|
2043
|
+
throw new DOMException(`Unsupported hash algorithm: ${hash}`, "NotSupportedError");
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
function normalizeRequiredHash(hash: string | { name: string } | undefined): string {
|
|
2048
|
+
if (typeof hash === 'string') {
|
|
2049
|
+
return normalizeHashName(hash);
|
|
2050
|
+
}
|
|
2051
|
+
if (hash && typeof hash === 'object' && typeof hash.name === 'string') {
|
|
2052
|
+
return normalizeHashName(hash.name);
|
|
2053
|
+
}
|
|
2054
|
+
throw new DOMException('Invalid hash algorithm', 'NotSupportedError');
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
/**
|
|
2058
|
+
* Return hash params in normalized format.
|
|
2059
|
+
*/
|
|
2060
|
+
function getHashParams(hash: string): { name: string; length: number } {
|
|
2061
|
+
const normalized = normalizeHashName(hash);
|
|
2062
|
+
return { name: normalized, length: getHashLength(normalized) / 8 };
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* Base64URL encode
|
|
2067
|
+
*/
|
|
2068
|
+
function base64UrlEncode(data: Uint8Array): string {
|
|
2069
|
+
const base64 = btoa(String.fromCharCode(...data));
|
|
2070
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
/**
|
|
2074
|
+
* Base64URL decode
|
|
2075
|
+
*/
|
|
2076
|
+
function base64UrlDecode(str: string): Uint8Array {
|
|
2077
|
+
// Add padding
|
|
2078
|
+
let padded = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
2079
|
+
while (padded.length % 4) {
|
|
2080
|
+
padded += '=';
|
|
2081
|
+
}
|
|
2082
|
+
const binary = atob(padded);
|
|
2083
|
+
const bytes = new Uint8Array(binary.length);
|
|
2084
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2085
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2086
|
+
}
|
|
2087
|
+
return bytes;
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* Constant-time comparison to prevent timing attacks
|
|
2092
|
+
*/
|
|
2093
|
+
function constantTimeCompare(a: Uint8Array, b: Uint8Array): boolean {
|
|
2094
|
+
if (a.length !== b.length) return false;
|
|
2095
|
+
let result = 0;
|
|
2096
|
+
for (let i = 0; i < a.length; i++) {
|
|
2097
|
+
result |= a[i] ^ b[i];
|
|
2098
|
+
}
|
|
2099
|
+
return result === 0;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Compute hash digest through platform subtle API when available.
|
|
2104
|
+
*/
|
|
2105
|
+
async function digestWithPlatformHash(hash: string, data: Uint8Array): Promise<ArrayBuffer> {
|
|
2106
|
+
const subtle = getPlatformSubtle();
|
|
2107
|
+
if (!subtle?.digest) {
|
|
2108
|
+
throw new DOMException(
|
|
2109
|
+
"Platform crypto.subtle.digest is not available for this fallback path",
|
|
2110
|
+
"NotSupportedError"
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
const normalizedHash = normalizeHashName(hash);
|
|
2114
|
+
return subtle.digest({ name: normalizedHash }, data);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
/**
|
|
2118
|
+
* Pure JS SHA-1 implementation
|
|
2119
|
+
*/
|
|
2120
|
+
async function jsSHA1(data: Uint8Array): Promise<ArrayBuffer> {
|
|
2121
|
+
let h0 = 0x67452301;
|
|
2122
|
+
let h1 = 0xEFCDAB89;
|
|
2123
|
+
let h2 = 0x98BADCFE;
|
|
2124
|
+
let h3 = 0x10325476;
|
|
2125
|
+
let h4 = 0xC3D2E1F0;
|
|
2126
|
+
|
|
2127
|
+
const msgLen = data.length;
|
|
2128
|
+
const bitLen = msgLen * 8;
|
|
2129
|
+
let paddedLen = msgLen + 1 + 8;
|
|
2130
|
+
const remainder = paddedLen % 64;
|
|
2131
|
+
if (remainder > 0) paddedLen += 64 - remainder;
|
|
2132
|
+
if (paddedLen - msgLen - 1 < 8) paddedLen += 64;
|
|
2133
|
+
|
|
2134
|
+
const padded = new Uint8Array(paddedLen);
|
|
2135
|
+
padded.set(data);
|
|
2136
|
+
padded[msgLen] = 0x80;
|
|
2137
|
+
const view = new DataView(padded.buffer);
|
|
2138
|
+
view.setUint32(paddedLen - 4, bitLen, false);
|
|
2139
|
+
|
|
2140
|
+
function rotl(x: number, n: number): number {
|
|
2141
|
+
return ((x << n) | (x >>> (32 - n))) >>> 0;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
for (let offset = 0; offset < paddedLen; offset += 64) {
|
|
2145
|
+
const W = new Uint32Array(80);
|
|
2146
|
+
for (let i = 0; i < 16; i++) {
|
|
2147
|
+
W[i] = view.getUint32(offset + i * 4, false);
|
|
2148
|
+
}
|
|
2149
|
+
for (let i = 16; i < 80; i++) {
|
|
2150
|
+
W[i] = rotl(W[i-3] ^ W[i-8] ^ W[i-14] ^ W[i-16], 1);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
let a = h0, b = h1, c = h2, d = h3, e = h4;
|
|
2154
|
+
|
|
2155
|
+
for (let i = 0; i < 80; i++) {
|
|
2156
|
+
let f: number, k: number;
|
|
2157
|
+
if (i < 20) {
|
|
2158
|
+
f = (b & c) | (~b & d);
|
|
2159
|
+
k = 0x5A827999;
|
|
2160
|
+
} else if (i < 40) {
|
|
2161
|
+
f = b ^ c ^ d;
|
|
2162
|
+
k = 0x6ED9EBA1;
|
|
2163
|
+
} else if (i < 60) {
|
|
2164
|
+
f = (b & c) | (b & d) | (c & d);
|
|
2165
|
+
k = 0x8F1BBCDC;
|
|
2166
|
+
} else {
|
|
2167
|
+
f = b ^ c ^ d;
|
|
2168
|
+
k = 0xCA62C1D6;
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const temp = (rotl(a, 5) + f + e + k + W[i]) >>> 0;
|
|
2172
|
+
e = d;
|
|
2173
|
+
d = c;
|
|
2174
|
+
c = rotl(b, 30);
|
|
2175
|
+
b = a;
|
|
2176
|
+
a = temp;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
h0 = (h0 + a) >>> 0;
|
|
2180
|
+
h1 = (h1 + b) >>> 0;
|
|
2181
|
+
h2 = (h2 + c) >>> 0;
|
|
2182
|
+
h3 = (h3 + d) >>> 0;
|
|
2183
|
+
h4 = (h4 + e) >>> 0;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
const result = new ArrayBuffer(20);
|
|
2187
|
+
const rv = new DataView(result);
|
|
2188
|
+
rv.setUint32(0, h0, false);
|
|
2189
|
+
rv.setUint32(4, h1, false);
|
|
2190
|
+
rv.setUint32(8, h2, false);
|
|
2191
|
+
rv.setUint32(12, h3, false);
|
|
2192
|
+
rv.setUint32(16, h4, false);
|
|
2193
|
+
return result;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/**
|
|
2197
|
+
* Pure JS SHA-512/384 implementation using BigInt for 64-bit operations
|
|
2198
|
+
*/
|
|
2199
|
+
async function jsSHA512Core(data: Uint8Array, is384: boolean): Promise<ArrayBuffer> {
|
|
2200
|
+
// SHA-512 round constants (first 80 primes)
|
|
2201
|
+
const K: bigint[] = [
|
|
2202
|
+
0x428a2f98d728ae22n, 0x7137449123ef65cdn, 0xb5c0fbcfec4d3b2fn, 0xe9b5dba58189dbbcn,
|
|
2203
|
+
0x3956c25bf348b538n, 0x59f111f1b605d019n, 0x923f82a4af194f9bn, 0xab1c5ed5da6d8118n,
|
|
2204
|
+
0xd807aa98a3030242n, 0x12835b0145706fben, 0x243185be4ee4b28cn, 0x550c7dc3d5ffb4e2n,
|
|
2205
|
+
0x72be5d74f27b896fn, 0x80deb1fe3b1696b1n, 0x9bdc06a725c71235n, 0xc19bf174cf692694n,
|
|
2206
|
+
0xe49b69c19ef14ad2n, 0xefbe4786384f25e3n, 0x0fc19dc68b8cd5b5n, 0x240ca1cc77ac9c65n,
|
|
2207
|
+
0x2de92c6f592b0275n, 0x4a7484aa6ea6e483n, 0x5cb0a9dcbd41fbd4n, 0x76f988da831153b5n,
|
|
2208
|
+
0x983e5152ee66dfabn, 0xa831c66d2db43210n, 0xb00327c898fb213fn, 0xbf597fc7beef0ee4n,
|
|
2209
|
+
0xc6e00bf33da88fc2n, 0xd5a79147930aa725n, 0x06ca6351e003826fn, 0x142929670a0e6e70n,
|
|
2210
|
+
0x27b70a8546d22ffcn, 0x2e1b21385c26c926n, 0x4d2c6dfc5ac42aedn, 0x53380d139d95b3dfn,
|
|
2211
|
+
0x650a73548baf63den, 0x766a0abb3c77b2a8n, 0x81c2c92e47edaee6n, 0x92722c851482353bn,
|
|
2212
|
+
0xa2bfe8a14cf10364n, 0xa81a664bbc423001n, 0xc24b8b70d0f89791n, 0xc76c51a30654be30n,
|
|
2213
|
+
0xd192e819d6ef5218n, 0xd69906245565a910n, 0xf40e35855771202an, 0x106aa07032bbd1b8n,
|
|
2214
|
+
0x19a4c116b8d2d0c8n, 0x1e376c085141ab53n, 0x2748774cdf8eeb99n, 0x34b0bcb5e19b48a8n,
|
|
2215
|
+
0x391c0cb3c5c95a63n, 0x4ed8aa4ae3418acbn, 0x5b9cca4f7763e373n, 0x682e6ff3d6b2b8a3n,
|
|
2216
|
+
0x748f82ee5defb2fcn, 0x78a5636f43172f60n, 0x84c87814a1f0ab72n, 0x8cc702081a6439ecn,
|
|
2217
|
+
0x90befffa23631e28n, 0xa4506cebde82bde9n, 0xbef9a3f7b2c67915n, 0xc67178f2e372532bn,
|
|
2218
|
+
0xca273eceea26619cn, 0xd186b8c721c0c207n, 0xeada7dd6cde0eb1en, 0xf57d4f7fee6ed178n,
|
|
2219
|
+
0x06f067aa72176fban, 0x0a637dc5a2c898a6n, 0x113f9804bef90daen, 0x1b710b35131c471bn,
|
|
2220
|
+
0x28db77f523047d84n, 0x32caab7b40c72493n, 0x3c9ebe0a15c9bebcn, 0x431d67c49c100d4cn,
|
|
2221
|
+
0x4cc5d4becb3e42b6n, 0x597f299cfc657e2an, 0x5fcb6fab3ad6faecn, 0x6c44198c4a475817n,
|
|
2222
|
+
];
|
|
2223
|
+
|
|
2224
|
+
const MASK64 = 0xffffffffffffffffn;
|
|
2225
|
+
|
|
2226
|
+
function rotr64(x: bigint, n: number): bigint {
|
|
2227
|
+
return ((x >> BigInt(n)) | (x << BigInt(64 - n))) & MASK64;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// Initial hash values
|
|
2231
|
+
let H: bigint[];
|
|
2232
|
+
if (is384) {
|
|
2233
|
+
H = [
|
|
2234
|
+
0xcbbb9d5dc1059ed8n, 0x629a292a367cd507n, 0x9159015a3070dd17n, 0x152fecd8f70e5939n,
|
|
2235
|
+
0x67332667ffc00b31n, 0x8eb44a8768581511n, 0xdb0c2e0d64f98fa7n, 0x47b5481dbefa4fa4n,
|
|
2236
|
+
];
|
|
2237
|
+
} else {
|
|
2238
|
+
H = [
|
|
2239
|
+
0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n,
|
|
2240
|
+
0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n,
|
|
2241
|
+
];
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// Padding
|
|
2245
|
+
const msgLen = data.length;
|
|
2246
|
+
const bitLen = BigInt(msgLen) * 8n;
|
|
2247
|
+
let paddedLen = msgLen + 1 + 16; // + 0x80 + 128-bit length
|
|
2248
|
+
const remainder = paddedLen % 128;
|
|
2249
|
+
if (remainder > 0) paddedLen += 128 - remainder;
|
|
2250
|
+
if (paddedLen - msgLen - 1 < 16) paddedLen += 128;
|
|
2251
|
+
|
|
2252
|
+
const padded = new Uint8Array(paddedLen);
|
|
2253
|
+
padded.set(data);
|
|
2254
|
+
padded[msgLen] = 0x80;
|
|
2255
|
+
// Append 128-bit length (big-endian) - we only use the low 64 bits
|
|
2256
|
+
const lenView = new DataView(padded.buffer);
|
|
2257
|
+
const lenHigh = Number((bitLen >> 32n) & 0xffffffffn);
|
|
2258
|
+
const lenLow = Number(bitLen & 0xffffffffn);
|
|
2259
|
+
lenView.setUint32(paddedLen - 8, lenHigh, false);
|
|
2260
|
+
lenView.setUint32(paddedLen - 4, lenLow, false);
|
|
2261
|
+
|
|
2262
|
+
// Process each 1024-bit (128-byte) block
|
|
2263
|
+
const dv = new DataView(padded.buffer);
|
|
2264
|
+
for (let offset = 0; offset < paddedLen; offset += 128) {
|
|
2265
|
+
const W: bigint[] = new Array(80);
|
|
2266
|
+
for (let i = 0; i < 16; i++) {
|
|
2267
|
+
const hi = BigInt(dv.getUint32(offset + i * 8, false));
|
|
2268
|
+
const lo = BigInt(dv.getUint32(offset + i * 8 + 4, false));
|
|
2269
|
+
W[i] = ((hi << 32n) | lo) & MASK64;
|
|
2270
|
+
}
|
|
2271
|
+
for (let i = 16; i < 80; i++) {
|
|
2272
|
+
const s0 = rotr64(W[i-15], 1) ^ rotr64(W[i-15], 8) ^ (W[i-15] >> 7n);
|
|
2273
|
+
const s1 = rotr64(W[i-2], 19) ^ rotr64(W[i-2], 61) ^ (W[i-2] >> 6n);
|
|
2274
|
+
W[i] = (W[i-16] + s0 + W[i-7] + s1) & MASK64;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
let [a, b, c, d, e, f, g, h] = H;
|
|
2278
|
+
|
|
2279
|
+
for (let i = 0; i < 80; i++) {
|
|
2280
|
+
const S1 = rotr64(e, 14) ^ rotr64(e, 18) ^ rotr64(e, 41);
|
|
2281
|
+
const ch = (e & f) ^ (~e & MASK64 & g);
|
|
2282
|
+
const temp1 = (h + S1 + ch + K[i] + W[i]) & MASK64;
|
|
2283
|
+
const S0 = rotr64(a, 28) ^ rotr64(a, 34) ^ rotr64(a, 39);
|
|
2284
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
2285
|
+
const temp2 = (S0 + maj) & MASK64;
|
|
2286
|
+
|
|
2287
|
+
h = g; g = f; f = e;
|
|
2288
|
+
e = (d + temp1) & MASK64;
|
|
2289
|
+
d = c; c = b; b = a;
|
|
2290
|
+
a = (temp1 + temp2) & MASK64;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
H[0] = (H[0] + a) & MASK64;
|
|
2294
|
+
H[1] = (H[1] + b) & MASK64;
|
|
2295
|
+
H[2] = (H[2] + c) & MASK64;
|
|
2296
|
+
H[3] = (H[3] + d) & MASK64;
|
|
2297
|
+
H[4] = (H[4] + e) & MASK64;
|
|
2298
|
+
H[5] = (H[5] + f) & MASK64;
|
|
2299
|
+
H[6] = (H[6] + g) & MASK64;
|
|
2300
|
+
H[7] = (H[7] + h) & MASK64;
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const outputWords = is384 ? 6 : 8;
|
|
2304
|
+
const result = new ArrayBuffer(outputWords * 8);
|
|
2305
|
+
const rv = new DataView(result);
|
|
2306
|
+
for (let i = 0; i < outputWords; i++) {
|
|
2307
|
+
rv.setUint32(i * 8, Number((H[i] >> 32n) & 0xffffffffn), false);
|
|
2308
|
+
rv.setUint32(i * 8 + 4, Number(H[i] & 0xffffffffn), false);
|
|
2309
|
+
}
|
|
2310
|
+
return result;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
/**
|
|
2314
|
+
* SHA-384 digest (pure JS)
|
|
2315
|
+
*/
|
|
2316
|
+
async function jsSHA384(data: Uint8Array): Promise<ArrayBuffer> {
|
|
2317
|
+
return jsSHA512Core(data, true);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
/**
|
|
2321
|
+
* SHA-512 digest (pure JS)
|
|
2322
|
+
*/
|
|
2323
|
+
async function jsSHA512(data: Uint8Array): Promise<ArrayBuffer> {
|
|
2324
|
+
return jsSHA512Core(data, false);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
/**
|
|
2328
|
+
* Generic HMAC fallback used when native bridge callbacks are unavailable.
|
|
2329
|
+
*/
|
|
2330
|
+
async function jsHmac(key: Uint8Array, data: Uint8Array, hash: string): Promise<ArrayBuffer> {
|
|
2331
|
+
const normalizedHash = normalizeHashName(hash);
|
|
2332
|
+
if (normalizedHash === "SHA-256") {
|
|
2333
|
+
return jsHmacSha256(key, data);
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
const subtle = getPlatformSubtle();
|
|
2337
|
+
if (!subtle?.importKey || !subtle?.sign) {
|
|
2338
|
+
throw new DOMException(
|
|
2339
|
+
`Native crypto module not available for HMAC: ${normalizedHash}`,
|
|
2340
|
+
"NotSupportedError"
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
const importedKey = await subtle.importKey(
|
|
2345
|
+
"raw",
|
|
2346
|
+
key,
|
|
2347
|
+
{ name: "HMAC", hash: { name: normalizedHash } },
|
|
2348
|
+
false,
|
|
2349
|
+
["sign"]
|
|
2350
|
+
);
|
|
2351
|
+
return subtle.sign({ name: "HMAC" }, importedKey, data);
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
/**
|
|
2355
|
+
* HMAC-SHA256 implementation (for JS fallback)
|
|
2356
|
+
*/
|
|
2357
|
+
async function jsHmacSha256(key: Uint8Array, data: Uint8Array): Promise<ArrayBuffer> {
|
|
2358
|
+
const blockSize = 64;
|
|
2359
|
+
const hashSize = 32;
|
|
2360
|
+
|
|
2361
|
+
// If key is longer than block size, hash it
|
|
2362
|
+
let keyBytes = key;
|
|
2363
|
+
if (keyBytes.length > blockSize) {
|
|
2364
|
+
keyBytes = new Uint8Array(await jsSHA256(keyBytes));
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// Pad key to block size
|
|
2368
|
+
const paddedKey = new Uint8Array(blockSize);
|
|
2369
|
+
paddedKey.set(keyBytes);
|
|
2370
|
+
|
|
2371
|
+
// Create ipad and opad
|
|
2372
|
+
const ipad = new Uint8Array(blockSize);
|
|
2373
|
+
const opad = new Uint8Array(blockSize);
|
|
2374
|
+
for (let i = 0; i < blockSize; i++) {
|
|
2375
|
+
ipad[i] = paddedKey[i] ^ 0x36;
|
|
2376
|
+
opad[i] = paddedKey[i] ^ 0x5c;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
// Inner hash: H(ipad || data)
|
|
2380
|
+
const innerData = new Uint8Array(blockSize + data.length);
|
|
2381
|
+
innerData.set(ipad);
|
|
2382
|
+
innerData.set(data, blockSize);
|
|
2383
|
+
const innerHash = new Uint8Array(await jsSHA256(innerData));
|
|
2384
|
+
|
|
2385
|
+
// Outer hash: H(opad || inner_hash)
|
|
2386
|
+
const outerData = new Uint8Array(blockSize + hashSize);
|
|
2387
|
+
outerData.set(opad);
|
|
2388
|
+
outerData.set(innerHash, blockSize);
|
|
2389
|
+
|
|
2390
|
+
return jsSHA256(outerData);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* Convert Uint8Array to its underlying ArrayBuffer (handling offset/length)
|
|
2395
|
+
*/
|
|
2396
|
+
function uint8ArrayToArrayBuffer(data: Uint8Array): ArrayBuffer {
|
|
2397
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
/**
|
|
2401
|
+
* Convert Uint8Array to a binary string (one char per byte)
|
|
2402
|
+
*/
|
|
2403
|
+
function uint8ArrayToString(data: Uint8Array): string {
|
|
2404
|
+
let str = '';
|
|
2405
|
+
for (let i = 0; i < data.length; i++) {
|
|
2406
|
+
str += String.fromCharCode(data[i]);
|
|
2407
|
+
}
|
|
2408
|
+
return str;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
/**
|
|
2412
|
+
* Convert a hex string to an ArrayBuffer
|
|
2413
|
+
*/
|
|
2414
|
+
function hexStringToArrayBuffer(hex: string): ArrayBuffer {
|
|
2415
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
2416
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
2417
|
+
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
|
|
2418
|
+
}
|
|
2419
|
+
return bytes.buffer;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
/**
|
|
2423
|
+
* HKDF implementation (JS fallback) per RFC 5869
|
|
2424
|
+
* HKDF-Extract: PRK = HMAC-Hash(salt, IKM)
|
|
2425
|
+
* HKDF-Expand: iteratively compute T(i) = HMAC-Hash(PRK, T(i-1) || info || i) until enough bytes
|
|
2426
|
+
*/
|
|
2427
|
+
async function jsHkdf(
|
|
2428
|
+
ikm: Uint8Array,
|
|
2429
|
+
salt: Uint8Array,
|
|
2430
|
+
info: Uint8Array,
|
|
2431
|
+
hash: string,
|
|
2432
|
+
length: number
|
|
2433
|
+
): Promise<ArrayBuffer> {
|
|
2434
|
+
const hashLen = getHashLength(hash) / 8;
|
|
2435
|
+
|
|
2436
|
+
// If salt is empty, use a hash-length block of zeros
|
|
2437
|
+
const actualSalt = salt.length > 0 ? salt : new Uint8Array(hashLen);
|
|
2438
|
+
|
|
2439
|
+
// Extract: PRK = HMAC-Hash(salt, IKM)
|
|
2440
|
+
const prk = new Uint8Array(await jsHmac(actualSalt, ikm, hash));
|
|
2441
|
+
|
|
2442
|
+
// Expand
|
|
2443
|
+
const n = Math.ceil(length / hashLen);
|
|
2444
|
+
const okm = new Uint8Array(n * hashLen);
|
|
2445
|
+
let prev = new Uint8Array(0);
|
|
2446
|
+
|
|
2447
|
+
for (let i = 1; i <= n; i++) {
|
|
2448
|
+
const input = new Uint8Array(prev.length + info.length + 1);
|
|
2449
|
+
input.set(prev, 0);
|
|
2450
|
+
input.set(info, prev.length);
|
|
2451
|
+
input[prev.length + info.length] = i;
|
|
2452
|
+
prev = new Uint8Array(await jsHmac(prk, input, hash));
|
|
2453
|
+
okm.set(prev, (i - 1) * hashLen);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
return okm.buffer.slice(0, length);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
/**
|
|
2460
|
+
* Parse RSA PEM key (public or private) and extract JWK components.
|
|
2461
|
+
* This parses the base64-encoded DER within the PEM and extracts the
|
|
2462
|
+
* RSA parameters (n, e, d, p, q, dp, dq, qi) from the ASN.1 structure.
|
|
2463
|
+
*/
|
|
2464
|
+
function parseRsaPemToJwkComponents(pem: string, isPrivate: boolean): Record<string, string> {
|
|
2465
|
+
// Strip PEM headers and decode base64
|
|
2466
|
+
const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s/g, '');
|
|
2467
|
+
const der = base64Decode(b64);
|
|
2468
|
+
|
|
2469
|
+
// Parse ASN.1 DER
|
|
2470
|
+
const parsed = parseAsn1(der, 0);
|
|
2471
|
+
|
|
2472
|
+
if (isPrivate) {
|
|
2473
|
+
// PKCS#1 RSAPrivateKey or PKCS#8 PrivateKeyInfo
|
|
2474
|
+
// PKCS#8: SEQUENCE { version, AlgorithmIdentifier, OCTET STRING { RSAPrivateKey } }
|
|
2475
|
+
// PKCS#1: SEQUENCE { version, n, e, d, p, q, dp, dq, qi }
|
|
2476
|
+
let seq = parsed;
|
|
2477
|
+
if (seq.children && seq.children.length === 3 && seq.children[2].tag === 0x04) {
|
|
2478
|
+
// This is PKCS#8 - the RSAPrivateKey is inside the OCTET STRING
|
|
2479
|
+
seq = parseAsn1(seq.children[2].value as Uint8Array, 0);
|
|
2480
|
+
}
|
|
2481
|
+
if (!seq.children || seq.children.length < 9) {
|
|
2482
|
+
throw new DOMException('Invalid RSA private key structure', 'DataError');
|
|
2483
|
+
}
|
|
2484
|
+
// Skip version (index 0)
|
|
2485
|
+
return {
|
|
2486
|
+
n: base64UrlEncode(trimLeadingZero(seq.children[1].value as Uint8Array)),
|
|
2487
|
+
e: base64UrlEncode(trimLeadingZero(seq.children[2].value as Uint8Array)),
|
|
2488
|
+
d: base64UrlEncode(trimLeadingZero(seq.children[3].value as Uint8Array)),
|
|
2489
|
+
p: base64UrlEncode(trimLeadingZero(seq.children[4].value as Uint8Array)),
|
|
2490
|
+
q: base64UrlEncode(trimLeadingZero(seq.children[5].value as Uint8Array)),
|
|
2491
|
+
dp: base64UrlEncode(trimLeadingZero(seq.children[6].value as Uint8Array)),
|
|
2492
|
+
dq: base64UrlEncode(trimLeadingZero(seq.children[7].value as Uint8Array)),
|
|
2493
|
+
qi: base64UrlEncode(trimLeadingZero(seq.children[8].value as Uint8Array)),
|
|
2494
|
+
};
|
|
2495
|
+
} else {
|
|
2496
|
+
// PKCS#1 RSAPublicKey or SPKI SubjectPublicKeyInfo
|
|
2497
|
+
let seq = parsed;
|
|
2498
|
+
if (seq.children && seq.children.length === 2 && seq.children[1].tag === 0x03) {
|
|
2499
|
+
// SPKI: SEQUENCE { AlgorithmIdentifier, BIT STRING { RSAPublicKey } }
|
|
2500
|
+
const bitStr = seq.children[1].value as Uint8Array;
|
|
2501
|
+
// BIT STRING has a leading byte for unused bits count (usually 0x00)
|
|
2502
|
+
seq = parseAsn1(bitStr.slice(1), 0);
|
|
2503
|
+
}
|
|
2504
|
+
if (!seq.children || seq.children.length < 2) {
|
|
2505
|
+
throw new DOMException('Invalid RSA public key structure', 'DataError');
|
|
2506
|
+
}
|
|
2507
|
+
return {
|
|
2508
|
+
n: base64UrlEncode(trimLeadingZero(seq.children[0].value as Uint8Array)),
|
|
2509
|
+
e: base64UrlEncode(trimLeadingZero(seq.children[1].value as Uint8Array)),
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
/**
|
|
2515
|
+
* Minimal ASN.1 DER parser
|
|
2516
|
+
*/
|
|
2517
|
+
interface Asn1Node {
|
|
2518
|
+
tag: number;
|
|
2519
|
+
value: Uint8Array;
|
|
2520
|
+
children?: Asn1Node[];
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function parseAsn1(data: Uint8Array, offset: number): Asn1Node {
|
|
2524
|
+
const tag = data[offset];
|
|
2525
|
+
let lengthOffset = offset + 1;
|
|
2526
|
+
let length: number;
|
|
2527
|
+
|
|
2528
|
+
if (data[lengthOffset] & 0x80) {
|
|
2529
|
+
const numLengthBytes = data[lengthOffset] & 0x7f;
|
|
2530
|
+
length = 0;
|
|
2531
|
+
for (let i = 0; i < numLengthBytes; i++) {
|
|
2532
|
+
length = (length << 8) | data[lengthOffset + 1 + i];
|
|
2533
|
+
}
|
|
2534
|
+
lengthOffset += 1 + numLengthBytes;
|
|
2535
|
+
} else {
|
|
2536
|
+
length = data[lengthOffset];
|
|
2537
|
+
lengthOffset += 1;
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
const value = data.slice(lengthOffset, lengthOffset + length);
|
|
2541
|
+
const node: Asn1Node = { tag, value };
|
|
2542
|
+
|
|
2543
|
+
// If it's a constructed type (SEQUENCE, SET), parse children
|
|
2544
|
+
if (tag === 0x30 || tag === 0x31) {
|
|
2545
|
+
node.children = [];
|
|
2546
|
+
let childOffset = 0;
|
|
2547
|
+
while (childOffset < value.length) {
|
|
2548
|
+
const child = parseAsn1(value, childOffset);
|
|
2549
|
+
node.children.push(child);
|
|
2550
|
+
// Compute child's total size
|
|
2551
|
+
let childLength = child.value.length;
|
|
2552
|
+
let headerLen = 2; // tag + length byte
|
|
2553
|
+
if (childLength >= 128) {
|
|
2554
|
+
let tmp = childLength;
|
|
2555
|
+
let numBytes = 0;
|
|
2556
|
+
while (tmp > 0) { numBytes++; tmp >>= 8; }
|
|
2557
|
+
headerLen = 2 + numBytes;
|
|
2558
|
+
}
|
|
2559
|
+
childOffset += headerLen + childLength;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// For INTEGER, store raw value bytes (may have leading zero for positive)
|
|
2564
|
+
return node;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/**
|
|
2568
|
+
* Trim the leading zero byte from an ASN.1 INTEGER value
|
|
2569
|
+
* (ASN.1 uses a leading 0x00 to indicate a positive number when the high bit is set)
|
|
2570
|
+
*/
|
|
2571
|
+
function trimLeadingZero(data: Uint8Array): Uint8Array {
|
|
2572
|
+
if (data.length > 1 && data[0] === 0x00) {
|
|
2573
|
+
return data.slice(1);
|
|
2574
|
+
}
|
|
2575
|
+
return data;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
/**
|
|
2579
|
+
* Standard base64 decode (not base64url)
|
|
2580
|
+
*/
|
|
2581
|
+
function base64Decode(str: string): Uint8Array {
|
|
2582
|
+
const binary = atob(str);
|
|
2583
|
+
const bytes = new Uint8Array(binary.length);
|
|
2584
|
+
for (let i = 0; i < binary.length; i++) {
|
|
2585
|
+
bytes[i] = binary.charCodeAt(i);
|
|
2586
|
+
}
|
|
2587
|
+
return bytes;
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Standard base64 encode
|
|
2592
|
+
*/
|
|
2593
|
+
function base64Encode(data: Uint8Array): string {
|
|
2594
|
+
return btoa(String.fromCharCode(...data));
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
/**
|
|
2598
|
+
* Convert RSA JWK to PEM format
|
|
2599
|
+
* Constructs ASN.1 DER for PKCS#1 and wraps in PEM
|
|
2600
|
+
*/
|
|
2601
|
+
function rsaJwkToPem(jwk: JsonWebKey, isPrivate: boolean): string {
|
|
2602
|
+
if (isPrivate) {
|
|
2603
|
+
// PKCS#1 RSAPrivateKey
|
|
2604
|
+
const version = encodeAsn1Integer(new Uint8Array([0]));
|
|
2605
|
+
const n = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.n!)));
|
|
2606
|
+
const e = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.e!)));
|
|
2607
|
+
const d = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.d!)));
|
|
2608
|
+
const p = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.p!)));
|
|
2609
|
+
const q = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.q!)));
|
|
2610
|
+
const dp = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.dp!)));
|
|
2611
|
+
const dq = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.dq!)));
|
|
2612
|
+
const qi = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.qi!)));
|
|
2613
|
+
|
|
2614
|
+
const inner = concatUint8Arrays([version, n, e, d, p, q, dp, dq, qi]);
|
|
2615
|
+
const seq = encodeAsn1Sequence(inner);
|
|
2616
|
+
|
|
2617
|
+
// Wrap in PKCS#8 PrivateKeyInfo
|
|
2618
|
+
// SEQUENCE { INTEGER 0, SEQUENCE { OID rsaEncryption, NULL }, OCTET STRING { seq } }
|
|
2619
|
+
const pkcs8Version = encodeAsn1Integer(new Uint8Array([0]));
|
|
2620
|
+
const rsaOid = new Uint8Array([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]); // OID 1.2.840.113549.1.1.1
|
|
2621
|
+
const nullTag = new Uint8Array([0x05, 0x00]);
|
|
2622
|
+
const algSeq = encodeAsn1Sequence(concatUint8Arrays([rsaOid, nullTag]));
|
|
2623
|
+
const octetStr = encodeAsn1Tag(0x04, seq);
|
|
2624
|
+
const pkcs8 = encodeAsn1Sequence(concatUint8Arrays([pkcs8Version, algSeq, octetStr]));
|
|
2625
|
+
|
|
2626
|
+
const b64 = base64Encode(pkcs8);
|
|
2627
|
+
const lines = b64.match(/.{1,64}/g) || [];
|
|
2628
|
+
return '-----BEGIN PRIVATE KEY-----\n' + lines.join('\n') + '\n-----END PRIVATE KEY-----\n';
|
|
2629
|
+
} else {
|
|
2630
|
+
// PKCS#1 RSAPublicKey wrapped in SubjectPublicKeyInfo (SPKI)
|
|
2631
|
+
const n = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.n!)));
|
|
2632
|
+
const e = encodeAsn1Integer(addLeadingZero(base64UrlDecode(jwk.e!)));
|
|
2633
|
+
const rsaPubKey = encodeAsn1Sequence(concatUint8Arrays([n, e]));
|
|
2634
|
+
|
|
2635
|
+
// SubjectPublicKeyInfo: SEQUENCE { AlgorithmIdentifier, BIT STRING }
|
|
2636
|
+
const rsaOid = new Uint8Array([0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01]);
|
|
2637
|
+
const nullTag = new Uint8Array([0x05, 0x00]);
|
|
2638
|
+
const algSeq = encodeAsn1Sequence(concatUint8Arrays([rsaOid, nullTag]));
|
|
2639
|
+
// BIT STRING: 0x00 prefix (no unused bits) + DER-encoded RSAPublicKey
|
|
2640
|
+
const bitStrContent = concatUint8Arrays([new Uint8Array([0x00]), rsaPubKey]);
|
|
2641
|
+
const bitStr = encodeAsn1Tag(0x03, bitStrContent);
|
|
2642
|
+
const spki = encodeAsn1Sequence(concatUint8Arrays([algSeq, bitStr]));
|
|
2643
|
+
|
|
2644
|
+
const b64 = base64Encode(spki);
|
|
2645
|
+
const lines = b64.match(/.{1,64}/g) || [];
|
|
2646
|
+
return '-----BEGIN PUBLIC KEY-----\n' + lines.join('\n') + '\n-----END PUBLIC KEY-----\n';
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
|
|
2650
|
+
/** Encode ASN.1 INTEGER */
|
|
2651
|
+
function encodeAsn1Integer(data: Uint8Array): Uint8Array {
|
|
2652
|
+
return encodeAsn1Tag(0x02, data);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/** Encode ASN.1 SEQUENCE */
|
|
2656
|
+
function encodeAsn1Sequence(content: Uint8Array): Uint8Array {
|
|
2657
|
+
return encodeAsn1Tag(0x30, content);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2660
|
+
/** Encode an ASN.1 TLV (tag-length-value) */
|
|
2661
|
+
function encodeAsn1Tag(tag: number, content: Uint8Array): Uint8Array {
|
|
2662
|
+
const length = content.length;
|
|
2663
|
+
let header: Uint8Array;
|
|
2664
|
+
if (length < 128) {
|
|
2665
|
+
header = new Uint8Array([tag, length]);
|
|
2666
|
+
} else if (length < 256) {
|
|
2667
|
+
header = new Uint8Array([tag, 0x81, length]);
|
|
2668
|
+
} else if (length < 65536) {
|
|
2669
|
+
header = new Uint8Array([tag, 0x82, (length >> 8) & 0xff, length & 0xff]);
|
|
2670
|
+
} else {
|
|
2671
|
+
header = new Uint8Array([tag, 0x83, (length >> 16) & 0xff, (length >> 8) & 0xff, length & 0xff]);
|
|
2672
|
+
}
|
|
2673
|
+
return concatUint8Arrays([header, content]);
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
/** Add leading zero byte if high bit is set (for ASN.1 positive INTEGER) */
|
|
2677
|
+
function addLeadingZero(data: Uint8Array): Uint8Array {
|
|
2678
|
+
if (data.length > 0 && (data[0] & 0x80) !== 0) {
|
|
2679
|
+
const result = new Uint8Array(data.length + 1);
|
|
2680
|
+
result[0] = 0x00;
|
|
2681
|
+
result.set(data, 1);
|
|
2682
|
+
return result;
|
|
2683
|
+
}
|
|
2684
|
+
return data;
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/** Concatenate multiple Uint8Arrays */
|
|
2688
|
+
function concatUint8Arrays(arrays: Uint8Array[]): Uint8Array {
|
|
2689
|
+
let totalLen = 0;
|
|
2690
|
+
for (const arr of arrays) totalLen += arr.length;
|
|
2691
|
+
const result = new Uint8Array(totalLen);
|
|
2692
|
+
let offset = 0;
|
|
2693
|
+
for (const arr of arrays) {
|
|
2694
|
+
result.set(arr, offset);
|
|
2695
|
+
offset += arr.length;
|
|
2696
|
+
}
|
|
2697
|
+
return result;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/**
|
|
2701
|
+
* PBKDF2-SHA256 implementation (for JS fallback)
|
|
2702
|
+
*/
|
|
2703
|
+
async function jsPbkdf2Sha256(
|
|
2704
|
+
password: Uint8Array,
|
|
2705
|
+
salt: Uint8Array,
|
|
2706
|
+
iterations: number,
|
|
2707
|
+
keyLength: number
|
|
2708
|
+
): Promise<ArrayBuffer> {
|
|
2709
|
+
const hashLength = 32; // SHA-256 output
|
|
2710
|
+
const numBlocks = Math.ceil(keyLength / hashLength);
|
|
2711
|
+
const result = new Uint8Array(numBlocks * hashLength);
|
|
2712
|
+
|
|
2713
|
+
for (let block = 1; block <= numBlocks; block++) {
|
|
2714
|
+
// U1 = PRF(Password, Salt || INT(i))
|
|
2715
|
+
const blockData = new Uint8Array(salt.length + 4);
|
|
2716
|
+
blockData.set(salt);
|
|
2717
|
+
blockData[salt.length] = (block >> 24) & 0xff;
|
|
2718
|
+
blockData[salt.length + 1] = (block >> 16) & 0xff;
|
|
2719
|
+
blockData[salt.length + 2] = (block >> 8) & 0xff;
|
|
2720
|
+
blockData[salt.length + 3] = block & 0xff;
|
|
2721
|
+
|
|
2722
|
+
let u = new Uint8Array(await jsHmacSha256(password, blockData));
|
|
2723
|
+
const f = new Uint8Array(u);
|
|
2724
|
+
|
|
2725
|
+
// U2...Uc
|
|
2726
|
+
for (let i = 1; i < iterations; i++) {
|
|
2727
|
+
u = new Uint8Array(await jsHmacSha256(password, u));
|
|
2728
|
+
for (let j = 0; j < hashLength; j++) {
|
|
2729
|
+
f[j] ^= u[j];
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
result.set(f, (block - 1) * hashLength);
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
return result.buffer.slice(0, keyLength);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
// =============================================================================
|
|
2740
|
+
// Singleton Export
|
|
2741
|
+
// =============================================================================
|
|
2742
|
+
|
|
2743
|
+
export const crypto = new Crypto();
|