@sd-jwt/core 0.1.0 → 0.1.1-next.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +201 -0
  3. package/README.md +106 -0
  4. package/dist/index.d.mts +659 -0
  5. package/dist/index.d.ts +659 -0
  6. package/dist/index.js +1906 -0
  7. package/dist/index.mjs +1846 -0
  8. package/package.json +59 -47
  9. package/src/decode/decode.ts +376 -0
  10. package/src/decode/index.ts +1 -0
  11. package/src/decoy.ts +15 -0
  12. package/src/flattenJSON.ts +99 -0
  13. package/src/generalJSON.ts +148 -0
  14. package/src/index.ts +945 -0
  15. package/src/jwt.ts +190 -0
  16. package/src/kbjwt.ts +81 -0
  17. package/src/present/index.ts +1 -0
  18. package/src/present/present.ts +210 -0
  19. package/src/sdjwt.ts +354 -0
  20. package/src/test/decode/decode.spec.ts +202 -0
  21. package/src/test/decoy.spec.ts +30 -0
  22. package/src/test/flattenJSON.spec.ts +122 -0
  23. package/src/test/generalJSON.spec.ts +198 -0
  24. package/src/test/index.spec.ts +974 -0
  25. package/src/test/jwt.spec.ts +301 -0
  26. package/src/test/kbjwt.spec.ts +565 -0
  27. package/src/test/pass.spec.ts +7 -0
  28. package/src/test/present/present.spec.ts +305 -0
  29. package/src/test/sdjwt.spec.ts +384 -0
  30. package/src/test/types/type.spec.ts +88 -0
  31. package/src/test/utils/base64url.spec.ts +33 -0
  32. package/src/test/utils/disclosure.spec.ts +170 -0
  33. package/src/test/utils/error.spec.ts +15 -0
  34. package/src/types/index.ts +2 -0
  35. package/src/types/type.ts +249 -0
  36. package/src/types/verification-error.ts +55 -0
  37. package/src/utils/base64url.ts +6 -0
  38. package/src/utils/disclosure.ts +101 -0
  39. package/src/utils/error.ts +25 -0
  40. package/src/utils/index.ts +3 -0
  41. package/src/utils/strict-json.ts +17 -0
  42. package/test/app-e2e.spec.ts +292 -0
  43. package/test/array_data_types.json +31 -0
  44. package/test/array_full_sd.json +21 -0
  45. package/test/array_in_sd.json +13 -0
  46. package/test/array_nested_in_plain.json +29 -0
  47. package/test/array_none_disclosed.json +17 -0
  48. package/test/array_of_nulls.json +15 -0
  49. package/test/array_of_objects.json +65 -0
  50. package/test/array_of_scalars.json +19 -0
  51. package/test/array_recursive_sd.json +35 -0
  52. package/test/array_recursive_sd_some_disclosed.json +63 -0
  53. package/test/complex.json +43 -0
  54. package/test/header_mod.json +48 -0
  55. package/test/json_serialization.json +48 -0
  56. package/test/key_binding.json +48 -0
  57. package/test/no_sd.json +36 -0
  58. package/test/object_data_types.json +62 -0
  59. package/test/recursions.json +117 -0
  60. package/test/rfc9901-validation.spec.ts +150 -0
  61. package/tsconfig.json +7 -0
  62. package/vitest.config.mts +4 -0
  63. package/build/base64url.d.ts +0 -28
  64. package/build/base64url.js +0 -40
  65. package/build/base64url.js.map +0 -1
  66. package/build/hasherAlgorithm.d.ts +0 -70
  67. package/build/hasherAlgorithm.js +0 -75
  68. package/build/hasherAlgorithm.js.map +0 -1
  69. package/build/index.d.ts +0 -11
  70. package/build/index.js +0 -20
  71. package/build/index.js.map +0 -1
  72. package/build/jwt/compact.d.ts +0 -6
  73. package/build/jwt/compact.js +0 -27
  74. package/build/jwt/compact.js.map +0 -1
  75. package/build/jwt/error.d.ts +0 -2
  76. package/build/jwt/error.js +0 -7
  77. package/build/jwt/error.js.map +0 -1
  78. package/build/jwt/index.d.ts +0 -3
  79. package/build/jwt/index.js +0 -20
  80. package/build/jwt/index.js.map +0 -1
  81. package/build/jwt/jwt.d.ts +0 -204
  82. package/build/jwt/jwt.js +0 -324
  83. package/build/jwt/jwt.js.map +0 -1
  84. package/build/keyBinding/index.d.ts +0 -1
  85. package/build/keyBinding/index.js +0 -18
  86. package/build/keyBinding/index.js.map +0 -1
  87. package/build/keyBinding/keyBinding.d.ts +0 -56
  88. package/build/keyBinding/keyBinding.js +0 -99
  89. package/build/keyBinding/keyBinding.js.map +0 -1
  90. package/build/sdJwt/compact.d.ts +0 -8
  91. package/build/sdJwt/compact.js +0 -39
  92. package/build/sdJwt/compact.js.map +0 -1
  93. package/build/sdJwt/decoys.d.ts +0 -2
  94. package/build/sdJwt/decoys.js +0 -35
  95. package/build/sdJwt/decoys.js.map +0 -1
  96. package/build/sdJwt/disclosureFrame.d.ts +0 -7
  97. package/build/sdJwt/disclosureFrame.js +0 -89
  98. package/build/sdJwt/disclosureFrame.js.map +0 -1
  99. package/build/sdJwt/disclosureMapping.d.ts +0 -44
  100. package/build/sdJwt/disclosureMapping.js +0 -289
  101. package/build/sdJwt/disclosureMapping.js.map +0 -1
  102. package/build/sdJwt/disclosures.d.ts +0 -14
  103. package/build/sdJwt/disclosures.js +0 -59
  104. package/build/sdJwt/disclosures.js.map +0 -1
  105. package/build/sdJwt/error.d.ts +0 -2
  106. package/build/sdJwt/error.js +0 -7
  107. package/build/sdJwt/error.js.map +0 -1
  108. package/build/sdJwt/index.d.ts +0 -7
  109. package/build/sdJwt/index.js +0 -24
  110. package/build/sdJwt/index.js.map +0 -1
  111. package/build/sdJwt/presentationFrame.d.ts +0 -4
  112. package/build/sdJwt/presentationFrame.js +0 -73
  113. package/build/sdJwt/presentationFrame.js.map +0 -1
  114. package/build/sdJwt/sdJwt.d.ts +0 -204
  115. package/build/sdJwt/sdJwt.js +0 -383
  116. package/build/sdJwt/sdJwt.js.map +0 -1
  117. package/build/sdJwt/swapClaim.d.ts +0 -3
  118. package/build/sdJwt/swapClaim.js +0 -90
  119. package/build/sdJwt/swapClaim.js.map +0 -1
  120. package/build/sdJwt/types.d.ts +0 -5
  121. package/build/sdJwt/types.js +0 -3
  122. package/build/sdJwt/types.js.map +0 -1
  123. package/build/sdJwtVc/error.d.ts +0 -2
  124. package/build/sdJwtVc/error.js +0 -7
  125. package/build/sdJwtVc/error.js.map +0 -1
  126. package/build/sdJwtVc/index.d.ts +0 -2
  127. package/build/sdJwtVc/index.js +0 -19
  128. package/build/sdJwtVc/index.js.map +0 -1
  129. package/build/sdJwtVc/sdJwtVc.d.ts +0 -47
  130. package/build/sdJwtVc/sdJwtVc.js +0 -125
  131. package/build/sdJwtVc/sdJwtVc.js.map +0 -1
  132. package/build/signatureAndEncryptionAlgorithm.d.ts +0 -105
  133. package/build/signatureAndEncryptionAlgorithm.js +0 -110
  134. package/build/signatureAndEncryptionAlgorithm.js.map +0 -1
  135. package/build/types/disclosure.d.ts +0 -5
  136. package/build/types/disclosure.js +0 -3
  137. package/build/types/disclosure.js.map +0 -1
  138. package/build/types/frame.d.ts +0 -5
  139. package/build/types/frame.js +0 -3
  140. package/build/types/frame.js.map +0 -1
  141. package/build/types/hasher.d.ts +0 -14
  142. package/build/types/hasher.js +0 -3
  143. package/build/types/hasher.js.map +0 -1
  144. package/build/types/index.d.ts +0 -6
  145. package/build/types/index.js +0 -23
  146. package/build/types/index.js.map +0 -1
  147. package/build/types/present.d.ts +0 -2
  148. package/build/types/present.js +0 -3
  149. package/build/types/present.js.map +0 -1
  150. package/build/types/saltGenerator.d.ts +0 -17
  151. package/build/types/saltGenerator.js +0 -3
  152. package/build/types/saltGenerator.js.map +0 -1
  153. package/build/types/signer.d.ts +0 -2
  154. package/build/types/signer.js +0 -3
  155. package/build/types/signer.js.map +0 -1
  156. package/build/types/utils.d.ts +0 -5
  157. package/build/types/utils.js +0 -3
  158. package/build/types/utils.js.map +0 -1
  159. package/build/types/verifier.d.ts +0 -14
  160. package/build/types/verifier.js +0 -3
  161. package/build/types/verifier.js.map +0 -1
  162. package/build/utils/index.d.ts +0 -2
  163. package/build/utils/index.js +0 -19
  164. package/build/utils/index.js.map +0 -1
  165. package/build/utils/traverse.d.ts +0 -8
  166. package/build/utils/traverse.js +0 -29
  167. package/build/utils/traverse.js.map +0 -1
  168. package/build/utils/utils.d.ts +0 -7
  169. package/build/utils/utils.js +0 -109
  170. package/build/utils/utils.js.map +0 -1
