gdc-common-utils-ts 1.0.1

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.
Files changed (100) hide show
  1. package/PUBLISHING.md +33 -0
  2. package/__tests__/AesManager.test.ts +53 -0
  3. package/__tests__/CryptographyService.test.ts +194 -0
  4. package/__tests__/bundle.test.ts +29 -0
  5. package/__tests__/content.test.ts +72 -0
  6. package/__tests__/crypto-encode-decode.test.ts +52 -0
  7. package/__tests__/crypto-hmac.test.ts +21 -0
  8. package/__tests__/did-generateServiceId.errors.test.ts +8 -0
  9. package/__tests__/did-generateServiceId.test.ts +18 -0
  10. package/__tests__/models-clinical-sections.test.ts +32 -0
  11. package/__tests__/models-multibase58.test.ts +33 -0
  12. package/__tests__/multibase58.errors.test.ts +7 -0
  13. package/__tests__/multibase58.test.ts +28 -0
  14. package/__tests__/multibasehash.test.ts +25 -0
  15. package/__tests__/utils-actor.test.ts +22 -0
  16. package/__tests__/utils-base-convert.test.ts +57 -0
  17. package/__tests__/utils-baseN.test.ts +40 -0
  18. package/__tests__/utils-did-extra.test.ts +33 -0
  19. package/__tests__/utils-format-converter.test.ts +87 -0
  20. package/__tests__/utils-jwt.test.ts +57 -0
  21. package/__tests__/utils-manager-error.test.ts +11 -0
  22. package/__tests__/utils-normalize.test.ts +15 -0
  23. package/__tests__/utils-object-convert.test.ts +38 -0
  24. package/__tests__/utils-string-convert.test.ts +20 -0
  25. package/__tests__/utils-string-utils.test.ts +25 -0
  26. package/__tests__/utils-url.test.ts +21 -0
  27. package/babel.config.cjs +5 -0
  28. package/jest.config.ts +46 -0
  29. package/package.json +36 -0
  30. package/src/AesManager.ts +82 -0
  31. package/src/CryptographyService.ts +461 -0
  32. package/src/JweManager.ts.txt +365 -0
  33. package/src/KmsService.txt +493 -0
  34. package/src/constants/Schemas.ts +61 -0
  35. package/src/constants/index.ts +1 -0
  36. package/src/constants/schemaorg.ts +193 -0
  37. package/src/cryptoDecode.ts +104 -0
  38. package/src/cryptoEncode.ts +36 -0
  39. package/src/cryptography.abstract.ts +29 -0
  40. package/src/hmac.ts +15 -0
  41. package/src/index.ts +3 -0
  42. package/src/interfaces/Cryptography.types.ts +131 -0
  43. package/src/interfaces/ICryptoHelper.ts +33 -0
  44. package/src/interfaces/ICryptography.ts +177 -0
  45. package/src/interfaces/IWallet.ts +62 -0
  46. package/src/interfaces/MlDsa.ts +25 -0
  47. package/src/interfaces/MlKem.ts +18 -0
  48. package/src/models/aes.ts +93 -0
  49. package/src/models/auth.ts +38 -0
  50. package/src/models/bundle.ts +152 -0
  51. package/src/models/bundle.txt +93 -0
  52. package/src/models/clinical-sections.en.ts +82 -0
  53. package/src/models/clinical-sections.ts +64 -0
  54. package/src/models/comm.ts +63 -0
  55. package/src/models/confidential-job.ts +100 -0
  56. package/src/models/confidential-message.ts +137 -0
  57. package/src/models/confidential-storage.ts +170 -0
  58. package/src/models/consent-rule.ts +141 -0
  59. package/src/models/crypto.ts +43 -0
  60. package/src/models/device-license.ts +161 -0
  61. package/src/models/did.ts +81 -0
  62. package/src/models/index.ts +31 -0
  63. package/src/models/indexing.ts +20 -0
  64. package/src/models/issue.ts +85 -0
  65. package/src/models/jsonapi.ts +19 -0
  66. package/src/models/jwe.ts +132 -0
  67. package/src/models/jwk.ts +50 -0
  68. package/src/models/jws.ts +42 -0
  69. package/src/models/jwt.ts +15 -0
  70. package/src/models/multibase58.ts +46 -0
  71. package/src/models/oidc4ida.common.model.ts +39 -0
  72. package/src/models/oidc4ida.document.model.ts +61 -0
  73. package/src/models/oidc4ida.electronicRecord.model.ts +86 -0
  74. package/src/models/oidc4ida.evidence.model.ts +69 -0
  75. package/src/models/openid-device.ts +146 -0
  76. package/src/models/operation-outcome.ts +34 -0
  77. package/src/models/params.ts +142 -0
  78. package/src/models/resource-document.ts +21 -0
  79. package/src/models/response.ts +5 -0
  80. package/src/models/urlPath.ts +76 -0
  81. package/src/models/verifiable-credential.ts +52 -0
  82. package/src/types/noble-hashes.d.ts +4 -0
  83. package/src/utils/actor.ts +52 -0
  84. package/src/utils/base-convert.ts +77 -0
  85. package/src/utils/baseN.ts +203 -0
  86. package/src/utils/bundle.ts +30 -0
  87. package/src/utils/content.ts +66 -0
  88. package/src/utils/did.ts +155 -0
  89. package/src/utils/format-converter.ts +119 -0
  90. package/src/utils/index.ts +13 -0
  91. package/src/utils/jwt.ts +165 -0
  92. package/src/utils/manager-error.ts +27 -0
  93. package/src/utils/multibase58.ts +46 -0
  94. package/src/utils/multibasehash.ts +28 -0
  95. package/src/utils/normalize.ts +43 -0
  96. package/src/utils/object-convert.ts +57 -0
  97. package/src/utils/string-convert.ts +71 -0
  98. package/src/utils/string-utils.ts +70 -0
  99. package/src/utils/url.ts +46 -0
  100. package/tsconfig.json +13 -0
