@sd-jwt/core 0.20.1 → 0.21.0-alpha-20260916100413

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 (68) hide show
  1. package/LICENSE +2 -2
  2. package/README.md +82 -80
  3. package/dist/index.cjs +1526 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +696 -0
  6. package/dist/index.d.mts +537 -527
  7. package/dist/index.mjs +1422 -1998
  8. package/dist/index.mjs.map +1 -0
  9. package/package.json +29 -49
  10. package/CHANGELOG.md +0 -39
  11. package/dist/index.d.ts +0 -686
  12. package/dist/index.js +0 -2100
  13. package/src/decode/decode.ts +0 -407
  14. package/src/decode/index.ts +0 -1
  15. package/src/decoy.ts +0 -15
  16. package/src/flattenJSON.ts +0 -99
  17. package/src/generalJSON.ts +0 -157
  18. package/src/index.ts +0 -1000
  19. package/src/jwt.ts +0 -286
  20. package/src/kbjwt.ts +0 -101
  21. package/src/present/index.ts +0 -1
  22. package/src/present/present.ts +0 -216
  23. package/src/sdjwt.ts +0 -384
  24. package/src/test/decode/decode.spec.ts +0 -197
  25. package/src/test/decoy.spec.ts +0 -30
  26. package/src/test/flattenJSON.spec.ts +0 -122
  27. package/src/test/generalJSON.spec.ts +0 -198
  28. package/src/test/index.spec.ts +0 -986
  29. package/src/test/jwt.spec.ts +0 -301
  30. package/src/test/kbjwt.spec.ts +0 -555
  31. package/src/test/pass.spec.ts +0 -7
  32. package/src/test/present/present.spec.ts +0 -305
  33. package/src/test/sdjwt.spec.ts +0 -384
  34. package/src/test/types/type.spec.ts +0 -88
  35. package/src/test/utils/base64url.spec.ts +0 -33
  36. package/src/test/utils/disclosure.spec.ts +0 -170
  37. package/src/test/utils/error.spec.ts +0 -15
  38. package/src/types/index.ts +0 -2
  39. package/src/types/type.ts +0 -270
  40. package/src/types/verification-error.ts +0 -55
  41. package/src/utils/base64url.ts +0 -6
  42. package/src/utils/disclosure.ts +0 -121
  43. package/src/utils/error.ts +0 -25
  44. package/src/utils/index.ts +0 -3
  45. package/src/utils/strict-json.ts +0 -17
  46. package/test/app-e2e.spec.ts +0 -292
  47. package/test/array_data_types.json +0 -31
  48. package/test/array_full_sd.json +0 -21
  49. package/test/array_in_sd.json +0 -13
  50. package/test/array_nested_in_plain.json +0 -29
  51. package/test/array_none_disclosed.json +0 -17
  52. package/test/array_of_nulls.json +0 -15
  53. package/test/array_of_objects.json +0 -65
  54. package/test/array_of_scalars.json +0 -19
  55. package/test/array_recursive_sd.json +0 -35
  56. package/test/array_recursive_sd_some_disclosed.json +0 -63
  57. package/test/complex.json +0 -43
  58. package/test/header_mod.json +0 -48
  59. package/test/json_serialization.json +0 -48
  60. package/test/key_binding.json +0 -48
  61. package/test/no_sd.json +0 -36
  62. package/test/object_data_types.json +0 -62
  63. package/test/recursions.json +0 -117
  64. package/test/rfc9901-audit-fixes.spec.ts +0 -252
  65. package/test/rfc9901-validation.spec.ts +0 -150
  66. package/test/tsconfig.json +0 -4
  67. package/tsconfig.json +0 -7
  68. package/vitest.config.mts +0 -4