package/src/sdjwt.ts ADDED
@@ -0,0 +1,354 @@
1
+ import { createHashMapping, getSDAlgAndPayload, unpack } from './decode';
2
+ import { createDecoy } from './decoy';
3
+ import { Jwt } from './jwt';
4
+ import { KBJwt } from './kbjwt';
5
+ import { transformPresentationFrame } from './present';
6
+ import {
7
+ type DisclosureFrame,
8
+ type Hasher,
9
+ type HasherAndAlg,
10
+ type kbHeader,
11
+ type kbPayload,
12
+ type PresentationFrame,
13
+ type SaltGenerator,
14
+ SD_DECOY,
15
+ SD_DIGEST,
16
+ SD_LIST_KEY,
17
+ SD_SEPARATOR,
18
+ type SDJWTCompact,
19
+ } from './types';
20
+ import { Disclosure, SDJWTException } from './utils';
21
+
22
+ export type SDJwtData<
23
+ Header extends Record<string, unknown>,
24
+ Payload extends Record<string, unknown>,
25
+ KBHeader extends kbHeader = kbHeader,
26
+ KBPayload extends kbPayload = kbPayload,
27
+ > = {
28
+ jwt?: Jwt<Header, Payload>;
29
+ disclosures?: Array<Disclosure>;
30
+ kbJwt?: KBJwt<KBHeader, KBPayload>;
31
+ };
32
+
33
+ export class SDJwt<
34
+ Header extends Record<string, unknown> = Record<string, unknown>,
35
+ Payload extends Record<string, unknown> = Record<string, unknown>,
36
+ KBHeader extends kbHeader = kbHeader,
37
+ KBPayload extends kbPayload = kbPayload,
38
+ > {
39
+ public jwt?: Jwt<Header, Payload>;
40
+ public disclosures?: Array<Disclosure>;
41
+ public kbJwt?: KBJwt<KBHeader, KBPayload>;
42
+
43
+ constructor(data?: SDJwtData<Header, Payload, KBHeader, KBPayload>) {
44
+ this.jwt = data?.jwt;
45
+ this.disclosures = data?.disclosures;
46
+ this.kbJwt = data?.kbJwt;
47
+ }
48
+
49
+ public static async decodeSDJwt<
50
+ Header extends Record<string, unknown> = Record<string, unknown>,
51
+ Payload extends Record<string, unknown> = Record<string, unknown>,
52
+ KBHeader extends kbHeader = kbHeader,
53
+ KBPayload extends kbPayload = kbPayload,
54
+ >(
55
+ sdjwt: SDJWTCompact,
56
+ hasher: Hasher,
57
+ ): Promise<{
58
+ jwt: Jwt<Header, Payload>;
59
+ disclosures: Array<Disclosure>;
60
+ kbJwt?: KBJwt<KBHeader, KBPayload>;
61
+ }> {
62
+ const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
63
+ const jwt = Jwt.fromEncode<Header, Payload>(encodedJwt);
64
+
65
+ if (!jwt.payload) {
66
+ throw new Error('Payload is undefined on the JWT. Invalid state reached');
67
+ }
68
+
69
+ if (encodedDisclosures.length === 0) {
70
+ return {
71
+ jwt,
72
+ disclosures: [],
73
+ };
74
+ }
75
+
76
+ const encodedKeyBindingJwt = encodedDisclosures.pop();
77
+ const kbJwt = encodedKeyBindingJwt
78
+ ? KBJwt.fromKBEncode<KBHeader, KBPayload>(encodedKeyBindingJwt)
79
+ : undefined;
80
+
81
+ const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
82
+
83
+ const disclosures = await Promise.all(
84
+ encodedDisclosures.map((ed) =>
85
+ Disclosure.fromEncode(ed, { alg: _sd_alg, hasher }),
86
+ ),
87
+ );
88
+
89
+ return {
90
+ jwt,
91
+ disclosures,
92
+ kbJwt,
93
+ };
94
+ }
95
+
96
+ public static async extractJwt<
97
+ Header extends Record<string, unknown> = Record<string, unknown>,
98
+ Payload extends Record<string, unknown> = Record<string, unknown>,
99
+ >(encodedSdJwt: SDJWTCompact): Promise<Jwt<Header, Payload>> {
100
+ const [encodedJwt, ..._encodedDisclosures] =
101
+ encodedSdJwt.split(SD_SEPARATOR);
102
+
103
+ return Jwt.fromEncode(encodedJwt);
104
+ }
105
+
106
+ public static async fromEncode<
107
+ Header extends Record<string, unknown> = Record<string, unknown>,
108
+ Payload extends Record<string, unknown> = Record<string, unknown>,
109
+ KBHeader extends kbHeader = kbHeader,
110
+ KBPayload extends kbPayload = kbPayload,
111
+ >(
112
+ encodedSdJwt: SDJWTCompact,
113
+ hasher: Hasher,
114
+ ): Promise<SDJwt<Header, Payload>> {
115
+ const { jwt, disclosures, kbJwt } = await SDJwt.decodeSDJwt<
116
+ Header,
117
+ Payload,
118
+ KBHeader,
119
+ KBPayload
120
+ >(encodedSdJwt, hasher);
121
+
122
+ return new SDJwt<Header, Payload, KBHeader, KBPayload>({
123
+ jwt,
124
+ disclosures,
125
+ kbJwt,
126
+ });
127
+ }
128
+
129
+ public async present<T extends Record<string, unknown>>(
130
+ presentFrame: PresentationFrame<T> | undefined,
131
+ hasher: Hasher,
132
+ ): Promise<SDJWTCompact> {
133
+ const disclosures = await this.getPresentDisclosures(presentFrame, hasher);
134
+ const presentSDJwt = new SDJwt({
135
+ jwt: this.jwt,
136
+ disclosures,
137
+ kbJwt: this.kbJwt,
138
+ });
139
+ return presentSDJwt.encodeSDJwt();
140
+ }
141
+
142
+ public async getPresentDisclosures<T extends Record<string, unknown>>(
143
+ presentFrame: PresentationFrame<T> | undefined,
144
+ hasher: Hasher,
145
+ ): Promise<Disclosure<unknown>[]> {
146
+ if (!this.jwt?.payload || !this.disclosures) {
147
+ throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
148
+ }
149
+ const { _sd_alg: alg } = getSDAlgAndPayload(this.jwt.payload);
150
+ const hash = { alg, hasher };
151
+ const hashmap = await createHashMapping(this.disclosures, hash);
152
+ const { disclosureKeymap } = await unpack(
153
+ this.jwt.payload,
154
+ this.disclosures,
155
+ hasher,
156
+ );
157
+
158
+ const keys = presentFrame
159
+ ? transformPresentationFrame(presentFrame)
160
+ : await this.presentableKeys(hasher);
161
+ const disclosures = keys
162
+ .map((k) => hashmap[disclosureKeymap[k]])
163
+ .filter((d) => d !== undefined);
164
+ return disclosures;
165
+ }
166
+
167
+ public encodeSDJwt(): SDJWTCompact {
168
+ const data: string[] = [];
169
+
170
+ if (!this.jwt) {
171
+ throw new SDJWTException('Invalid sd-jwt: jwt is missing');
172
+ }
173
+
174
+ const encodedJwt = this.jwt.encodeJwt();
175
+ data.push(encodedJwt);
176
+
177
+ if (this.disclosures && this.disclosures.length > 0) {
178
+ const encodeddisclosures = this.disclosures
179
+ .map((dc) => dc.encode())
180
+ .join(SD_SEPARATOR);
181
+ data.push(encodeddisclosures);
182
+ }
183
+
184
+ data.push(this.kbJwt ? this.kbJwt.encodeJwt() : '');
185
+ return data.join(SD_SEPARATOR);
186
+ }
187
+
188
+ public async keys(hasher: Hasher): Promise<string[]> {
189
+ return listKeys(await this.getClaims(hasher)).sort();
190
+ }
191
+
192
+ public async presentableKeys(hasher: Hasher): Promise<string[]> {
193
+ if (!this.jwt?.payload || !this.disclosures) {
194
+ throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
195
+ }
196
+ const { disclosureKeymap } = await unpack(
197
+ this.jwt?.payload,
198
+ this.disclosures,
199
+ hasher,
200
+ );
201
+ return Object.keys(disclosureKeymap).sort();
202
+ }
203
+
204
+ public async getClaims<T>(hasher: Hasher): Promise<T> {
205
+ if (!this.jwt?.payload || !this.disclosures) {
206
+ throw new SDJWTException('Invalid sd-jwt: jwt or disclosures is missing');
207
+ }
208
+ const { unpackedObj } = await unpack(
209
+ this.jwt.payload,
210
+ this.disclosures,
211
+ hasher,
212
+ );
213
+ return unpackedObj as T;
214
+ }
215
+ }
216
+
217
+ export const listKeys = (obj: Record<string, unknown>, prefix = '') => {
218
+ const keys: string[] = [];
219
+ for (const key in obj) {
220
+ if (obj[key] === undefined) continue;
221
+ const newKey = prefix ? `${prefix}.${key}` : key;
222
+ keys.push(newKey);
223
+
224
+ const value = obj[key];
225
+ if (value && typeof value === 'object') {
226
+ keys.push(...listKeys(value as Record<string, unknown>, newKey));
227
+ }
228
+ }
229
+ return keys;
230
+ };
231
+
232
+ export const pack = async <T extends Record<string, unknown>>(
233
+ claims: T,
234
+ disclosureFrame: DisclosureFrame<T> | undefined,
235
+ hash: HasherAndAlg,
236
+ saltGenerator: SaltGenerator,
237
+ ): Promise<{
238
+ packedClaims: Record<string, unknown> | Array<Record<string, unknown>>;
239
+ disclosures: Array<Disclosure>;
240
+ }> => {
241
+ if (!disclosureFrame) {
242
+ return {
243
+ packedClaims: claims,
244
+ disclosures: [],
245
+ };
246
+ }
247
+
248
+ const sd = disclosureFrame[SD_DIGEST] ?? [];
249
+ const decoyCount = disclosureFrame[SD_DECOY] ?? 0;
250
+
251
+ if (Array.isArray(claims)) {
252
+ const packedClaims: Array<Record<typeof SD_LIST_KEY, string>> = [];
253
+ const disclosures: Array<Disclosure> = [];
254
+ const recursivePackedClaims: Record<number, unknown> = {};
255
+
256
+ for (const key in disclosureFrame) {
257
+ if (key !== SD_DIGEST) {
258
+ const idx = Number.parseInt(key, 10);
259
+ const packed = await pack(
260
+ claims[idx],
261
+ disclosureFrame[idx],
262
+ hash,
263
+ saltGenerator,
264
+ );
265
+ recursivePackedClaims[idx] = packed.packedClaims;
266
+ disclosures.push(...packed.disclosures);
267
+ }
268
+ }
269
+
270
+ for (let i = 0; i < claims.length; i++) {
271
+ const claim = recursivePackedClaims[i]
272
+ ? recursivePackedClaims[i]
273
+ : claims[i];
274
+ /** This part is set discloure for array items.
275
+ * The example of disclosureFrame of an Array is
276
+ *
277
+ * const claims = {
278
+ * array: ['a', 'b', 'c']
279
+ * }
280
+ *
281
+ * diclosureFrame: DisclosureFrame<typeof claims> = {
282
+ * array: {
283
+ * _sd: [0, 2]
284
+ * }
285
+ * }
286
+ *
287
+ * It means that we want to disclose the first and the third item of the array
288
+ *
289
+ * So If the index `i` is in the disclosure list(sd), then we create a disclosure for the claim
290
+ */
291
+ // @ts-expect-error
292
+ if (sd.includes(i)) {
293
+ const salt = await saltGenerator(16);
294
+ const disclosure = new Disclosure([salt, claim]);
295
+ const digest = await disclosure.digest(hash);
296
+ packedClaims.push({ [SD_LIST_KEY]: digest });
297
+ disclosures.push(disclosure);
298
+ } else {
299
+ packedClaims.push(claim);
300
+ }
301
+ }
302
+ for (let j = 0; j < decoyCount; j++) {
303
+ const decoyDigest = await createDecoy(hash, saltGenerator);
304
+ packedClaims.push({ [SD_LIST_KEY]: decoyDigest });
305
+ }
306
+ return { packedClaims, disclosures };
307
+ }
308
+
309
+ const packedClaims: Record<string, unknown> = {};
310
+ const disclosures: Array<Disclosure> = [];
311
+ const recursivePackedClaims: Record<string, unknown> = {};
312
+
313
+ for (const key in disclosureFrame) {
314
+ if (key !== SD_DIGEST) {
315
+ const packed = await pack(
316
+ // @ts-expect-error
317
+ claims[key],
318
+ disclosureFrame[key],
319
+ hash,
320
+ saltGenerator,
321
+ );
322
+ recursivePackedClaims[key] = packed.packedClaims;
323
+ disclosures.push(...packed.disclosures);
324
+ }
325
+ }
326
+
327
+ const _sd: string[] = [];
328
+
329
+ for (const key in claims) {
330
+ const claim = recursivePackedClaims[key]
331
+ ? recursivePackedClaims[key]
332
+ : claims[key];
333
+ if (sd.includes(key)) {
334
+ const salt = await saltGenerator(16);
335
+ const disclosure = new Disclosure([salt, key, claim]);
336
+ const digest = await disclosure.digest(hash);
337
+
338
+ _sd.push(digest);
339
+ disclosures.push(disclosure);
340
+ } else {
341
+ packedClaims[key] = claim;
342
+ }
343
+ }
344
+
345
+ for (let j = 0; j < decoyCount; j++) {
346
+ const decoyDigest = await createDecoy(hash, saltGenerator);
347
+ _sd.push(decoyDigest);
348
+ }
349
+
350
+ if (_sd.length > 0) {
351
+ packedClaims[SD_DIGEST] = _sd.sort();
352
+ }
353
+ return { packedClaims, disclosures };
354
+ };
@@ -0,0 +1,202 @@
1
+ import { hasher as digest } from '@owf/crypto';
2
+ import { describe, expect, test } from 'vitest';
3
+ import {
4
+ createHashMapping,
5
+ decodeJwt,
6
+ decodeSdJwt,
7
+ decodeSdJwtSync,
8
+ getClaims,
9
+ getClaimsSync,
10
+ getSDAlgAndPayload,
11
+ splitSdJwt,
12
+ unpackObj,
13
+ } from '../../decode';
14
+
15
+ describe('decode tests', () => {
16
+ test('decode jwt', () => {
17
+ const jwt =
18
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
19
+ const { header, payload, signature } = decodeJwt(jwt);
20
+ expect(signature).toBeDefined();
21
+ expect(header).toStrictEqual({ alg: 'HS256', typ: 'JWT' });
22
+ expect(payload).toStrictEqual({
23
+ sub: '1234567890',
24
+ name: 'John Doe',
25
+ iat: 1516239022,
26
+ });
27
+ });
28
+
29
+ test('decode jwt with invalid input', () => {
30
+ const jwt = 'invalid.invalid';
31
+ expect(() => decodeJwt(jwt)).toThrow('Invalid JWT as input');
32
+ });
33
+
34
+ test('split sdjwt', () => {
35
+ const sdjwt = 'h.p.s~d1~d2~';
36
+ const { jwt, disclosures, kbJwt } = splitSdJwt(sdjwt);
37
+ expect(jwt).toBe('h.p.s');
38
+ expect(disclosures).toStrictEqual(['d1', 'd2']);
39
+ expect(kbJwt).toBeUndefined();
40
+ });
41
+
42
+ test('split sdjwt without disclosures', () => {
43
+ const sdjwt = 'h.p.s';
44
+ const { jwt, disclosures, kbJwt } = splitSdJwt(sdjwt);
45
+ expect(jwt).toBe('h.p.s');
46
+ expect(disclosures).toStrictEqual([]);
47
+ expect(kbJwt).toBeUndefined();
48
+ });
49
+
50
+ test('split sdjwt with kbjwt', () => {
51
+ const sdjwt = 'h.p.s~d1~d2~kbh.kbp.kbs';
52
+ const { jwt, disclosures, kbJwt } = splitSdJwt(sdjwt);
53
+ expect(jwt).toBe('h.p.s');
54
+ expect(disclosures).toStrictEqual(['d1', 'd2']);
55
+ expect(kbJwt).toBe('kbh.kbp.kbs');
56
+ });
57
+
58
+ test('decode sdjwt', async () => {
59
+ const sdjwt =
60
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJfc2QiOlsiaWQ1azZ1ZVplVTY4bExaMlU2YjJJbF9QR3ZKb1RDMlpkMkpwY0RwMzFIWSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.GiLF_HhacrstqCJ223VvWOoJJWU8qk4dYQHklSMwxv36pPF_7ER53Wbty1qYRlQ6NeMUdBRRdj9JQLLCzz1gCQ~WyI2NTMxNDA2ZmVhZmU0YjBmIiwiZm9vIiwiYmFyIl0~';
61
+ const decodedSdJwt = await decodeSdJwt(sdjwt, digest);
62
+ expect(decodedSdJwt).toBeDefined();
63
+ expect(decodedSdJwt.kbJwt).toBeUndefined();
64
+ expect(decodedSdJwt.disclosures.length).toEqual(1);
65
+ expect(decodedSdJwt.jwt).toBeDefined();
66
+ });
67
+
68
+ test('decode jwt', async () => {
69
+ const jwt =
70
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6InNkK2p3dCJ9.eyJsYXN0bmFtZSI6IkRvZSIsInNzbiI6IjEyMy00NS02Nzg5IiwiX3NkIjpbIk4yUXhZV1UxTlRnME1qQmpOR1JpWVRCaU1tRmtaamN5WXpSbFpXUmhaRGd5WkRCbE1qaGhZVGcwTnpJMU9XSXpZek5qWkdNNE1qZG1NVGN6TmpZd05RIiwiWlRSalkyUTVOemRoWkRVM05tWTFZV0UyTmpka01XVmpNRE16WXpOak5qQmtNak5pT0dZelpHSTBOelV4TURsak9EWTRNREEzWm1JeFpUY3daREZqTmciXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.mX14Sw86xy8NFQta7tCfNmhVCqzfaJ_K3VEIhTjbLDY';
71
+ const decodedSdJwt = await decodeSdJwt(jwt, digest);
72
+ expect(decodedSdJwt).toBeDefined();
73
+ expect(decodedSdJwt.kbJwt).toBeUndefined();
74
+ expect(decodedSdJwt.disclosures.length).toEqual(0);
75
+ expect(decodedSdJwt.jwt).toBeDefined();
76
+ });
77
+
78
+ test('decode sdjwt sync', () => {
79
+ const sdjwt =
80
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJfc2QiOlsiaWQ1azZ1ZVplVTY4bExaMlU2YjJJbF9QR3ZKb1RDMlpkMkpwY0RwMzFIWSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.GiLF_HhacrstqCJ223VvWOoJJWU8qk4dYQHklSMwxv36pPF_7ER53Wbty1qYRlQ6NeMUdBRRdj9JQLLCzz1gCQ~WyI2NTMxNDA2ZmVhZmU0YjBmIiwiZm9vIiwiYmFyIl0~';
81
+ const decodedSdJwt = decodeSdJwtSync(sdjwt, digest);
82
+ expect(decodedSdJwt).toBeDefined();
83
+ expect(decodedSdJwt.kbJwt).toBeUndefined();
84
+ expect(decodedSdJwt.disclosures.length).toEqual(1);
85
+ expect(decodedSdJwt.jwt).toBeDefined();
86
+ });
87
+
88
+ test('decode jwt sync', () => {
89
+ const jwt =
90
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6InNkK2p3dCJ9.eyJsYXN0bmFtZSI6IkRvZSIsInNzbiI6IjEyMy00NS02Nzg5IiwiX3NkIjpbIk4yUXhZV1UxTlRnME1qQmpOR1JpWVRCaU1tRmtaamN5WXpSbFpXUmhaRGd5WkRCbE1qaGhZVGcwTnpJMU9XSXpZek5qWkdNNE1qZG1NVGN6TmpZd05RIiwiWlRSalkyUTVOemRoWkRVM05tWTFZV0UyTmpka01XVmpNRE16WXpOak5qQmtNak5pT0dZelpHSTBOelV4TURsak9EWTRNREEzWm1JeFpUY3daREZqTmciXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.mX14Sw86xy8NFQta7tCfNmhVCqzfaJ_K3VEIhTjbLDY';
91
+ const decodedSdJwt = decodeSdJwtSync(jwt, digest);
92
+ expect(decodedSdJwt).toBeDefined();
93
+ expect(decodedSdJwt.kbJwt).toBeUndefined();
94
+ expect(decodedSdJwt.disclosures.length).toEqual(0);
95
+ expect(decodedSdJwt.jwt).toBeDefined();
96
+ });
97
+
98
+ test('decode sdjwt sync (with KB)', () => {
99
+ const sdjwt =
100
+ 'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg~WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA';
101
+ const decodedSdJwt = decodeSdJwtSync(sdjwt, digest);
102
+ expect(decodedSdJwt).toBeDefined();
103
+ expect(decodedSdJwt.kbJwt).toBeDefined();
104
+ expect(decodedSdJwt.disclosures.length).toEqual(3);
105
+ expect(decodedSdJwt.jwt).toBeDefined();
106
+ });
107
+
108
+ test('decode sdjwt (with KB)', async () => {
109
+ const sdjwt =
110
+ 'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg~WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA';
111
+ const decodedSdJwt = await decodeSdJwt(sdjwt, digest);
112
+ expect(decodedSdJwt).toBeDefined();
113
+ expect(decodedSdJwt.kbJwt).toBeDefined();
114
+ expect(decodedSdJwt.disclosures.length).toEqual(3);
115
+ expect(decodedSdJwt.jwt).toBeDefined();
116
+ });
117
+
118
+ test('get claims', async () => {
119
+ const sdjwt =
120
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJfc2QiOlsiaWQ1azZ1ZVplVTY4bExaMlU2YjJJbF9QR3ZKb1RDMlpkMkpwY0RwMzFIWSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.GiLF_HhacrstqCJ223VvWOoJJWU8qk4dYQHklSMwxv36pPF_7ER53Wbty1qYRlQ6NeMUdBRRdj9JQLLCzz1gCQ~WyI2NTMxNDA2ZmVhZmU0YjBmIiwiZm9vIiwiYmFyIl0~';
121
+ const decodedSdJwt = await decodeSdJwt(sdjwt, digest);
122
+ const claims = await getClaims(
123
+ decodedSdJwt.jwt.payload,
124
+ decodedSdJwt.disclosures,
125
+ digest,
126
+ );
127
+ expect(claims).toStrictEqual({
128
+ foo: 'bar',
129
+ });
130
+ });
131
+
132
+ test('getClaims #2', async () => {
133
+ const sdjwt =
134
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJ0ZXN0Ijp7Il9zZCI6WyJqVEszMHNleDZhYV9kUk1KSWZDR056Q0FwbVB5MzRRNjNBa3QzS3hhSktzIl19LCJfc2QiOlsiME9nMi1ReG95eW1UOGNnVzZZUjVSSFpQLUJuR2tHUi1NM2otLV92RWlzSSIsIkcwZ3lHNnExVFMyUlQxMkZ3X2RRRDVVcjlZc1AwZlVWOXVtQWdGMC1jQ1EiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.ggEyE4SeDO2Hu3tol3VLmi7NQj56yKzKQDaafocgkLrUBdivghohtzrfcbrMN7CRufJ_Cnh0EL54kymXLGTdDQ~WyIwNGU0MjAzOWU4ZWFiOWRjIiwiYSIsIjEiXQ~WyIwOGE1Yjc5MjMyYjAzYzBhIiwiMSJd~WyJiNWE2YjUzZGQwYTFmMGIwIiwienp6IiwieHh4Il0~WyIxYzdmOTE4ZTE0MjA2NzZiIiwiZm9vIiwiYmFyIl0~WyJmZjYxYzQ5ZGU2NjFiYzMxIiwiYXJyIixbeyIuLi4iOiJTSG96VW5KNUpkd0ZtTjVCbXB5dXZCWGZfZWRjckVvcExPYThTVlBFUmg0In0sIjIiLHsiX3NkIjpbIkpuODNhZkp0OGx4NG1FMzZpRkZyS2U2R2VnN0dlVUQ4Z3UwdVo3NnRZcW8iXX1dXQ~';
135
+ const decodedSdJwt = await decodeSdJwt(sdjwt, digest);
136
+ const claims = await getClaims(
137
+ decodedSdJwt.jwt.payload,
138
+ decodedSdJwt.disclosures,
139
+ digest,
140
+ );
141
+ expect(claims).toStrictEqual({
142
+ foo: 'bar',
143
+ arr: ['1', '2', { a: '1' }],
144
+ test: {
145
+ zzz: 'xxx',
146
+ },
147
+ });
148
+ });
149
+
150
+ test('get claims sync', () => {
151
+ const sdjwt =
152
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJfc2QiOlsiaWQ1azZ1ZVplVTY4bExaMlU2YjJJbF9QR3ZKb1RDMlpkMkpwY0RwMzFIWSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.GiLF_HhacrstqCJ223VvWOoJJWU8qk4dYQHklSMwxv36pPF_7ER53Wbty1qYRlQ6NeMUdBRRdj9JQLLCzz1gCQ~WyI2NTMxNDA2ZmVhZmU0YjBmIiwiZm9vIiwiYmFyIl0~';
153
+ const decodedSdJwt = decodeSdJwtSync(sdjwt, digest);
154
+ const claims = getClaimsSync(
155
+ decodedSdJwt.jwt.payload,
156
+ decodedSdJwt.disclosures,
157
+ digest,
158
+ );
159
+ expect(claims).toStrictEqual({
160
+ foo: 'bar',
161
+ });
162
+ });
163
+
164
+ test('getClaims sync #2', () => {
165
+ const sdjwt =
166
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJ0ZXN0Ijp7Il9zZCI6WyJqVEszMHNleDZhYV9kUk1KSWZDR056Q0FwbVB5MzRRNjNBa3QzS3hhSktzIl19LCJfc2QiOlsiME9nMi1ReG95eW1UOGNnVzZZUjVSSFpQLUJuR2tHUi1NM2otLV92RWlzSSIsIkcwZ3lHNnExVFMyUlQxMkZ3X2RRRDVVcjlZc1AwZlVWOXVtQWdGMC1jQ1EiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.ggEyE4SeDO2Hu3tol3VLmi7NQj56yKzKQDaafocgkLrUBdivghohtzrfcbrMN7CRufJ_Cnh0EL54kymXLGTdDQ~WyIwNGU0MjAzOWU4ZWFiOWRjIiwiYSIsIjEiXQ~WyIwOGE1Yjc5MjMyYjAzYzBhIiwiMSJd~WyJiNWE2YjUzZGQwYTFmMGIwIiwienp6IiwieHh4Il0~WyIxYzdmOTE4ZTE0MjA2NzZiIiwiZm9vIiwiYmFyIl0~WyJmZjYxYzQ5ZGU2NjFiYzMxIiwiYXJyIixbeyIuLi4iOiJTSG96VW5KNUpkd0ZtTjVCbXB5dXZCWGZfZWRjckVvcExPYThTVlBFUmg0In0sIjIiLHsiX3NkIjpbIkpuODNhZkp0OGx4NG1FMzZpRkZyS2U2R2VnN0dlVUQ4Z3UwdVo3NnRZcW8iXX1dXQ~';
167
+ const decodedSdJwt = decodeSdJwtSync(sdjwt, digest);
168
+ const claims = getClaimsSync(
169
+ decodedSdJwt.jwt.payload,
170
+ decodedSdJwt.disclosures,
171
+ digest,
172
+ );
173
+ expect(claims).toStrictEqual({
174
+ foo: 'bar',
175
+ arr: ['1', '2', { a: '1' }],
176
+ test: {
177
+ zzz: 'xxx',
178
+ },
179
+ });
180
+ });
181
+
182
+ test('Test default sd hash algorithm', () => {
183
+ const { _sd_alg } = getSDAlgAndPayload({});
184
+ expect(_sd_alg).toBe('sha-256');
185
+ });
186
+
187
+ test('unpackObj clone', async () => {
188
+ const sdjwt =
189
+ 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJ0ZXN0Ijp7Il9zZCI6WyJqVEszMHNleDZhYV9kUk1KSWZDR056Q0FwbVB5MzRRNjNBa3QzS3hhSktzIl19LCJfc2QiOlsiME9nMi1ReG95eW1UOGNnVzZZUjVSSFpQLUJuR2tHUi1NM2otLV92RWlzSSIsIkcwZ3lHNnExVFMyUlQxMkZ3X2RRRDVVcjlZc1AwZlVWOXVtQWdGMC1jQ1EiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.ggEyE4SeDO2Hu3tol3VLmi7NQj56yKzKQDaafocgkLrUBdivghohtzrfcbrMN7CRufJ_Cnh0EL54kymXLGTdDQ~WyIwNGU0MjAzOWU4ZWFiOWRjIiwiYSIsIjEiXQ~WyIwOGE1Yjc5MjMyYjAzYzBhIiwiMSJd~WyJiNWE2YjUzZGQwYTFmMGIwIiwienp6IiwieHh4Il0~WyIxYzdmOTE4ZTE0MjA2NzZiIiwiZm9vIiwiYmFyIl0~WyJmZjYxYzQ5ZGU2NjFiYzMxIiwiYXJyIixbeyIuLi4iOiJTSG96VW5KNUpkd0ZtTjVCbXB5dXZCWGZfZWRjckVvcExPYThTVlBFUmg0In0sIjIiLHsiX3NkIjpbIkpuODNhZkp0OGx4NG1FMzZpRkZyS2U2R2VnN0dlVUQ4Z3UwdVo3NnRZcW8iXX1dXQ~';
190
+ const decodedSdJwt = await decodeSdJwt(sdjwt, digest);
191
+ const jwtPayload = JSON.parse(JSON.stringify(decodedSdJwt.jwt.payload));
192
+
193
+ const { _sd_alg } = getSDAlgAndPayload(decodedSdJwt.jwt.payload);
194
+ const hash = { hasher: digest, alg: _sd_alg };
195
+ const map = await createHashMapping(decodedSdJwt.disclosures, hash);
196
+
197
+ const { unpackedObj } = unpackObj(decodedSdJwt.jwt.payload, map);
198
+
199
+ expect(unpackedObj).toBeDefined();
200
+ expect(jwtPayload).toStrictEqual(decodedSdJwt.jwt.payload);
201
+ });
202
+ });
@@ -0,0 +1,30 @@
1
+ import { hasher as digest, generateSalt } from '@owf/crypto';
2
+ import { describe, expect, test } from 'vitest';
3
+ import { createDecoy } from '../decoy';
4
+ import { base64urlEncode } from '../utils';
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
+ });