@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/package.json CHANGED
@@ -1,50 +1,62 @@
1
1
  {
2
- "name": "@sd-jwt/core",
3
- "version": "0.1.0",
4
- "description": "Core implementation of sd-jwt Draft 06 and sd-jwt-vc Draft 01",
5
- "license": "(MIT OR Apache-2.0)",
6
- "author": "Berend Sliedrecht <sliedrecht@berend.io>",
7
- "readme": "README.md",
8
- "keywords": [
9
- "jwt",
10
- "jwt-sd",
11
- "sd-jwt",
12
- "sd-jwt-vc",
13
- "decentralized-identity",
14
- "ssi",
15
- "oauth",
16
- "oauth2",
17
- "openid-connect"
18
- ],
19
- "repository": {
20
- "url": "https://github.com/berendsliedrecht/sd-jwt-ts",
21
- "type": "git"
22
- },
23
- "homepage": "https://github.com/berendsliedrecht/sd-jwt-ts",
24
- "bugs": {
25
- "url": "https://github.com/berendsliedrecht/sd-jwt-ts/issues",
26
- "email": "sliedrecht@berend.io"
27
- },
28
- "publishConfig": {
29
- "access": "public"
30
- },
31
- "main": "build/index.js",
32
- "files": [
33
- "build",
34
- "LICENSE-MIT",
35
- "LICENSE-APACHE-2.0"
36
- ],
37
- "scripts": {
38
- "build": "tsc",
39
- "test": "node --require ts-node/register --test ./tests/*.test.ts",
40
- "release": "release-it"
41
- },
42
- "dependencies": {
43
- "buffer": "*"
44
- },
45
- "devDependencies": {
46
- "@types/node": "*",
47
- "ts-node": "*",
48
- "typescript": "*"
2
+ "name": "@sd-jwt/core",
3
+ "version": "0.1.1-next.0+13a269e",
4
+ "description": "sd-jwt draft 7 implementation in typescript",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js"
49
12
  }
13
+ },
14
+ "scripts": {
15
+ "build": "rm -rf **/dist && tsup",
16
+ "lint": "biome lint ./src",
17
+ "test": "pnpm run test:node && pnpm run test:browser && pnpm run test:cov",
18
+ "test:node": "vitest run ./src/test/*.spec.ts",
19
+ "test:browser": "vitest run ./src/test/*.spec.ts --environment jsdom",
20
+ "test:cov": "vitest run --coverage"
21
+ },
22
+ "keywords": [
23
+ "sd-jwt",
24
+ "sdjwt",
25
+ "sd-jwt-vc"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "repository": {
31
+ "url": "https://github.com/openwallet-foundation/sd-jwt-js"
32
+ },
33
+ "author": "Lukas.J.Han <lukas.j.han@gmail.com>",
34
+ "homepage": "https://github.com/openwallet-foundation/sd-jwt-js/wiki",
35
+ "bugs": {
36
+ "url": "https://github.com/openwallet-foundation/sd-jwt-js/issues"
37
+ },
38
+ "license": "Apache-2.0",
39
+ "devDependencies": {
40
+ "@owf/crypto": "^0.1.0-alpha-20260312123226"
41
+ },
42
+ "dependencies": {
43
+ "@owf/identity-common": "^0.1.0-alpha-20260312123226"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "tsup": {
49
+ "entry": [
50
+ "./src/index.ts"
51
+ ],
52
+ "sourceMap": true,
53
+ "splitting": false,
54
+ "clean": true,
55
+ "dts": true,
56
+ "format": [
57
+ "cjs",
58
+ "esm"
59
+ ]
60
+ },
61
+ "gitHead": "13a269ea5d9459fee02dd65de8d13361d94402d2"
50
62
  }
@@ -0,0 +1,376 @@
1
+ import {
2
+ type Hasher,
3
+ type HasherAndAlg,
4
+ type HasherAndAlgSync,
5
+ type HasherSync,
6
+ IANA_HASH_ALGORITHMS,
7
+ SD_DIGEST,
8
+ SD_LIST_KEY,
9
+ SD_SEPARATOR,
10
+ } from '../types';
11
+ import { Disclosure, SDJWTException } from '../utils';
12
+ import { decodeBase64urlJsonStrict } from '../utils/strict-json';
13
+
14
+ export const decodeJwt = <
15
+ H extends Record<string, unknown>,
16
+ T extends Record<string, unknown>,
17
+ >(
18
+ jwt: string,
19
+ ): { header: H; payload: T; signature: string } => {
20
+ const { 0: header, 1: payload, 2: signature, length } = jwt.split('.');
21
+ if (length !== 3) {
22
+ throw new SDJWTException('Invalid JWT as input');
23
+ }
24
+
25
+ return {
26
+ header: decodeBase64urlJsonStrict(header, 'Invalid JWT as input'),
27
+ payload: decodeBase64urlJsonStrict(payload, 'Invalid JWT as input'),
28
+ signature: signature,
29
+ };
30
+ };
31
+
32
+ // Split the sdjwt into 3 parts: jwt, disclosures and keybinding jwt. each part is base64url encoded
33
+ // It's separated by the ~ character
34
+ //
35
+ // If there is no keybinding jwt, the third part will be undefined
36
+ // If there are no disclosures, the second part will be an empty array
37
+ export const splitSdJwt = (
38
+ sdjwt: string,
39
+ ): { jwt: string; disclosures: string[]; kbJwt?: string } => {
40
+ const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
41
+ if (encodedDisclosures.length === 0) {
42
+ // if input is just jwt, then return here.
43
+ // This is for compatibility with jwt
44
+ return {
45
+ jwt: encodedJwt,
46
+ disclosures: [],
47
+ };
48
+ }
49
+
50
+ const encodedKeyBindingJwt = encodedDisclosures.pop();
51
+ return {
52
+ jwt: encodedJwt,
53
+ disclosures: encodedDisclosures,
54
+ kbJwt: encodedKeyBindingJwt || undefined,
55
+ };
56
+ };
57
+
58
+ // Decode the sdjwt into the jwt, disclosures and keybinding jwt
59
+ // jwt, disclosures and keybinding jwt are also decoded
60
+ export const decodeSdJwt = async (
61
+ sdjwt: string,
62
+ hasher: Hasher,
63
+ ): Promise<DecodedSDJwt> => {
64
+ const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
65
+ const jwt = decodeJwt(encodedJwt);
66
+
67
+ if (encodedDisclosures.length === 0) {
68
+ // if input is just jwt, then return here.
69
+ // This is for compatibility with jwt
70
+ return {
71
+ jwt,
72
+ disclosures: [],
73
+ };
74
+ }
75
+
76
+ const encodedKeyBindingJwt = encodedDisclosures.pop();
77
+ const kbJwt = encodedKeyBindingJwt
78
+ ? decodeJwt(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
+ export const decodeSdJwtSync = (
97
+ sdjwt: string,
98
+ hasher: HasherSync,
99
+ ): DecodedSDJwt => {
100
+ const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
101
+ const jwt = decodeJwt(encodedJwt);
102
+
103
+ if (encodedDisclosures.length === 0) {
104
+ // if input is just jwt, then return here.
105
+ // This is for compatibility with jwt
106
+ return {
107
+ jwt,
108
+ disclosures: [],
109
+ };
110
+ }
111
+
112
+ const encodedKeyBindingJwt = encodedDisclosures.pop();
113
+ const kbJwt = encodedKeyBindingJwt
114
+ ? decodeJwt(encodedKeyBindingJwt)
115
+ : undefined;
116
+
117
+ const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
118
+
119
+ const disclosures = encodedDisclosures.map((ed) =>
120
+ Disclosure.fromEncodeSync(ed, { alg: _sd_alg, hasher }),
121
+ );
122
+
123
+ return {
124
+ jwt,
125
+ disclosures,
126
+ kbJwt,
127
+ };
128
+ };
129
+
130
+ // Get the claims from jwt and disclosures
131
+ // The digested values are matched with the disclosures and the claims are extracted
132
+ export const getClaims = async <T = Record<string, unknown>>(
133
+ rawPayload: Record<string, unknown>,
134
+ disclosures: Array<Disclosure>,
135
+ hasher: Hasher,
136
+ ): Promise<T> => {
137
+ const { unpackedObj } = await unpack(rawPayload, disclosures, hasher);
138
+ // The caller supplies T to match their expected shape
139
+ return unpackedObj as T;
140
+ };
141
+
142
+ export const getClaimsSync = <T = Record<string, unknown>>(
143
+ rawPayload: Record<string, unknown>,
144
+ disclosures: Array<Disclosure>,
145
+ hasher: HasherSync,
146
+ ): T => {
147
+ const { unpackedObj } = unpackSync(rawPayload, disclosures, hasher);
148
+ // The caller supplies T to match their expected shape
149
+ return unpackedObj as T;
150
+ };
151
+
152
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
153
+ typeof v === 'object' && v !== null && !Array.isArray(v);
154
+
155
+ const unpackArray = (
156
+ arr: Array<unknown>,
157
+ map: Record<string, Disclosure>,
158
+ prefix = '',
159
+ seenDigests?: Set<string>,
160
+ ): { unpackedObj: unknown; disclosureKeymap: Record<string, string> } => {
161
+ const keys: Record<string, string> = {};
162
+ const unpackedArray: unknown[] = [];
163
+ arr.forEach((item, idx) => {
164
+ if (isRecord(item)) {
165
+ const hash = item[SD_LIST_KEY];
166
+ if (typeof hash === 'string') {
167
+ // RFC 9901 Section 7.1 step 4: reject duplicate digests
168
+ if (seenDigests) {
169
+ if (seenDigests.has(hash)) {
170
+ throw new SDJWTException(
171
+ 'Duplicate digest found in SD-JWT payload',
172
+ );
173
+ }
174
+ seenDigests.add(hash);
175
+ }
176
+ const disclosed = map[hash];
177
+ if (disclosed) {
178
+ const presentKey = prefix ? `${prefix}.${idx}` : `${idx}`;
179
+ keys[presentKey] = hash;
180
+
181
+ const { unpackedObj, disclosureKeymap: disclosureKeys } =
182
+ unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
183
+ unpackedArray.push(unpackedObj);
184
+ Object.assign(keys, disclosureKeys);
185
+ }
186
+ } else {
187
+ const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
188
+ const { unpackedObj, disclosureKeymap: disclosureKeys } =
189
+ unpackObjInternal(item, map, newKey, seenDigests);
190
+ unpackedArray.push(unpackedObj);
191
+ Object.assign(keys, disclosureKeys);
192
+ }
193
+ } else if (Array.isArray(item)) {
194
+ const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
195
+ const { unpackedObj, disclosureKeymap: disclosureKeys } =
196
+ unpackObjInternal(item, map, newKey, seenDigests);
197
+ unpackedArray.push(unpackedObj);
198
+ Object.assign(keys, disclosureKeys);
199
+ } else {
200
+ unpackedArray.push(item);
201
+ }
202
+ });
203
+ return { unpackedObj: unpackedArray, disclosureKeymap: keys };
204
+ };
205
+
206
+ export const unpackObj = (obj: unknown, map: Record<string, Disclosure>) => {
207
+ const copiedObj = JSON.parse(JSON.stringify(obj));
208
+ const seenDigests = new Set<string>();
209
+ const result = unpackObjInternal(copiedObj, map, '', seenDigests);
210
+
211
+ // RFC 9901 Section 7.1 step 5: reject unreferenced disclosures
212
+ const mapDigests = Object.keys(map);
213
+ const unusedDigests = mapDigests.filter((d) => !seenDigests.has(d));
214
+ if (unusedDigests.length > 0) {
215
+ throw new SDJWTException('Unreferenced disclosure(s) detected in SD-JWT');
216
+ }
217
+
218
+ return result;
219
+ };
220
+
221
+ const unpackObjInternal = (
222
+ obj: unknown,
223
+ map: Record<string, Disclosure>,
224
+ prefix = '',
225
+ seenDigests?: Set<string>,
226
+ ): { unpackedObj: unknown; disclosureKeymap: Record<string, string> } => {
227
+ const keys: Record<string, string> = {};
228
+ if (typeof obj === 'object' && obj !== null) {
229
+ if (Array.isArray(obj)) {
230
+ return unpackArray(obj, map, prefix, seenDigests);
231
+ }
232
+
233
+ const record = obj as Record<string, unknown>;
234
+ for (const key in record) {
235
+ if (
236
+ key !== SD_DIGEST &&
237
+ key !== SD_LIST_KEY &&
238
+ typeof record[key] === 'object'
239
+ ) {
240
+ const newKey = prefix ? `${prefix}.${key}` : key;
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) {
253
+ for (const hash of _sd) {
254
+ // RFC 9901 Section 7.1 step 4: reject duplicate digests
255
+ if (seenDigests) {
256
+ if (seenDigests.has(hash)) {
257
+ throw new SDJWTException(
258
+ 'Duplicate digest found in SD-JWT payload',
259
+ );
260
+ }
261
+ seenDigests.add(hash);
262
+ }
263
+ const disclosed = map[hash];
264
+ if (disclosed?.key) {
265
+ // RFC 9901 Section 7.1 step 3c.ii.3: reject if claim name already exists
266
+ if (disclosed.key in payload) {
267
+ throw new SDJWTException(
268
+ `Disclosed claim name "${disclosed.key}" conflicts with existing payload key`,
269
+ );
270
+ }
271
+
272
+ const presentKey = prefix
273
+ ? `${prefix}.${disclosed.key}`
274
+ : disclosed.key;
275
+ keys[presentKey] = hash;
276
+
277
+ const { unpackedObj, disclosureKeymap: disclosureKeys } =
278
+ unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
279
+ claims[disclosed.key] = unpackedObj;
280
+ Object.assign(keys, disclosureKeys);
281
+ }
282
+ }
283
+ }
284
+
285
+ const unpackedObj = Object.assign(payload, claims);
286
+ return { unpackedObj, disclosureKeymap: keys };
287
+ }
288
+ return { unpackedObj: obj, disclosureKeymap: keys };
289
+ };
290
+
291
+ // Creates a mapping of the digests of the disclosures to the actual disclosures
292
+ export const createHashMapping = async (
293
+ disclosures: Array<Disclosure>,
294
+ hash: HasherAndAlg,
295
+ ) => {
296
+ const map: Record<string, Disclosure> = {};
297
+ for (let i = 0; i < disclosures.length; i++) {
298
+ const disclosure = disclosures[i];
299
+ const digest = await disclosure.digest(hash);
300
+ map[digest] = disclosure;
301
+ }
302
+ return map;
303
+ };
304
+
305
+ export const createHashMappingSync = (
306
+ disclosures: Array<Disclosure>,
307
+ hash: HasherAndAlgSync,
308
+ ) => {
309
+ const map: Record<string, Disclosure> = {};
310
+ for (let i = 0; i < disclosures.length; i++) {
311
+ const disclosure = disclosures[i];
312
+ const digest = disclosure.digestSync(hash);
313
+ map[digest] = disclosure;
314
+ }
315
+ return map;
316
+ };
317
+
318
+ // Extract _sd_alg. If it is not present, it is assumed to be sha-256
319
+ export const getSDAlgAndPayload = (SdJwtPayload: Record<string, unknown>) => {
320
+ const { _sd_alg, ...payload } = SdJwtPayload;
321
+ if (typeof _sd_alg !== 'string') {
322
+ // This is for compatibility
323
+ return { _sd_alg: 'sha-256', payload };
324
+ }
325
+ if (
326
+ !IANA_HASH_ALGORITHMS.includes(
327
+ _sd_alg as (typeof IANA_HASH_ALGORITHMS)[number],
328
+ )
329
+ ) {
330
+ throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
331
+ }
332
+ return { _sd_alg, payload };
333
+ };
334
+
335
+ // Match the digests of the disclosures with the claims and extract the claims
336
+ // unpack function use unpackObjInternal and unpackArray to recursively unpack the claims
337
+ // Since getSDAlgAndPayload create new object So we don't need to clone it again
338
+ export const unpack = async (
339
+ SdJwtPayload: Record<string, unknown>,
340
+ disclosures: Array<Disclosure>,
341
+ hasher: Hasher,
342
+ ) => {
343
+ const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
344
+ const hash = { hasher, alg: _sd_alg };
345
+ const map = await createHashMapping(disclosures, hash);
346
+
347
+ return unpackObj(payload, map);
348
+ };
349
+
350
+ export const unpackSync = (
351
+ SdJwtPayload: Record<string, unknown>,
352
+ disclosures: Array<Disclosure>,
353
+ hasher: HasherSync,
354
+ ) => {
355
+ const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
356
+ const hash = { hasher, alg: _sd_alg };
357
+ const map = createHashMappingSync(disclosures, hash);
358
+
359
+ return unpackObj(payload, map);
360
+ };
361
+
362
+ // This is the type of the object that is returned by the decodeSdJwt function
363
+ // It is a combination of the decoded jwt, the disclosures and the keybinding jwt
364
+ export type DecodedSDJwt = {
365
+ jwt: {
366
+ header: Record<string, unknown>;
367
+ payload: Record<string, unknown>; // raw payload of sd-jwt
368
+ signature: string;
369
+ };
370
+ disclosures: Array<Disclosure>;
371
+ kbJwt?: {
372
+ header: Record<string, unknown>;
373
+ payload: Record<string, unknown>;
374
+ signature: string;
375
+ };
376
+ };
@@ -0,0 +1 @@
1
+ export * from './decode';
package/src/decoy.ts ADDED
@@ -0,0 +1,15 @@
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
+ };
@@ -0,0 +1,99 @@
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
+ }