@@ -0,0 +1,461 @@
1
+ // Copyright 2025 Antifraud Services Inc. under the Apache License, Version 2.0.
2
+ // File: crypto-ts/CryptographyService.ts
3
+
4
+ // Use `import * as pako` to ensure compatibility with CommonJS/ESM module resolution.
5
+ // This resolves a stubborn TypeScript error (`esModuleInterop`) during testing.
6
+
7
+ import { ICryptoHelper } from './interfaces/ICryptoHelper';
8
+ import * as pako from 'pako';
9
+ import * as jwtUtils from './utils/jwt';
10
+ import { ICryptography } from './interfaces/ICryptography';
11
+ import { AesManager } from './AesManager';
12
+ import { DataCompactJWT, JwtCompactParts } from './models/jwt';
13
+ import { JweObject, ProtectedHeadersJWE, RecipientDataJWE } from './models/jwe';
14
+ import { MlkemPublicJwk, MldsaPublicJwk, PublicJwk, MlkemPrivateJwk, MldsaAlg, MlkemCurve, BaseJwk, EcBaseJwk } from './interfaces/Cryptography.types';
15
+ import { ProtectedDataAES } from './models/aes';
16
+ import { Content } from './utils/content';
17
+
18
+
19
+ /**
20
+ * Implements the ICryptography interface, providing a complete suite of low-level,
21
+ * stateless cryptographic functions. This service is the "engine" of the security layer,
22
+ * orchestrating Post-Quantum and AES primitives.
23
+ */
24
+ export class CryptographyService implements ICryptography {
25
+ private aesManager: AesManager;
26
+ private cryptoHelper: ICryptoHelper;
27
+ private mlDsaModule: any | null = null;
28
+ private mlKemModule: any | null = null;
29
+
30
+ // Constants for seed sizes, as per @noble library requirements.
31
+ private readonly ML_KEM_SEED_SIZE = 64;
32
+ private readonly ML_DSA_SEED_SIZE = 32;
33
+
34
+ constructor(cryptoHelper: ICryptoHelper) {
35
+ this.aesManager = new AesManager();
36
+ this.cryptoHelper = cryptoHelper;
37
+ }
38
+
39
+ private async loadMlDsa(): Promise<any> {
40
+ if (this.mlDsaModule) return this.mlDsaModule;
41
+ try {
42
+ // Use explicit .js subpath to satisfy package exports in Metro/Node ESM.
43
+ const module = await import('@noble/post-quantum/ml-dsa.js');
44
+ this.mlDsaModule = module;
45
+ return module;
46
+ } catch (error) {
47
+ throw new Error(
48
+ '[CryptographyService] Missing dependency "@noble/post-quantum/ml-dsa.js". Install it for ML-DSA operations.',
49
+ );
50
+ }
51
+ }
52
+
53
+ private async loadMlKem(): Promise<any> {
54
+ if (this.mlKemModule) return this.mlKemModule;
55
+ try {
56
+ // Use explicit .js subpath to satisfy package exports in Metro/Node ESM.
57
+ const module = await import('@noble/post-quantum/ml-kem.js');
58
+ this.mlKemModule = module;
59
+ return module;
60
+ } catch (error) {
61
+ throw new Error(
62
+ '[CryptographyService] Missing dependency "@noble/post-quantum/ml-kem.js". Install it for ML-KEM operations.',
63
+ );
64
+ }
65
+ }
66
+
67
+ digestString(data: string, algorithm: string): Promise<string> {
68
+ return this.cryptoHelper.digestString(data, algorithm);
69
+ }
70
+
71
+ // --- Key Generation ---
72
+
73
+ async generateKeyPairMlKem(seedBytes?: Uint8Array, crv: MlkemCurve = 'ML-KEM-768'): Promise<{ publicJWKey: MlkemPublicJwk & { kid: string }; secretKeyBytes: Uint8Array }> {
74
+ const mlKem = await this.loadMlKem();
75
+ let seed: Uint8Array;
76
+ if (seedBytes && seedBytes.length === this.ML_KEM_SEED_SIZE) {
77
+ seed = seedBytes;
78
+ } else {
79
+ seed = await this.cryptoHelper.getRandomBytes(this.ML_KEM_SEED_SIZE);
80
+ }
81
+
82
+ let keygenFn: (seed: Uint8Array) => { publicKey: Uint8Array; secretKey: Uint8Array; };
83
+ switch (crv) {
84
+ case 'ML-KEM-512': keygenFn = mlKem.ml_kem512.keygen; break;
85
+ case 'ML-KEM-1024': keygenFn = mlKem.ml_kem1024.keygen; break;
86
+ case 'ML-KEM-768':
87
+ default:
88
+ keygenFn = mlKem.ml_kem768.keygen; break;
89
+ }
90
+
91
+ const { secretKey, publicKey: publicKeyBytes } = keygenFn(seed);
92
+ const pubJwkWithoutKid: MlkemPublicJwk = {
93
+ kty: 'OKP', crv: crv, x: Content.bytesToRawBase64UrlSafe(publicKeyBytes),
94
+ };
95
+ const kid = await this._computeJwkThumbprint(pubJwkWithoutKid);
96
+ const publicKey = { ...pubJwkWithoutKid, kid };
97
+ return { publicJWKey: publicKey, secretKeyBytes: secretKey };
98
+ }
99
+
100
+ async generateKeyPairMlDsa(seedBytes?: Uint8Array, alg: MldsaAlg = 'ML-DSA-44'): Promise<{ publicJWKey: MldsaPublicJwk & { kid: string }; secretKeyBytes: Uint8Array }> {
101
+ const mlDsa = await this.loadMlDsa();
102
+ let seed: Uint8Array;
103
+ if (seedBytes && seedBytes.length === this.ML_DSA_SEED_SIZE) {
104
+ seed = seedBytes;
105
+ } else {
106
+ seed = await this.cryptoHelper.getRandomBytes(this.ML_DSA_SEED_SIZE);
107
+ }
108
+
109
+ let keygenFn: (seed: Uint8Array) => { publicKey: Uint8Array; secretKey: Uint8Array; };
110
+ switch (alg) {
111
+ case 'ML-DSA-65': keygenFn = mlDsa.ml_dsa65.keygen; break;
112
+ case 'ML-DSA-87': keygenFn = mlDsa.ml_dsa87.keygen; break;
113
+ case 'ML-DSA-44':
114
+ default:
115
+ keygenFn = mlDsa.ml_dsa44.keygen; break;
116
+ }
117
+
118
+ const { secretKey, publicKey: publicKeyBytes } = keygenFn(seed);
119
+ const pubJwkWithoutKid: MldsaPublicJwk = {
120
+ kty: 'AKP', alg: alg, pub: Content.bytesToRawBase64UrlSafe(publicKeyBytes),
121
+ };
122
+ const kid = await this._computeJwkThumbprint(pubJwkWithoutKid);
123
+ const publicKey = { ...pubJwkWithoutKid, kid };
124
+ return { publicJWKey: publicKey, secretKeyBytes: secretKey };
125
+ }
126
+
127
+
128
+ // --- High-Level Workflows ---
129
+
130
+ async encryptJwe(payload: object, protectedHeader: object, secretJWKey: MlkemPrivateJwk, recipientsJWKeys: MlkemPublicJwk[]): Promise<JweObject> {
131
+ // ARCHITECTURAL NOTE: This implementation is currently only suitable for a single recipient.
132
+ // A Key Encapsulation Mechanism (KEM) derives a *different* shared secret for each recipient's public key.
133
+ // A true multi-recipient JWE requires a single Content Encryption Key (CEK) that is then
134
+ // encrypted (wrapped) for each recipient. This code uses the KEM-derived shared secret as the CEK.
135
+ // This must be refactored to a key-wrapping approach to support multiple recipients correctly.
136
+ if (recipientsJWKeys.length !== 1) {
137
+ // Temporarily throw until the architecture is fixed for multi-recipient.
138
+ throw new Error("CryptographyService.encryptJwe currently only supports a single recipient.");
139
+ }
140
+ const recipient = recipientsJWKeys[0];
141
+ const publicKeyBytes = Content.base64ToBytes(recipient.x);
142
+
143
+ // Per RFC 9278, we generate a random seed for the KEM. The KEM then derives both the
144
+ // final Content Encryption Key (CEK) and the encapsulated key from this seed.
145
+
146
+ const cekSeedBytes = await this.cryptoHelper.getRandomBytes(32);
147
+ const {
148
+ derivedCekBytes, // This is the actual Content Encryption Key
149
+ encapsulatedCekBytes // This is the encrypted key for the recipient
150
+ } = await this.encapsulate(cekSeedBytes, secretJWKey.dBytes, publicKeyBytes);
151
+
152
+ // 2. Now, use the *derived* CEK to encrypt the payload with AES.
153
+ const protectedHeaderB64Url = Content.objectToRawBase64UrlSafe(protectedHeader);
154
+ let payloadBytes = Content.objectToBytes(payload);
155
+ let payloadString: string;
156
+ if ((protectedHeader as ProtectedHeadersJWE).zip === 'DEF') {
157
+ payloadBytes = pako.deflate(payloadBytes);
158
+ payloadString = Content.bytesToRawBase64UrlSafe(payloadBytes);
159
+ } else {
160
+ payloadString = Content.bytesToStringASCII(payloadBytes);
161
+ }
162
+ const encrypted = await this.encrypt(payloadString, derivedCekBytes, protectedHeaderB64Url);
163
+
164
+ // 3. Assemble the JWE. The `encrypted_key` is the result of the KEM encapsulation.
165
+ const recipientData: RecipientDataJWE[] = [{
166
+ header: { alg: recipient.crv, kid: recipient.kid! },
167
+ encrypted_key: Content.bytesToRawBase64UrlSafe(encapsulatedCekBytes),
168
+ }];
169
+
170
+ return {
171
+ protected: protectedHeaderB64Url,
172
+ recipients: recipientData,
173
+ iv: encrypted.iv,
174
+ ciphertext: encrypted.ciphertext,
175
+ tag: encrypted.tag,
176
+ };
177
+ }
178
+
179
+ async encryptJweToCompact(payload: object | string, protectedHeader: object, secretJWKey: MlkemPrivateJwk, recipientJWKey: MlkemPublicJwk): Promise<string> {
180
+ // 1. Construct the complete, final protected header by merging the main and recipient headers.
181
+ const recipientHeader = { alg: recipientJWKey.crv, kid: recipientJWKey.kid! };
182
+ const finalProtectedHeader = { ...protectedHeader, ...recipientHeader };
183
+ const protectedHeaderB64Url = Content.objectToRawBase64UrlSafe(finalProtectedHeader);
184
+
185
+ // 2. Perform KEM to derive the Content Encryption Key (CEK).
186
+ const publicKeyBytes = Content.base64ToBytes(recipientJWKey.x);
187
+ const cekSeedBytes = await this.cryptoHelper.getRandomBytes(32);
188
+ const { derivedCekBytes, encapsulatedCekBytes } = await this.encapsulate(cekSeedBytes, secretJWKey.dBytes, publicKeyBytes);
189
+ const encapsulatedKeyB64Url = Content.bytesToRawBase64UrlSafe(encapsulatedCekBytes);
190
+
191
+ // 3. Encrypt the payload using the derived CEK and the *final* protected header as AAD.
192
+ const payloadBytes = typeof payload === 'string'
193
+ ? Content.stringToBytesUTF8(payload)
194
+ : Content.objectToBytes(payload);
195
+
196
+ if ((finalProtectedHeader as ProtectedHeadersJWE).zip === 'DEF') {
197
+ // Note: Compressing a compact JWS string is often inefficient, but supported.
198
+ const compressedPayload = pako.deflate(payloadBytes);
199
+ const payloadString = Content.bytesToRawBase64UrlSafe(compressedPayload);
200
+ const encrypted = await this.encrypt(payloadString, derivedCekBytes, protectedHeaderB64Url);
201
+ return `${protectedHeaderB64Url}.${encapsulatedKeyB64Url}.${encrypted.iv}.${encrypted.ciphertext}.${encrypted.tag}`;
202
+ }
203
+
204
+ const payloadString = Content.bytesToStringASCII(payloadBytes);
205
+ const encrypted = await this.encrypt(payloadString, derivedCekBytes, protectedHeaderB64Url);
206
+
207
+ // 4. Assemble the 5 parts of the compact JWE.
208
+ return `${protectedHeaderB64Url}.${encapsulatedKeyB64Url}.${encrypted.iv}.${encrypted.ciphertext}.${encrypted.tag}`;
209
+ }
210
+
211
+ async decryptJwe(
212
+ jwe: JweObject | string,
213
+ secretKeyJwk: MlkemPrivateJwk
214
+ ): Promise<{ decryptedBytes: Uint8Array, protectedHeader: object }> {
215
+ const jweObject = typeof jwe === 'string' ? this.parseCompactJwe(jwe) : jwe;
216
+
217
+ const recipient = jweObject.recipients.find(r => r.header?.kid === secretKeyJwk.kid);
218
+ if (!recipient || !recipient.encrypted_key) {
219
+ throw new Error(`JWE does not contain a recipient with kid=${secretKeyJwk.kid}`);
220
+ }
221
+
222
+ // Decapsulate to get the CEK
223
+ const encapsulatedKeyBytes = Content.base64ToBytes(recipient.encrypted_key);
224
+ const cekBytes = await this.decapsulate(encapsulatedKeyBytes, secretKeyJwk.dBytes);
225
+
226
+ // Decrypt the payload
227
+ const encryptedData = { ciphertext: jweObject.ciphertext, iv: jweObject.iv, tag: jweObject.tag };
228
+ const decryptedPayloadString = await this.decrypt(encryptedData, cekBytes, jweObject.protected);
229
+
230
+ // Handle decompression
231
+ const protectedHeader = Content.base64UrlSafeToJSON(jweObject.protected) as ProtectedHeadersJWE;
232
+ let decryptedBytes: Uint8Array;
233
+ if (protectedHeader.zip === 'DEF') {
234
+ const compressedBytes = Content.base64ToBytes(decryptedPayloadString);
235
+ decryptedBytes = pako.inflate(compressedBytes);
236
+ } else {
237
+ decryptedBytes = Content.stringToBytesUTF8(decryptedPayloadString);
238
+ }
239
+
240
+ return { decryptedBytes, protectedHeader };
241
+ }
242
+
243
+ getRecipientKidsFromJwe(jwe: JweObject | string): string[] {
244
+ const jweObject = typeof jwe === 'string' ? this.parseCompactJwe(jwe) : jwe;
245
+
246
+ if (!jweObject.recipients) {
247
+ return [];
248
+ }
249
+
250
+ return jweObject.recipients
251
+ .map(recipient => recipient.header?.kid)
252
+ .filter((kid): kid is string => !!kid);
253
+ }
254
+
255
+ async signDataJws(payload: object, protectedHeader: object, secretKeyBytes: Uint8Array): Promise<JwtCompactParts> {
256
+ const protectedHeaderB64Url = Content.objectToRawBase64UrlSafe(protectedHeader);
257
+ const payloadB64Url = await jwtUtils.encodePayload(payload);
258
+ const signingInput = `${protectedHeaderB64Url}.${payloadB64Url}`;
259
+ const signingInputBytes = Content.stringToBytesUTF8(signingInput);
260
+
261
+ // Infer algorithm from protected header
262
+ const alg = (protectedHeader as any).alg as MldsaAlg;
263
+ if (!alg) throw new Error("Protected header must contain 'alg' property for signing.");
264
+
265
+ const signatureBytes = await this.signBytes(signingInputBytes, secretKeyBytes, alg);
266
+
267
+ const jwsParts: JwtCompactParts = {
268
+ protected: protectedHeaderB64Url,
269
+ payload: payloadB64Url,
270
+ signature: Content.bytesToRawBase64UrlSafe(signatureBytes),
271
+ };
272
+
273
+ return jwsParts;
274
+ }
275
+
276
+ async verifyJws(jws: JwtCompactParts | string, publicJwk: PublicJwk): Promise<boolean> {
277
+ const parts = typeof jws === 'string' ? jwtUtils.getPartsJWT(jws) : jws;
278
+ if (!parts) throw new Error('Invalid Compact JWS format');
279
+ const signingInput = `${parts.protected}.${parts.payload}`;
280
+ const signingInputBytes = Content.stringToBytesUTF8(signingInput);
281
+ const signatureBytes = Content.base64ToBytes(parts.signature as string);
282
+ return this.verifyBytes(signatureBytes, signingInputBytes, publicJwk);
283
+ }
284
+
285
+ async verifyDetachedJws(payloadBytes: Uint8Array, detachedJws: string, publicJWKey: PublicJwk): Promise<boolean> {
286
+ const parts = detachedJws.split('..');
287
+ if (parts.length !== 2) throw new Error("Invalid Detached JWS format");
288
+ const protectedHeaderB64Url = parts[0];
289
+ const signatureB64Url = parts[1];
290
+
291
+ const payloadB64Url = Content.bytesToRawBase64UrlSafe(payloadBytes);
292
+ const signingInput = `${protectedHeaderB64Url}.${payloadB64Url}`;
293
+ const signingInputBytes = Content.stringToBytesUTF8(signingInput);
294
+ const signatureBytes = Content.base64ToBytes(signatureB64Url);
295
+
296
+ return this.verifyBytes(signatureBytes, signingInputBytes, publicJWKey);
297
+ }
298
+
299
+ // --- Low-Level Primitives ---
300
+
301
+ encrypt(plaintext: string, cekBytes: Uint8Array, aad: string): Promise<ProtectedDataAES> {
302
+ return this.aesManager.encrypt(plaintext, cekBytes, aad);
303
+ }
304
+
305
+ decrypt(encryptedData: ProtectedDataAES, cekBytes: Uint8Array, aad: string): Promise<string> {
306
+ return this.aesManager.decrypt(encryptedData, cekBytes, aad);
307
+ }
308
+
309
+ async encapsulate(cekSeedBytes: Uint8Array, secretKeyBytes: Uint8Array, recipientPublicKeyBytes: Uint8Array): Promise<{ encapsulatedCekBytes: Uint8Array; derivedCekBytes: Uint8Array; }> {
310
+ // According to RFC 9278 (JWE with ML-KEM), a seed is used for the KEM encapsulation.
311
+ // The KEM then derives a shared secret from this seed. It is this *derived* shared secret
312
+ // that is used to encrypt the content, NOT the original seed.
313
+ // The `encapsulate` function from the noble library handles this correctly by accepting the
314
+ // seed as the second argument. It returns both the encapsulated key (`cipherText`)
315
+ // and the derived shared secret, which we must use as the actual AES key.
316
+ const mlKem = await this.loadMlKem();
317
+ const { sharedSecret, cipherText } = await mlKem.ml_kem768.encapsulate(recipientPublicKeyBytes, cekSeedBytes);
318
+ return { derivedCekBytes: sharedSecret, encapsulatedCekBytes: cipherText };
319
+ }
320
+
321
+ async decapsulate(encapsulatedBytes: Uint8Array, secretKeyBytes: Uint8Array): Promise<Uint8Array> {
322
+ const mlKem = await this.loadMlKem();
323
+ return mlKem.ml_kem768.decapsulate(encapsulatedBytes, secretKeyBytes);
324
+ }
325
+
326
+ async signBytes(payloadBytes: Uint8Array, secretKeyBytes: Uint8Array, alg: MldsaAlg): Promise<Uint8Array> {
327
+ const mlDsa = await this.loadMlDsa();
328
+ switch (alg) {
329
+ case 'ML-DSA-44': return mlDsa.ml_dsa44.sign(payloadBytes, secretKeyBytes);
330
+ case 'ML-DSA-65': return mlDsa.ml_dsa65.sign(payloadBytes, secretKeyBytes);
331
+ case 'ML-DSA-87': return mlDsa.ml_dsa87.sign(payloadBytes, secretKeyBytes);
332
+ default: throw new Error(`Unsupported ML-DSA algorithm: ${alg}`);
333
+ }
334
+ }
335
+
336
+ async verifyBytes(signatureBytes: Uint8Array, dataBytes: Uint8Array, publicKey: PublicJwk): Promise<boolean> {
337
+ const mlDsa = await this.loadMlDsa();
338
+ const publicKeyBytes = Content.base64ToBytes((publicKey as any).pub || (publicKey as any).x);
339
+ const alg = (publicKey as MldsaPublicJwk).alg;
340
+ if (!alg) throw new Error("Public key must contain 'alg' property for verification.");
341
+
342
+ switch (alg) {
343
+ case 'ML-DSA-44': return mlDsa.ml_dsa44.verify(signatureBytes, dataBytes, publicKeyBytes);
344
+ case 'ML-DSA-65': return mlDsa.ml_dsa65.verify(signatureBytes, dataBytes, publicKeyBytes);
345
+ case 'ML-DSA-87': return mlDsa.ml_dsa87.verify(signatureBytes, dataBytes, publicKeyBytes);
346
+ default: throw new Error(`Unsupported ML-DSA algorithm: ${alg}`);
347
+ }
348
+ }
349
+
350
+ // --- Formatting & Parsing Utilities ---
351
+
352
+ jwsToCompact(jws: DataCompactJWT): string {
353
+ return `${jws.protected}.${jws.payload}.${jws.signature}`;
354
+ }
355
+
356
+ parseCompactJws(jwsString: string): DataCompactJWT {
357
+ if (jwsString.trim().startsWith('{')) {
358
+ const parsed = JSON.parse(jwsString);
359
+ if (!parsed.payload || !parsed.signatures || !parsed.signatures[0]) {
360
+ throw new Error("Invalid JWS JSON format");
361
+ }
362
+ return {
363
+ payload: parsed.payload,
364
+ protected: parsed.signatures[0].protected,
365
+ signature: parsed.signatures[0].signature,
366
+ };
367
+ }
368
+ const parts = jwtUtils.getPartsJWT(jwsString);
369
+ if (!parts) throw new Error("Invalid Compact JWS format");
370
+
371
+ const result: DataCompactJWT = {
372
+ payload: Content.base64UrlSafeToJSON(parts.payload),
373
+ protected: Content.base64UrlSafeToJSON(parts.protected),
374
+ signature: Content.base64ToBytes(parts.signature),
375
+ };
376
+ return result;
377
+ }
378
+
379
+ parseCompactJwe(jweString: string): JweObject {
380
+ if (jweString.trim().startsWith('{')) {
381
+ return JSON.parse(jweString);
382
+ }
383
+ const parts = jweString.split('.');
384
+ if (parts.length !== 5) throw new Error("Invalid Compact JWE format");
385
+ const protectedHeader = Content.base64UrlSafeToJSON(parts[0]);
386
+ // Compact JWE has no per-recipient header, but our model requires one.
387
+ // The 'kid' should be in the main protected header for decryption to work.
388
+ return {
389
+ protected: parts[0],
390
+ recipients: [{
391
+ header: { alg: (protectedHeader as any).alg || '', kid: (protectedHeader as any).kid || '' },
392
+ encrypted_key: parts[1]
393
+ }],
394
+ iv: parts[2],
395
+ ciphertext: parts[3],
396
+ tag: parts[4],
397
+ };
398
+ }
399
+
400
+ // --- JWK Thumbprint Calculation (RFC 7638) ---
401
+
402
+ /**
403
+ * Computes a JWK thumbprint using a specified hash algorithm.
404
+ * This implementation is platform-agnostic by using the injected ICryptoHelper.
405
+ */
406
+ private async _computeJwkThumbprint(
407
+ jwk: PublicJwk,
408
+ hash: "SHA-256" | "SHA-384" = "SHA-256"
409
+ ): Promise<string> {
410
+ const baseJwk = this._toBaseJwk(jwk);
411
+ const canonical = this._canonicalizeForJwkThumbprint(baseJwk);
412
+ // Use the platform-agnostic digest method
413
+ const digestHex = await this.cryptoHelper.digestString(canonical, hash);
414
+ // The digestString returns a hex string, but thumbprints are Base64UrlSafe.
415
+ // We need to convert from hex to bytes, then bytes to Base64UrlSafe.
416
+ const digestBytes = this._hexToBytes(digestHex);
417
+ return Content.bytesToRawBase64UrlSafe(digestBytes);
418
+ }
419
+
420
+ /**
421
+ * Creates a canonical string from a simple, flat JSON object as required by
422
+ * RFC 7638 for JWK thumbprints.
423
+ */
424
+ private _canonicalizeForJwkThumbprint(obj: Record<string, unknown>): string {
425
+ const keys = Object.keys(obj).sort();
426
+ const parts = keys.map(k => `"${k}":${JSON.stringify(obj[k])}`);
427
+ return `{${parts.join(",")}}`;
428
+ }
429
+
430
+ /**
431
+ * Extracts the Base JWK for thumbprint calculation per RFC 7638.
432
+ * This handles both Post-Quantum (OKP, AKP) and legacy (EC) key types.
433
+ */
434
+ private _toBaseJwk(jwk: PublicJwk): BaseJwk {
435
+ if (jwk.kty === "OKP") {
436
+ const { crv, x } = jwk;
437
+ return { kty: "OKP", crv, x };
438
+ } else if (jwk.kty === "AKP") {
439
+ const { alg, pub } = jwk;
440
+ return { kty: "AKP", alg, pub };
441
+ } else if (jwk.kty === "EC") {
442
+ const { crv, x, y } = jwk;
443
+ const baseJwk: EcBaseJwk = { kty: "EC", crv, x, y };
444
+ return baseJwk;
445
+ } else {
446
+ const exhaustiveCheck: never = jwk;
447
+ throw new Error(`Unsupported key type for JWK thumbprint: ${(exhaustiveCheck as any).kty}`);
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Utility to convert a hex string to a Uint8Array.
453
+ */
454
+ private _hexToBytes(hex: string): Uint8Array {
455
+ const bytes = new Uint8Array(hex.length / 2);
456
+ for (let i = 0; i < hex.length; i += 2) {
457
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
458
+ }
459
+ return bytes;
460
+ }
461
+ }