@sd-jwt/core 0.3.0 → 0.3.2-next.101

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 (107) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +27 -82
  3. package/dist/index.d.mts +118 -0
  4. package/dist/index.d.ts +118 -0
  5. package/dist/index.js +675 -0
  6. package/dist/index.mjs +654 -0
  7. package/package.json +62 -48
  8. package/src/decoy.ts +15 -0
  9. package/src/index.ts +313 -0
  10. package/src/jwt.ts +107 -0
  11. package/src/kbjwt.ts +61 -0
  12. package/src/sdjwt.ts +337 -0
  13. package/src/test/decoy.spec.ts +30 -0
  14. package/src/test/index.spec.ts +528 -0
  15. package/src/test/jwt.spec.ts +141 -0
  16. package/src/test/kbjwt.spec.ts +341 -0
  17. package/src/test/pass.spec.ts +6 -0
  18. package/src/test/sdjwt.spec.ts +382 -0
  19. package/test/app-e2e.spec.ts +248 -0
  20. package/test/array_data_types.json +29 -0
  21. package/test/array_full_sd.json +21 -0
  22. package/test/array_in_sd.json +13 -0
  23. package/test/array_nested_in_plain.json +20 -0
  24. package/test/array_none_disclosed.json +17 -0
  25. package/test/array_of_nulls.json +15 -0
  26. package/test/array_of_objects.json +58 -0
  27. package/test/array_of_scalars.json +15 -0
  28. package/test/array_recursive_sd.json +35 -0
  29. package/test/array_recursive_sd_some_disclosed.json +55 -0
  30. package/test/complex.json +43 -0
  31. package/test/header_mod.json +44 -0
  32. package/test/json_serialization.json +44 -0
  33. package/test/key_binding.json +44 -0
  34. package/test/no_sd.json +36 -0
  35. package/test/object_data_types.json +60 -0
  36. package/test/recursions.json +98 -0
  37. package/tsconfig.json +7 -0
  38. package/vitest.config.mts +4 -0
  39. package/build/index.d.ts +0 -13
  40. package/build/index.js +0 -20
  41. package/build/index.js.map +0 -1
  42. package/build/jwt/error.d.ts +0 -2
  43. package/build/jwt/error.js +0 -7
  44. package/build/jwt/error.js.map +0 -1
  45. package/build/jwt/index.d.ts +0 -2
  46. package/build/jwt/index.js +0 -19
  47. package/build/jwt/index.js.map +0 -1
  48. package/build/jwt/jwt.d.ts +0 -208
  49. package/build/jwt/jwt.js +0 -325
  50. package/build/jwt/jwt.js.map +0 -1
  51. package/build/keyBinding/index.d.ts +0 -1
  52. package/build/keyBinding/index.js +0 -18
  53. package/build/keyBinding/index.js.map +0 -1
  54. package/build/keyBinding/keyBinding.d.ts +0 -64
  55. package/build/keyBinding/keyBinding.js +0 -119
  56. package/build/keyBinding/keyBinding.js.map +0 -1
  57. package/build/sdJwt/decoys.d.ts +0 -3
  58. package/build/sdJwt/decoys.js +0 -35
  59. package/build/sdJwt/decoys.js.map +0 -1
  60. package/build/sdJwt/disclosureFrame.d.ts +0 -8
  61. package/build/sdJwt/disclosureFrame.js +0 -87
  62. package/build/sdJwt/disclosureFrame.js.map +0 -1
  63. package/build/sdJwt/disclosures.d.ts +0 -33
  64. package/build/sdJwt/disclosures.js +0 -114
  65. package/build/sdJwt/disclosures.js.map +0 -1
  66. package/build/sdJwt/error.d.ts +0 -2
  67. package/build/sdJwt/error.js +0 -7
  68. package/build/sdJwt/error.js.map +0 -1
  69. package/build/sdJwt/index.d.ts +0 -6
  70. package/build/sdJwt/index.js +0 -23
  71. package/build/sdJwt/index.js.map +0 -1
  72. package/build/sdJwt/sdJwt.d.ts +0 -206
  73. package/build/sdJwt/sdJwt.js +0 -442
  74. package/build/sdJwt/sdJwt.js.map +0 -1
  75. package/build/sdJwt/types.d.ts +0 -5
  76. package/build/sdJwt/types.js +0 -3
  77. package/build/sdJwt/types.js.map +0 -1
  78. package/build/sdJwtVc/error.d.ts +0 -2
  79. package/build/sdJwtVc/error.js +0 -7
  80. package/build/sdJwtVc/error.js.map +0 -1
  81. package/build/sdJwtVc/index.d.ts +0 -2
  82. package/build/sdJwtVc/index.js +0 -19
  83. package/build/sdJwtVc/index.js.map +0 -1
  84. package/build/sdJwtVc/sdJwtVc.d.ts +0 -47
  85. package/build/sdJwtVc/sdJwtVc.js +0 -149
  86. package/build/sdJwtVc/sdJwtVc.js.map +0 -1
  87. package/build/signatureAndEncryptionAlgorithm.d.ts +0 -105
  88. package/build/signatureAndEncryptionAlgorithm.js +0 -110
  89. package/build/signatureAndEncryptionAlgorithm.js.map +0 -1
  90. package/build/types/disclosure.d.ts +0 -5
  91. package/build/types/disclosure.js +0 -3
  92. package/build/types/disclosure.js.map +0 -1
  93. package/build/types/index.d.ts +0 -5
  94. package/build/types/index.js +0 -22
  95. package/build/types/index.js.map +0 -1
  96. package/build/types/saltGenerator.d.ts +0 -17
  97. package/build/types/saltGenerator.js +0 -3
  98. package/build/types/saltGenerator.js.map +0 -1
  99. package/build/types/signer.d.ts +0 -2
  100. package/build/types/signer.js +0 -3
  101. package/build/types/signer.js.map +0 -1
  102. package/build/types/utils.d.ts +0 -2
  103. package/build/types/utils.js +0 -3
  104. package/build/types/utils.js.map +0 -1
  105. package/build/types/verifier.d.ts +0 -14
  106. package/build/types/verifier.js +0 -3
  107. package/build/types/verifier.js.map +0 -1
