@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.
- package/LICENSE +2 -2
- package/README.md +82 -80
- package/dist/index.cjs +1526 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +696 -0
- package/dist/index.d.mts +537 -527
- package/dist/index.mjs +1422 -1998
- package/dist/index.mjs.map +1 -0
- package/package.json +29 -49
- package/CHANGELOG.md +0 -39
- package/dist/index.d.ts +0 -686
- package/dist/index.js +0 -2100
- package/src/decode/decode.ts +0 -407
- package/src/decode/index.ts +0 -1
- package/src/decoy.ts +0 -15
- package/src/flattenJSON.ts +0 -99
- package/src/generalJSON.ts +0 -157
- package/src/index.ts +0 -1000
- package/src/jwt.ts +0 -286
- package/src/kbjwt.ts +0 -101
- package/src/present/index.ts +0 -1
- package/src/present/present.ts +0 -216
- package/src/sdjwt.ts +0 -384
- package/src/test/decode/decode.spec.ts +0 -197
- package/src/test/decoy.spec.ts +0 -30
- package/src/test/flattenJSON.spec.ts +0 -122
- package/src/test/generalJSON.spec.ts +0 -198
- package/src/test/index.spec.ts +0 -986
- package/src/test/jwt.spec.ts +0 -301
- package/src/test/kbjwt.spec.ts +0 -555
- package/src/test/pass.spec.ts +0 -7
- package/src/test/present/present.spec.ts +0 -305
- package/src/test/sdjwt.spec.ts +0 -384
- package/src/test/types/type.spec.ts +0 -88
- package/src/test/utils/base64url.spec.ts +0 -33
- package/src/test/utils/disclosure.spec.ts +0 -170
- package/src/test/utils/error.spec.ts +0 -15
- package/src/types/index.ts +0 -2
- package/src/types/type.ts +0 -270
- package/src/types/verification-error.ts +0 -55
- package/src/utils/base64url.ts +0 -6
- package/src/utils/disclosure.ts +0 -121
- package/src/utils/error.ts +0 -25
- package/src/utils/index.ts +0 -3
- package/src/utils/strict-json.ts +0 -17
- package/test/app-e2e.spec.ts +0 -292
- package/test/array_data_types.json +0 -31
- package/test/array_full_sd.json +0 -21
- package/test/array_in_sd.json +0 -13
- package/test/array_nested_in_plain.json +0 -29
- package/test/array_none_disclosed.json +0 -17
- package/test/array_of_nulls.json +0 -15
- package/test/array_of_objects.json +0 -65
- package/test/array_of_scalars.json +0 -19
- package/test/array_recursive_sd.json +0 -35
- package/test/array_recursive_sd_some_disclosed.json +0 -63
- package/test/complex.json +0 -43
- package/test/header_mod.json +0 -48
- package/test/json_serialization.json +0 -48
- package/test/key_binding.json +0 -48
- package/test/no_sd.json +0 -36
- package/test/object_data_types.json +0 -62
- package/test/recursions.json +0 -117
- package/test/rfc9901-audit-fixes.spec.ts +0 -252
- package/test/rfc9901-validation.spec.ts +0 -150
- package/test/tsconfig.json +0 -4
- package/tsconfig.json +0 -7
- package/vitest.config.mts +0 -4
package/src/decode/decode.ts
DELETED
|
@@ -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
|
-
};
|
package/src/decode/index.ts
DELETED
|
@@ -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
|
-
};
|
package/src/flattenJSON.ts
DELETED
|
@@ -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
|
-
}
|
package/src/generalJSON.ts
DELETED
|
@@ -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
|
-
}
|