@@ -1,407 +0,0 @@
1
- import {
2
- DEFAULT_SECURE_HASH_ALGORITHMS,
3
- encodePathSegment,
4
- type HashAlgorithm,
5
- type Hasher,
6
- type HasherAndAlg,
7
- type HasherAndAlgSync,
8
- type HasherSync,
9
- IANA_HASH_ALGORITHMS,
10
- SD_DIGEST,
11
- SD_LIST_KEY,
12
- SD_SEPARATOR,
13
- } from '../types';
14
- import { Disclosure, SDJWTException } from '../utils';
15
- import { decodeBase64urlJsonStrict } from '../utils/strict-json';
16
-
17
- export const decodeJwt = <
18
- H extends Record<string, unknown>,
19
- T extends Record<string, unknown>,
20
- >(
21
- jwt: string,
22
- ): { header: H; payload: T; signature: string } => {
23
- const { 0: header, 1: payload, 2: signature, length } = jwt.split('.');
24
- if (length !== 3) {
25
- throw new SDJWTException('Invalid JWT as input');
26
- }
27
-
28
- return {
29
- header: decodeBase64urlJsonStrict(header, 'Invalid JWT as input'),
30
- payload: decodeBase64urlJsonStrict(payload, 'Invalid JWT as input'),
31
- signature: signature,
32
- };
33
- };
34
-
35
- // Split the sdjwt into 3 parts: jwt, disclosures and keybinding jwt. each part is base64url encoded
36
- // It's separated by the ~ character
37
- //
38
- // If there is no keybinding jwt, the third part will be undefined
39
- // If there are no disclosures, the second part will be an empty array
40
- export const splitSdJwt = (
41
- sdjwt: string,
42
- ): { jwt: string; disclosures: string[]; kbJwt?: string } => {
43
- const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
44
- if (encodedDisclosures.length === 0) {
45
- throw new SDJWTException('Invalid SD-JWT: missing SD-JWT separator');
46
- }
47
-
48
- const encodedKeyBindingJwt = encodedDisclosures.pop();
49
- return {
50
- jwt: encodedJwt,
51
- disclosures: encodedDisclosures,
52
- kbJwt: encodedKeyBindingJwt || undefined,
53
- };
54
- };
55
-
56
- // Decode the sdjwt into the jwt, disclosures and keybinding jwt
57
- // jwt, disclosures and keybinding jwt are also decoded
58
- export const decodeSdJwt = async (
59
- sdjwt: string,
60
- hasher: Hasher,
61
- ): Promise<DecodedSDJwt> => {
62
- const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
63
- const jwt = decodeJwt(encodedJwt);
64
-
65
- if (encodedDisclosures.length === 0) {
66
- throw new SDJWTException('Invalid SD-JWT: missing SD-JWT separator');
67
- }
68
-
69
- const encodedKeyBindingJwt = encodedDisclosures.pop();
70
- const kbJwt = encodedKeyBindingJwt
71
- ? decodeJwt(encodedKeyBindingJwt)
72
- : undefined;
73
-
74
- const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
75
-
76
- const disclosures = await Promise.all(
77
- encodedDisclosures.map((ed) =>
78
- Disclosure.fromEncode(ed, { alg: _sd_alg, hasher }),
79
- ),
80
- );
81
-
82
- return {
83
- jwt,
84
- disclosures,
85
- kbJwt,
86
- };
87
- };
88
-
89
- export const decodeSdJwtSync = (
90
- sdjwt: string,
91
- hasher: HasherSync,
92
- ): DecodedSDJwt => {
93
- const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
94
- const jwt = decodeJwt(encodedJwt);
95
-
96
- if (encodedDisclosures.length === 0) {
97
- throw new SDJWTException('Invalid SD-JWT: missing SD-JWT separator');
98
- }
99
-
100
- const encodedKeyBindingJwt = encodedDisclosures.pop();
101
- const kbJwt = encodedKeyBindingJwt
102
- ? decodeJwt(encodedKeyBindingJwt)
103
- : undefined;
104
-
105
- const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
106
-
107
- const disclosures = encodedDisclosures.map((ed) =>
108
- Disclosure.fromEncodeSync(ed, { alg: _sd_alg, hasher }),
109
- );
110
-
111
- return {
112
- jwt,
113
- disclosures,
114
- kbJwt,
115
- };
116
- };
117
-
118
- // Get the claims from jwt and disclosures
119
- // The digested values are matched with the disclosures and the claims are extracted
120
- export const getClaims = async <T = Record<string, unknown>>(
121
- rawPayload: Record<string, unknown>,
122
- disclosures: Array<Disclosure>,
123
- hasher: Hasher,
124
- ): Promise<T> => {
125
- const { unpackedObj } = await unpack(rawPayload, disclosures, hasher);
126
- // The caller supplies T to match their expected shape
127
- return unpackedObj as T;
128
- };
129
-
130
- export const getClaimsSync = <T = Record<string, unknown>>(
131
- rawPayload: Record<string, unknown>,
132
- disclosures: Array<Disclosure>,
133
- hasher: HasherSync,
134
- ): T => {
135
- const { unpackedObj } = unpackSync(rawPayload, disclosures, hasher);
136
- // The caller supplies T to match their expected shape
137
- return unpackedObj as T;
138
- };
139
-
140
- const isRecord = (v: unknown): v is Record<string, unknown> =>
141
- typeof v === 'object' && v !== null && !Array.isArray(v);
142
-
143
- const unpackArray = (
144
- arr: Array<unknown>,
145
- map: Record<string, Disclosure>,
146
- prefix = '',
147
- seenDigests?: Set<string>,
148
- ): { unpackedObj: unknown; disclosureKeymap: Record<string, string> } => {
149
- const keys: Record<string, string> = {};
150
- const unpackedArray: unknown[] = [];
151
- arr.forEach((item, idx) => {
152
- if (isRecord(item)) {
153
- const hash = item[SD_LIST_KEY];
154
- if (SD_LIST_KEY in item) {
155
- if (Object.keys(item).length !== 1 || typeof hash !== 'string') {
156
- throw new SDJWTException('Invalid array disclosure placeholder');
157
- }
158
- // RFC 9901 Section 7.1 step 4: reject duplicate digests
159
- if (seenDigests) {
160
- if (seenDigests.has(hash)) {
161
- throw new SDJWTException(
162
- 'Duplicate digest found in SD-JWT payload',
163
- );
164
- }
165
- seenDigests.add(hash);
166
- }
167
- const disclosed = map[hash];
168
- if (disclosed) {
169
- if (typeof disclosed.key === 'string') {
170
- throw new SDJWTException(
171
- 'Object-property disclosure cannot be used as an array element',
172
- );
173
- }
174
- const presentKey = prefix ? `${prefix}.${idx}` : `${idx}`;
175
- keys[presentKey] = hash;
176
-
177
- const { unpackedObj, disclosureKeymap: disclosureKeys } =
178
- unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
179
- unpackedArray.push(unpackedObj);
180
- Object.assign(keys, disclosureKeys);
181
- }
182
- } else {
183
- const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
184
- const { unpackedObj, disclosureKeymap: disclosureKeys } =
185
- unpackObjInternal(item, map, newKey, seenDigests);
186
- unpackedArray.push(unpackedObj);
187
- Object.assign(keys, disclosureKeys);
188
- }
189
- } else if (Array.isArray(item)) {
190
- const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
191
- const { unpackedObj, disclosureKeymap: disclosureKeys } =
192
- unpackObjInternal(item, map, newKey, seenDigests);
193
- unpackedArray.push(unpackedObj);
194
- Object.assign(keys, disclosureKeys);
195
- } else {
196
- unpackedArray.push(item);
197
- }
198
- });
199
- return { unpackedObj: unpackedArray, disclosureKeymap: keys };
200
- };
201
-
202
- export const unpackObj = (obj: unknown, map: Record<string, Disclosure>) => {
203
- const copiedObj = JSON.parse(JSON.stringify(obj));
204
- const seenDigests = new Set<string>();
205
- const result = unpackObjInternal(copiedObj, map, '', seenDigests);
206
-
207
- // RFC 9901 Section 7.1 step 5: reject unreferenced disclosures
208
- const mapDigests = Object.keys(map);
209
- const unusedDigests = mapDigests.filter((d) => !seenDigests.has(d));
210
- if (unusedDigests.length > 0) {
211
- throw new SDJWTException('Unreferenced disclosure(s) detected in SD-JWT');
212
- }
213
-
214
- return result;
215
- };
216
-
217
- const unpackObjInternal = (
218
- obj: unknown,
219
- map: Record<string, Disclosure>,
220
- prefix = '',
221
- seenDigests?: Set<string>,
222
- ): { unpackedObj: unknown; disclosureKeymap: Record<string, string> } => {
223
- const keys: Record<string, string> = {};
224
- if (typeof obj === 'object' && obj !== null) {
225
- if (Array.isArray(obj)) {
226
- return unpackArray(obj, map, prefix, seenDigests);
227
- }
228
-
229
- const record = obj as Record<string, unknown>;
230
- for (const key in record) {
231
- if (prefix && key === '_sd_alg') {
232
- throw new SDJWTException('Nested _sd_alg is not allowed');
233
- }
234
- if (
235
- key !== SD_DIGEST &&
236
- key !== SD_LIST_KEY &&
237
- typeof record[key] === 'object'
238
- ) {
239
- const escapedKey = encodePathSegment(key);
240
- const newKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
241
- const { unpackedObj, disclosureKeymap: disclosureKeys } =
242
- unpackObjInternal(record[key], map, newKey, seenDigests);
243
- record[key] = unpackedObj;
244
- Object.assign(keys, disclosureKeys);
245
- }
246
- }
247
-
248
- const { _sd, ...payload } = record as Record<string, unknown> & {
249
- _sd?: Array<string>;
250
- };
251
- const claims: Record<string, unknown> = {};
252
- if (_sd !== undefined) {
253
- if (
254
- !Array.isArray(_sd) ||
255
- !_sd.every((hash) => typeof hash === 'string')
256
- ) {
257
- throw new SDJWTException(
258
- 'Invalid _sd claim: expected array of strings',
259
- );
260
- }
261
- for (const hash of _sd) {
262
- // RFC 9901 Section 7.1 step 4: reject duplicate digests
263
- if (seenDigests) {
264
- if (seenDigests.has(hash)) {
265
- throw new SDJWTException(
266
- 'Duplicate digest found in SD-JWT payload',
267
- );
268
- }
269
- seenDigests.add(hash);
270
- }
271
- const disclosed = map[hash];
272
- if (disclosed) {
273
- if (typeof disclosed.key !== 'string') {
274
- throw new SDJWTException(
275
- 'Array disclosure cannot be used as an object property',
276
- );
277
- }
278
- // RFC 9901 Section 7.1 step 3c.ii.3: reject if claim name already exists
279
- if (disclosed.key in payload) {
280
- throw new SDJWTException(
281
- `Disclosed claim name "${disclosed.key}" conflicts with existing payload key`,
282
- );
283
- }
284
- if (disclosed.key in claims) {
285
- throw new SDJWTException(
286
- `Disclosed claim name "${disclosed.key}" conflicts with another disclosure`,
287
- );
288
- }
289
-
290
- const escapedKey = encodePathSegment(disclosed.key);
291
- const presentKey = prefix ? `${prefix}.${escapedKey}` : escapedKey;
292
- keys[presentKey] = hash;
293
-
294
- const { unpackedObj, disclosureKeymap: disclosureKeys } =
295
- unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
296
- claims[disclosed.key] = unpackedObj;
297
- Object.assign(keys, disclosureKeys);
298
- }
299
- }
300
- }
301
-
302
- const unpackedObj = Object.assign(payload, claims);
303
- return { unpackedObj, disclosureKeymap: keys };
304
- }
305
- return { unpackedObj: obj, disclosureKeymap: keys };
306
- };
307
-
308
- // Creates a mapping of the digests of the disclosures to the actual disclosures
309
- export const createHashMapping = async (
310
- disclosures: Array<Disclosure>,
311
- hash: HasherAndAlg,
312
- ) => {
313
- const map: Record<string, Disclosure> = {};
314
- for (let i = 0; i < disclosures.length; i++) {
315
- const disclosure = disclosures[i];
316
- const digest = await disclosure.digest(hash);
317
- if (digest in map) {
318
- throw new SDJWTException('Duplicate disclosure digest detected');
319
- }
320
- map[digest] = disclosure;
321
- }
322
- return map;
323
- };
324
-
325
- export const createHashMappingSync = (
326
- disclosures: Array<Disclosure>,
327
- hash: HasherAndAlgSync,
328
- ) => {
329
- const map: Record<string, Disclosure> = {};
330
- for (let i = 0; i < disclosures.length; i++) {
331
- const disclosure = disclosures[i];
332
- const digest = disclosure.digestSync(hash);
333
- if (digest in map) {
334
- throw new SDJWTException('Duplicate disclosure digest detected');
335
- }
336
- map[digest] = disclosure;
337
- }
338
- return map;
339
- };
340
-
341
- // Extract _sd_alg. If it is not present, it is assumed to be sha-256
342
- export const getSDAlgAndPayload = (
343
- SdJwtPayload: Record<string, unknown>,
344
- allowedAlgorithms: ReadonlyArray<HashAlgorithm> = DEFAULT_SECURE_HASH_ALGORITHMS,
345
- ) => {
346
- const { _sd_alg, ...payload } = SdJwtPayload;
347
- if (_sd_alg === undefined) {
348
- return { _sd_alg: 'sha-256', payload };
349
- }
350
- if (typeof _sd_alg !== 'string') {
351
- throw new SDJWTException('Invalid _sd_alg: expected string');
352
- }
353
- if (
354
- !IANA_HASH_ALGORITHMS.includes(
355
- _sd_alg as (typeof IANA_HASH_ALGORITHMS)[number],
356
- )
357
- ) {
358
- throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
359
- }
360
- if (!allowedAlgorithms.includes(_sd_alg as HashAlgorithm)) {
361
- throw new SDJWTException(`Disallowed _sd_alg: ${_sd_alg}`);
362
- }
363
- return { _sd_alg, payload };
364
- };
365
-
366
- // Match the digests of the disclosures with the claims and extract the claims
367
- // unpack function use unpackObjInternal and unpackArray to recursively unpack the claims
368
- // Since getSDAlgAndPayload create new object So we don't need to clone it again
369
- export const unpack = async (
370
- SdJwtPayload: Record<string, unknown>,
371
- disclosures: Array<Disclosure>,
372
- hasher: Hasher,
373
- ) => {
374
- const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
375
- const hash = { hasher, alg: _sd_alg };
376
- const map = await createHashMapping(disclosures, hash);
377
-
378
- return unpackObj(payload, map);
379
- };
380
-
381
- export const unpackSync = (
382
- SdJwtPayload: Record<string, unknown>,
383
- disclosures: Array<Disclosure>,
384
- hasher: HasherSync,
385
- ) => {
386
- const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
387
- const hash = { hasher, alg: _sd_alg };
388
- const map = createHashMappingSync(disclosures, hash);
389
-
390
- return unpackObj(payload, map);
391
- };
392
-
393
- // This is the type of the object that is returned by the decodeSdJwt function
394
- // It is a combination of the decoded jwt, the disclosures and the keybinding jwt
395
- export type DecodedSDJwt = {
396
- jwt: {
397
- header: Record<string, unknown>;
398
- payload: Record<string, unknown>; // raw payload of sd-jwt
399
- signature: string;
400
- };
401
- disclosures: Array<Disclosure>;
402
- kbJwt?: {
403
- header: Record<string, unknown>;
404
- payload: Record<string, unknown>;
405
- signature: string;
406
- };
407
- };
@@ -1 +0,0 @@
1
- export * from './decode';
package/src/decoy.ts DELETED
@@ -1,15 +0,0 @@
1
- import type { HasherAndAlg, SaltGenerator } from './types';
2
- import { uint8ArrayToBase64Url } from './utils';
3
-
4
- // This function creates a decoy value that can be used to obscure SD JWT payload.
5
- // The value is basically a hash of a random salt. So the value is not predictable.
6
- // return value is a base64url encoded string.
7
- export const createDecoy = async (
8
- hash: HasherAndAlg,
9
- saltGenerator: SaltGenerator,
10
- ): Promise<string> => {
11
- const { hasher, alg } = hash;
12
- const salt = await saltGenerator(16);
13
- const decoy = await hasher(salt, alg);
14
- return uint8ArrayToBase64Url(decoy);
15
- };
@@ -1,99 +0,0 @@
1
- import { splitSdJwt } from './decode';
2
- import { SD_SEPARATOR } from './types';
3
- import { SDJWTException } from './utils';
4
-
5
- export type FlattenJSONData = {
6
- jwtData: {
7
- protected: string;
8
- payload: string;
9
- signature: string;
10
- };
11
- disclosures: Array<string>;
12
- kb_jwt?: string;
13
- };
14
-
15
- export type FlattenJSONSerialized = {
16
- payload: string;
17
- signature: string;
18
- protected: string;
19
- header: {
20
- disclosures: Array<string>;
21
- kb_jwt?: string;
22
- };
23
- };
24
-
25
- export class FlattenJSON {
26
- public disclosures: Array<string>;
27
- public kb_jwt?: string;
28
-
29
- public payload: string;
30
- public signature: string;
31
- public protected: string;
32
-
33
- constructor(data: FlattenJSONData) {
34
- this.disclosures = data.disclosures;
35
- this.kb_jwt = data.kb_jwt;
36
- this.payload = data.jwtData.payload;
37
- this.signature = data.jwtData.signature;
38
- this.protected = data.jwtData.protected;
39
- }
40
-
41
- public static fromEncode(encodedSdJwt: string) {
42
- const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);
43
-
44
- const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split('.');
45
- if (!protectedHeader || !payload || !signature) {
46
- throw new SDJWTException('Invalid JWT');
47
- }
48
-
49
- return new FlattenJSON({
50
- jwtData: {
51
- protected: protectedHeader,
52
- payload,
53
- signature,
54
- },
55
- disclosures,
56
- kb_jwt: kbJwt,
57
- });
58
- }
59
-
60
- public static fromSerialized(json: FlattenJSONSerialized) {
61
- return new FlattenJSON({
62
- jwtData: {
63
- protected: json.protected,
64
- payload: json.payload,
65
- signature: json.signature,
66
- },
67
- disclosures: json.header.disclosures,
68
- kb_jwt: json.header.kb_jwt,
69
- });
70
- }
71
-
72
- public toJson(): FlattenJSONSerialized {
73
- return {
74
- payload: this.payload,
75
- signature: this.signature,
76
- protected: this.protected,
77
- header: {
78
- disclosures: this.disclosures,
79
- kb_jwt: this.kb_jwt,
80
- },
81
- };
82
- }
83
-
84
- public toEncoded() {
85
- const data: string[] = [];
86
-
87
- const jwt = `${this.protected}.${this.payload}.${this.signature}`;
88
- data.push(jwt);
89
-
90
- if (this.disclosures && this.disclosures.length > 0) {
91
- const disclosures = this.disclosures.join(SD_SEPARATOR);
92
- data.push(disclosures);
93
- }
94
-
95
- const kb_jwt = this.kb_jwt ?? '';
96
- data.push(kb_jwt);
97
- return data.join(SD_SEPARATOR);
98
- }
99
- }
@@ -1,157 +0,0 @@
1
- import { splitSdJwt } from './decode';
2
- import { SD_SEPARATOR, type Signer } from './types';
3
- import { base64urlEncode, SDJWTException } from './utils';
4
-
5
- export type GeneralJSONData = {
6
- payload: string;
7
- disclosures: Array<string>;
8
- kb_jwt?: string;
9
- signatures: Array<{
10
- protected: string;
11
- signature: string;
12
- kid?: string;
13
- }>;
14
- };
15
-
16
- export type GeneralJSONSerialized = {
17
- payload: string;
18
- signatures: Array<{
19
- header: {
20
- disclosures?: Array<string>;
21
- kid?: string;
22
- kb_jwt?: string;
23
- [key: string]: unknown;
24
- };
25
- protected: string;
26
- signature: string;
27
- }>;
28
- };
29
-
30
- export class GeneralJSON {
31
- public payload: string;
32
- public disclosures: Array<string>;
33
- public kb_jwt?: string;
34
- public signatures: Array<{
35
- protected: string;
36
- signature: string;
37
- kid?: string;
38
- }>;
39
-
40
- constructor(data: GeneralJSONData) {
41
- this.payload = data.payload;
42
- this.disclosures = data.disclosures;
43
- this.kb_jwt = data.kb_jwt;
44
- this.signatures = data.signatures;
45
- }
46
-
47
- public static fromEncode(encodedSdJwt: string) {
48
- const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);
49
-
50
- const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split('.');
51
- if (!protectedHeader || !payload || !signature) {
52
- throw new SDJWTException('Invalid JWT');
53
- }
54
-
55
- return new GeneralJSON({
56
- payload,
57
- disclosures,
58
- kb_jwt: kbJwt,
59
- signatures: [
60
- {
61
- protected: protectedHeader,
62
- signature,
63
- },
64
- ],
65
- });
66
- }
67
-
68
- public static fromSerialized(json: GeneralJSONSerialized) {
69
- if (!json.signatures[0]) {
70
- throw new SDJWTException('Invalid JSON');
71
- }
72
- for (let index = 1; index < json.signatures.length; index++) {
73
- const header = json.signatures[index].header;
74
- if (header && ('disclosures' in header || 'kb_jwt' in header)) {
75
- throw new SDJWTException(
76
- 'disclosures and kb_jwt MUST only appear in the first unprotected header',
77
- );
78
- }
79
- }
80
- const disclosures = json.signatures[0].header?.disclosures ?? [];
81
- const kb_jwt = json.signatures[0].header?.kb_jwt;
82
- return new GeneralJSON({
83
- payload: json.payload,
84
- disclosures,
85
- kb_jwt,
86
- signatures: json.signatures.map((s) => {
87
- return {
88
- protected: s.protected,
89
- signature: s.signature,
90
- kid: s.header?.kid,
91
- };
92
- }),
93
- });
94
- }
95
-
96
- public toJson() {
97
- return {
98
- payload: this.payload,
99
- signatures: this.signatures.map((s, i) => {
100
- if (i !== 0) {
101
- // If present, disclosures and kb_jwt, MUST be included in the first unprotected header and
102
- // MUST NOT be present in any following unprotected headers.
103
- return {
104
- header: {
105
- kid: s.kid,
106
- },
107
- protected: s.protected,
108
- signature: s.signature,
109
- };
110
- }
111
- return {
112
- header: {
113
- disclosures: this.disclosures,
114
- kid: s.kid,
115
- kb_jwt: this.kb_jwt,
116
- },
117
- protected: s.protected,
118
- signature: s.signature,
119
- };
120
- }),
121
- };
122
- }
123
-
124
- public toEncoded(index: number) {
125
- if (index < 0 || index >= this.signatures.length) {
126
- throw new SDJWTException('Index out of bounds');
127
- }
128
- const data: string[] = [];
129
-
130
- const { protected: protectedHeader, signature } = this.signatures[index];
131
- const jwt = `${protectedHeader}.${this.payload}.${signature}`;
132
- data.push(jwt);
133
-
134
- if (this.disclosures && this.disclosures.length > 0) {
135
- const disclosures = this.disclosures.join(SD_SEPARATOR);
136
- data.push(disclosures);
137
- }
138
-
139
- const kb = this.kb_jwt ?? '';
140
- data.push(kb);
141
- return data.join(SD_SEPARATOR);
142
- }
143
-
144
- public async addSignature(
145
- protectedHeader: Record<string, unknown>,
146
- signer: Signer,
147
- kid?: string,
148
- ) {
149
- const header = base64urlEncode(JSON.stringify(protectedHeader));
150
- const signature = await signer(`${header}.${this.payload}`);
151
- this.signatures.push({
152
- protected: header,
153
- signature,
154
- kid,
155
- });
156
- }
157
- }