@sd-jwt/core 0.19.1-next.0 → 0.19.1-next.10

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