package/src/sdjwt.ts ADDED
@@ -0,0 +1,337 @@
1
+ import { createDecoy } from './decoy';
2
+ import { SDJWTException, Disclosure } from '@sd-jwt/utils';
3
+ import { Jwt } from './jwt';
4
+ import { KBJwt } from './kbjwt';
5
+ import {
6
+ DisclosureFrame,
7
+ Hasher,
8
+ HasherAndAlg,
9
+ KBOptions,
10
+ KB_JWT_TYP,
11
+ SDJWTCompact,
12
+ SD_DECOY,
13
+ SD_DIGEST,
14
+ SD_LIST_KEY,
15
+ SD_SEPARATOR,
16
+ SaltGenerator,
17
+ Signer,
18
+ kbHeader,
19
+ kbPayload,
20
+ } from '@sd-jwt/types';
21
+ import { createHashMapping, getSDAlgAndPayload, unpack } from '@sd-jwt/decode';
22
+
23
+ export type SDJwtData<
24
+ Header extends Record<string, unknown>,
25
+ Payload extends Record<string, unknown>,
26
+ KBHeader extends kbHeader = kbHeader,
27
+ KBPayload extends kbPayload = kbPayload,
28
+ > = {
29
+ jwt?: Jwt<Header, Payload>;
30
+ disclosures?: Array<Disclosure>;
31
+ kbJwt?: KBJwt<KBHeader, KBPayload>;
32
+ };
33
+
34
+ export class SDJwt<
35
+ Header extends Record<string, unknown> = Record<string, unknown>,
36
+ Payload extends Record<string, unknown> = Record<string, unknown>,
37
+ KBHeader extends kbHeader = kbHeader,
38
+ KBPayload extends kbPayload = kbPayload,
39
+ > {
40
+ public jwt?: Jwt<Header, Payload>;
41
+ public disclosures?: Array<Disclosure>;
42
+ public kbJwt?: KBJwt<KBHeader, KBPayload>;
43
+
44
+ constructor(data?: SDJwtData<Header, Payload, KBHeader, KBPayload>) {
45
+ this.jwt = data?.jwt;
46
+ this.disclosures = data?.disclosures;
47
+ this.kbJwt = data?.kbJwt;
48
+ }
49
+
50
+ public static async decodeSDJwt<
51
+ Header extends Record<string, unknown> = Record<string, unknown>,
52
+ Payload extends Record<string, unknown> = Record<string, unknown>,
53
+ KBHeader extends kbHeader = kbHeader,
54
+ KBPayload extends kbPayload = kbPayload,
55
+ >(
56
+ sdjwt: SDJWTCompact,
57
+ hasher: Hasher,
58
+ ): Promise<{
59
+ jwt: Jwt<Header, Payload>;
60
+ disclosures: Array<Disclosure>;
61
+ kbJwt?: KBJwt<KBHeader, KBPayload>;
62
+ }> {
63
+ const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
64
+ const jwt = Jwt.fromEncode<Header, Payload>(encodedJwt);
65
+
66
+ if (!jwt.payload) {
67
+ throw new Error('Payload is undefined on the JWT. Invalid state reached');
68
+ }
69
+
70
+ if (encodedDisclosures.length === 0) {
71
+ return {
72
+ jwt,
73
+ disclosures: [],
74
+ };
75
+ }
76
+
77
+ const encodedKeyBindingJwt = encodedDisclosures.pop();
78
+ const kbJwt = encodedKeyBindingJwt
79
+ ? KBJwt.fromKBEncode<KBHeader, KBPayload>(encodedKeyBindingJwt)
80
+ : undefined;
81
+
82
+ const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
83
+
84
+ const disclosures = await Promise.all(
85
+ (encodedDisclosures as Array<string>).map((ed) =>
86
+ Disclosure.fromEncode(ed, { alg: _sd_alg, hasher }),
87
+ ),
88
+ );
89
+
90
+ return {
91
+ jwt,
92
+ disclosures,
93
+ kbJwt,
94
+ };
95
+ }
96
+
97
+ public static async fromEncode<
98
+ Header extends Record<string, unknown> = Record<string, unknown>,
99
+ Payload extends Record<string, unknown> = Record<string, unknown>,
100
+ KBHeader extends kbHeader = kbHeader,
101
+ KBPayload extends kbPayload = kbPayload,
102
+ >(
103
+ encodedSdJwt: SDJWTCompact,
104
+ hasher: Hasher,
105
+ ): Promise<SDJwt<Header, Payload>> {
106
+ const { jwt, disclosures, kbJwt } = await SDJwt.decodeSDJwt<
107
+ Header,
108
+ Payload,
109
+ KBHeader,
110
+ KBPayload
111
+ >(encodedSdJwt, hasher);
112
+
113
+ return new SDJwt<Header, Payload, KBHeader, KBPayload>({
114
+ jwt,
115
+ disclosures,
116
+ kbJwt,
117
+ });
118
+ }
119
+
120
+ public async present(keys: string[], hasher: Hasher): Promise<SDJWTCompact> {
121
+ if (!this.jwt?.payload || !this.disclosures) {
122
+ throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
123
+ }
124
+ const { _sd_alg: alg } = getSDAlgAndPayload(this.jwt.payload);
125
+ const hash = { alg, hasher };
126
+ const hashmap = await createHashMapping(this.disclosures, hash);
127
+ const { disclosureKeymap } = await unpack(
128
+ this.jwt.payload,
129
+ this.disclosures,
130
+ hasher,
131
+ );
132
+
133
+ const presentableKeys = Object.keys(disclosureKeymap);
134
+ const missingKeys = keys.filter((k) => !presentableKeys.includes(k));
135
+ if (missingKeys.length > 0) {
136
+ throw new SDJWTException(
137
+ `Invalid sd-jwt: invalid present keys: ${missingKeys.join(', ')}`,
138
+ );
139
+ }
140
+
141
+ const disclosures = keys.map((k) => hashmap[disclosureKeymap[k]]);
142
+ const presentSDJwt = new SDJwt({
143
+ jwt: this.jwt,
144
+ disclosures,
145
+ kbJwt: this.kbJwt,
146
+ });
147
+ return presentSDJwt.encodeSDJwt();
148
+ }
149
+
150
+ public encodeSDJwt(): SDJWTCompact {
151
+ const data: string[] = [];
152
+
153
+ if (!this.jwt) {
154
+ throw new SDJWTException('Invalid sd-jwt: jwt is missing');
155
+ }
156
+
157
+ const encodedJwt = this.jwt.encodeJwt();
158
+ data.push(encodedJwt);
159
+
160
+ if (this.disclosures && this.disclosures.length > 0) {
161
+ const encodeddisclosures = this.disclosures
162
+ .map((dc) => dc.encode())
163
+ .join(SD_SEPARATOR);
164
+ data.push(encodeddisclosures);
165
+ }
166
+
167
+ data.push(this.kbJwt ? this.kbJwt.encodeJwt() : '');
168
+ return data.join(SD_SEPARATOR);
169
+ }
170
+
171
+ public async keys(hasher: Hasher): Promise<string[]> {
172
+ return listKeys(await this.getClaims(hasher)).sort();
173
+ }
174
+
175
+ public async presentableKeys(hasher: Hasher): Promise<string[]> {
176
+ if (!this.jwt?.payload || !this.disclosures) {
177
+ throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
178
+ }
179
+ const { disclosureKeymap } = await unpack(
180
+ this.jwt?.payload,
181
+ this.disclosures,
182
+ hasher,
183
+ );
184
+ return Object.keys(disclosureKeymap).sort();
185
+ }
186
+
187
+ public async getClaims<T>(hasher: Hasher): Promise<T> {
188
+ if (!this.jwt?.payload || !this.disclosures) {
189
+ throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
190
+ }
191
+ const { unpackedObj } = await unpack(
192
+ this.jwt.payload,
193
+ this.disclosures,
194
+ hasher,
195
+ );
196
+ return unpackedObj as T;
197
+ }
198
+ }
199
+
200
+ export const listKeys = (obj: Record<string, unknown>, prefix = '') => {
201
+ const keys: string[] = [];
202
+ for (const key in obj) {
203
+ if (obj[key] === undefined) continue;
204
+ const newKey = prefix ? `${prefix}.${key}` : key;
205
+ keys.push(newKey);
206
+
207
+ if (obj[key] && typeof obj[key] === 'object' && obj[key] !== null) {
208
+ keys.push(...listKeys(obj[key] as Record<string, unknown>, newKey));
209
+ }
210
+ }
211
+ return keys;
212
+ };
213
+
214
+ export const pack = async <T extends Record<string, unknown>>(
215
+ claims: T,
216
+ disclosureFrame: DisclosureFrame<T> | undefined,
217
+ hash: HasherAndAlg,
218
+ saltGenerator: SaltGenerator,
219
+ ): Promise<{
220
+ packedClaims: Record<string, unknown> | Array<Record<string, unknown>>;
221
+ disclosures: Array<Disclosure>;
222
+ }> => {
223
+ if (!disclosureFrame) {
224
+ return {
225
+ packedClaims: claims,
226
+ disclosures: [],
227
+ };
228
+ }
229
+
230
+ const sd = disclosureFrame[SD_DIGEST] ?? [];
231
+ const decoyCount = disclosureFrame[SD_DECOY] ?? 0;
232
+
233
+ if (Array.isArray(claims)) {
234
+ const packedClaims: Array<Record<typeof SD_LIST_KEY, string>> = [];
235
+ const disclosures: Array<Disclosure> = [];
236
+ const recursivePackedClaims: Record<number, unknown> = {};
237
+
238
+ for (const key in disclosureFrame) {
239
+ if (key !== SD_DIGEST) {
240
+ const idx = parseInt(key);
241
+ const packed = await pack(
242
+ claims[idx],
243
+ disclosureFrame[idx],
244
+ hash,
245
+ saltGenerator,
246
+ );
247
+ recursivePackedClaims[idx] = packed.packedClaims;
248
+ disclosures.push(...packed.disclosures);
249
+ }
250
+ }
251
+
252
+ for (let i = 0; i < claims.length; i++) {
253
+ const claim = recursivePackedClaims[i]
254
+ ? recursivePackedClaims[i]
255
+ : claims[i];
256
+ /** This part is set discloure for array items.
257
+ * The example of disclosureFrame of an Array is
258
+ *
259
+ * const claims = {
260
+ * array: ['a', 'b', 'c']
261
+ * }
262
+ *
263
+ * diclosureFrame: DisclosureFrame<typeof claims> = {
264
+ * array: {
265
+ * _sd: [0, 2]
266
+ * }
267
+ * }
268
+ *
269
+ * It means that we want to disclose the first and the third item of the array
270
+ *
271
+ * So If the index `i` is in the disclosure list(sd), then we create a disclosure for the claim
272
+ */
273
+ // @ts-ignore
274
+ if (sd.includes(i)) {
275
+ const salt = await saltGenerator(16);
276
+ const disclosure = new Disclosure([salt, claim]);
277
+ const digest = await disclosure.digest(hash);
278
+ packedClaims.push({ [SD_LIST_KEY]: digest });
279
+ disclosures.push(disclosure);
280
+ } else {
281
+ packedClaims.push(claim);
282
+ }
283
+ }
284
+ for (let j = 0; j < decoyCount; j++) {
285
+ const decoyDigest = await createDecoy(hash, saltGenerator);
286
+ packedClaims.push({ [SD_LIST_KEY]: decoyDigest });
287
+ }
288
+ return { packedClaims, disclosures };
289
+ }
290
+
291
+ const packedClaims: Record<string, unknown> = {};
292
+ const disclosures: Array<Disclosure> = [];
293
+ const recursivePackedClaims: Record<string, unknown> = {};
294
+
295
+ for (const key in disclosureFrame) {
296
+ if (key !== SD_DIGEST) {
297
+ const packed = await pack(
298
+ // @ts-ignore
299
+ claims[key],
300
+ disclosureFrame[key],
301
+ hash,
302
+ saltGenerator,
303
+ );
304
+ recursivePackedClaims[key] = packed.packedClaims;
305
+ disclosures.push(...packed.disclosures);
306
+ }
307
+ }
308
+
309
+ const _sd: string[] = [];
310
+
311
+ for (const key in claims) {
312
+ const claim = recursivePackedClaims[key]
313
+ ? recursivePackedClaims[key]
314
+ : claims[key];
315
+ // @ts-ignore
316
+ if (sd.includes(key)) {
317
+ const salt = await saltGenerator(16);
318
+ const disclosure = new Disclosure([salt, key, claim]);
319
+ const digest = await disclosure.digest(hash);
320
+
321
+ _sd.push(digest);
322
+ disclosures.push(disclosure);
323
+ } else {
324
+ packedClaims[key] = claim;
325
+ }
326
+ }
327
+
328
+ for (let j = 0; j < decoyCount; j++) {
329
+ const decoyDigest = await createDecoy(hash, saltGenerator);
330
+ _sd.push(decoyDigest);
331
+ }
332
+
333
+ if (_sd.length > 0) {
334
+ packedClaims[SD_DIGEST] = _sd.sort();
335
+ }
336
+ return { packedClaims, disclosures };
337
+ };
@@ -0,0 +1,30 @@
1
+ import { createDecoy } from '../decoy';
2
+ import { describe, expect, test } from 'vitest';
3
+ import { Base64urlEncode } from '@sd-jwt/utils';
4
+ import { digest, generateSalt } from '@sd-jwt/crypto-nodejs';
5
+
6
+ const hash = {
7
+ hasher: digest,
8
+ alg: 'SHA256',
9
+ };
10
+
11
+ describe('Decoy', () => {
12
+ test('decoy', async () => {
13
+ const decoyValue = await createDecoy(hash, generateSalt);
14
+ expect(decoyValue.length).toBe(43);
15
+ });
16
+
17
+ // ref https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/07/
18
+ // *Claim email*:
19
+ // * SHA-256 Hash: JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE
20
+ // * Disclosure: WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ
21
+ // * Contents: ["6Ij7tM-a5iVPGboS5tmvVA", "email", "johndoe@example.com"]
22
+ test('apply hasher and saltGenerator', async () => {
23
+ const decoyValue = await createDecoy(hash, () =>
24
+ Base64urlEncode(
25
+ '["6Ij7tM-a5iVPGboS5tmvVA", "email", "johndoe@example.com"]',
26
+ ),
27
+ );
28
+ expect(decoyValue).toBe('JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE');
29
+ });
30
+ });