@sd-jwt/core
Version:
sd-jwt draft 7 implementation in typescript
1,511 lines (1,496 loc) • 61.1 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __objRest = (source, exclude) => {
var target = {};
for (var prop in source)
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
target[prop] = source[prop];
if (source != null && __getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(source)) {
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
target[prop] = source[prop];
}
return target;
};
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/types/type.ts
var SD_SEPARATOR = "~";
var SD_LIST_KEY = "...";
var SD_DIGEST = "_sd";
var SD_DECOY = "_sd_decoy";
var KB_JWT_TYP = "kb+jwt";
var IANA_HASH_ALGORITHMS = [
"sha-256",
"sha-256-128",
"sha-256-120",
"sha-256-96",
"sha-256-64",
"sha-256-32",
"sha-384",
"sha-512",
"sha3-224",
"sha3-256",
"sha3-384",
"sha3-512",
"blake2s-256",
"blake2b-256",
"blake2b-512",
"k12-256",
"k12-512"
];
// src/utils/base64url.ts
import {
base64UrlToUint8Array,
base64urlDecode,
base64urlEncode,
uint8ArrayToBase64Url
} from "@owf/identity-common";
// src/utils/error.ts
var SDJWTException = class _SDJWTException extends Error {
constructor(message, details) {
super(message);
Object.setPrototypeOf(this, _SDJWTException.prototype);
this.name = "SDJWTException";
this.details = details;
}
getFullMessage() {
return `${this.name}: ${this.message} ${this.details ? `- ${JSON.stringify(this.details)}` : ""}`;
}
};
function ensureError(value) {
if (value instanceof Error) return value;
if (typeof value === "string") return new Error(value);
return new Error(String(value));
}
// src/utils/strict-json.ts
var utf8Decoder = new TextDecoder("utf-8", { fatal: true });
var decodeBase64urlJsonStrict = (encoded, errorMessage) => {
try {
const bytes = base64UrlToUint8Array(encoded);
const decoded = utf8Decoder.decode(bytes);
return JSON.parse(decoded);
} catch (e) {
throw new SDJWTException(errorMessage);
}
};
// src/utils/disclosure.ts
var Disclosure = class _Disclosure {
constructor(data, _meta) {
this._digest = _meta == null ? void 0 : _meta.digest;
this._encoded = _meta == null ? void 0 : _meta.encoded;
if (data.length === 2) {
this.salt = data[0];
this.value = data[1];
return;
}
if (data.length === 3) {
this.salt = data[0];
this.key = data[1];
this.value = data[2];
return;
}
throw new SDJWTException("Invalid disclosure data");
}
// We need to digest of the original encoded data.
// After decode process, we use JSON.stringify to encode the data.
// This can be different from the original encoded data.
static fromEncode(s, hash) {
return __async(this, null, function* () {
const { hasher, alg } = hash;
const digest = yield hasher(s, alg);
const digestStr = uint8ArrayToBase64Url(digest);
const item = decodeBase64urlJsonStrict(
s,
"Invalid disclosure data"
);
return _Disclosure.fromArray(item, { digest: digestStr, encoded: s });
});
}
static fromEncodeSync(s, hash) {
const { hasher, alg } = hash;
const digest = hasher(s, alg);
const digestStr = uint8ArrayToBase64Url(digest);
const item = decodeBase64urlJsonStrict(
s,
"Invalid disclosure data"
);
return _Disclosure.fromArray(item, { digest: digestStr, encoded: s });
}
static fromArray(item, _meta) {
return new _Disclosure(item, _meta);
}
encode() {
if (!this._encoded) {
this._encoded = base64urlEncode(JSON.stringify(this.decode()));
}
return this._encoded;
}
decode() {
return this.key ? [this.salt, this.key, this.value] : [this.salt, this.value];
}
digest(hash) {
return __async(this, null, function* () {
const { hasher, alg } = hash;
if (!this._digest) {
const hash2 = yield hasher(this.encode(), alg);
this._digest = uint8ArrayToBase64Url(hash2);
}
return this._digest;
});
}
digestSync(hash) {
const { hasher, alg } = hash;
if (!this._digest) {
const hash2 = hasher(this.encode(), alg);
this._digest = uint8ArrayToBase64Url(hash2);
}
return this._digest;
}
};
// src/decode/decode.ts
var decodeJwt = (jwt) => {
const { 0: header, 1: payload, 2: signature, length } = jwt.split(".");
if (length !== 3) {
throw new SDJWTException("Invalid JWT as input");
}
return {
header: decodeBase64urlJsonStrict(header, "Invalid JWT as input"),
payload: decodeBase64urlJsonStrict(payload, "Invalid JWT as input"),
signature
};
};
var splitSdJwt = (sdjwt) => {
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
if (encodedDisclosures.length === 0) {
return {
jwt: encodedJwt,
disclosures: []
};
}
const encodedKeyBindingJwt = encodedDisclosures.pop();
return {
jwt: encodedJwt,
disclosures: encodedDisclosures,
kbJwt: encodedKeyBindingJwt || void 0
};
};
var decodeSdJwt = (sdjwt, hasher) => __async(null, null, function* () {
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
const jwt = decodeJwt(encodedJwt);
if (encodedDisclosures.length === 0) {
return {
jwt,
disclosures: []
};
}
const encodedKeyBindingJwt = encodedDisclosures.pop();
const kbJwt = encodedKeyBindingJwt ? decodeJwt(encodedKeyBindingJwt) : void 0;
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
const disclosures = yield Promise.all(
encodedDisclosures.map(
(ed) => Disclosure.fromEncode(ed, { alg: _sd_alg, hasher })
)
);
return {
jwt,
disclosures,
kbJwt
};
});
var decodeSdJwtSync = (sdjwt, hasher) => {
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
const jwt = decodeJwt(encodedJwt);
if (encodedDisclosures.length === 0) {
return {
jwt,
disclosures: []
};
}
const encodedKeyBindingJwt = encodedDisclosures.pop();
const kbJwt = encodedKeyBindingJwt ? decodeJwt(encodedKeyBindingJwt) : void 0;
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
const disclosures = encodedDisclosures.map(
(ed) => Disclosure.fromEncodeSync(ed, { alg: _sd_alg, hasher })
);
return {
jwt,
disclosures,
kbJwt
};
};
var getClaims = (rawPayload, disclosures, hasher) => __async(null, null, function* () {
const { unpackedObj } = yield unpack(rawPayload, disclosures, hasher);
return unpackedObj;
});
var getClaimsSync = (rawPayload, disclosures, hasher) => {
const { unpackedObj } = unpackSync(rawPayload, disclosures, hasher);
return unpackedObj;
};
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
var unpackArray = (arr, map, prefix = "", seenDigests) => {
const keys = {};
const unpackedArray = [];
arr.forEach((item, idx) => {
if (isRecord(item)) {
const hash = item[SD_LIST_KEY];
if (typeof hash === "string") {
if (seenDigests) {
if (seenDigests.has(hash)) {
throw new SDJWTException(
"Duplicate digest found in SD-JWT payload"
);
}
seenDigests.add(hash);
}
const disclosed = map[hash];
if (disclosed) {
const presentKey = prefix ? `${prefix}.${idx}` : `${idx}`;
keys[presentKey] = hash;
const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
unpackedArray.push(unpackedObj);
Object.assign(keys, disclosureKeys);
}
} else {
const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObjInternal(item, map, newKey, seenDigests);
unpackedArray.push(unpackedObj);
Object.assign(keys, disclosureKeys);
}
} else if (Array.isArray(item)) {
const newKey = prefix ? `${prefix}.${idx}` : `${idx}`;
const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObjInternal(item, map, newKey, seenDigests);
unpackedArray.push(unpackedObj);
Object.assign(keys, disclosureKeys);
} else {
unpackedArray.push(item);
}
});
return { unpackedObj: unpackedArray, disclosureKeymap: keys };
};
var unpackObj = (obj, map) => {
const copiedObj = JSON.parse(JSON.stringify(obj));
const seenDigests = /* @__PURE__ */ new Set();
const result = unpackObjInternal(copiedObj, map, "", seenDigests);
const mapDigests = Object.keys(map);
const unusedDigests = mapDigests.filter((d) => !seenDigests.has(d));
if (unusedDigests.length > 0) {
throw new SDJWTException("Unreferenced disclosure(s) detected in SD-JWT");
}
return result;
};
var unpackObjInternal = (obj, map, prefix = "", seenDigests) => {
const keys = {};
if (typeof obj === "object" && obj !== null) {
if (Array.isArray(obj)) {
return unpackArray(obj, map, prefix, seenDigests);
}
const record = obj;
for (const key in record) {
if (key !== SD_DIGEST && key !== SD_LIST_KEY && typeof record[key] === "object") {
const newKey = prefix ? `${prefix}.${key}` : key;
const { unpackedObj: unpackedObj2, disclosureKeymap: disclosureKeys } = unpackObjInternal(record[key], map, newKey, seenDigests);
record[key] = unpackedObj2;
Object.assign(keys, disclosureKeys);
}
}
const _a = record, { _sd } = _a, payload = __objRest(_a, ["_sd"]);
const claims = {};
if (_sd) {
for (const hash of _sd) {
if (seenDigests) {
if (seenDigests.has(hash)) {
throw new SDJWTException(
"Duplicate digest found in SD-JWT payload"
);
}
seenDigests.add(hash);
}
const disclosed = map[hash];
if (disclosed == null ? void 0 : disclosed.key) {
if (disclosed.key in payload) {
throw new SDJWTException(
`Disclosed claim name "${disclosed.key}" conflicts with existing payload key`
);
}
const presentKey = prefix ? `${prefix}.${disclosed.key}` : disclosed.key;
keys[presentKey] = hash;
const { unpackedObj: unpackedObj2, disclosureKeymap: disclosureKeys } = unpackObjInternal(disclosed.value, map, presentKey, seenDigests);
claims[disclosed.key] = unpackedObj2;
Object.assign(keys, disclosureKeys);
}
}
}
const unpackedObj = Object.assign(payload, claims);
return { unpackedObj, disclosureKeymap: keys };
}
return { unpackedObj: obj, disclosureKeymap: keys };
};
var createHashMapping = (disclosures, hash) => __async(null, null, function* () {
const map = {};
for (let i = 0; i < disclosures.length; i++) {
const disclosure = disclosures[i];
const digest = yield disclosure.digest(hash);
map[digest] = disclosure;
}
return map;
});
var createHashMappingSync = (disclosures, hash) => {
const map = {};
for (let i = 0; i < disclosures.length; i++) {
const disclosure = disclosures[i];
const digest = disclosure.digestSync(hash);
map[digest] = disclosure;
}
return map;
};
var getSDAlgAndPayload = (SdJwtPayload) => {
const _a = SdJwtPayload, { _sd_alg } = _a, payload = __objRest(_a, ["_sd_alg"]);
if (typeof _sd_alg !== "string") {
return { _sd_alg: "sha-256", payload };
}
if (!IANA_HASH_ALGORITHMS.includes(
_sd_alg
)) {
throw new SDJWTException(`Invalid _sd_alg: ${_sd_alg}`);
}
return { _sd_alg, payload };
};
var unpack = (SdJwtPayload, disclosures, hasher) => __async(null, null, function* () {
const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
const hash = { hasher, alg: _sd_alg };
const map = yield createHashMapping(disclosures, hash);
return unpackObj(payload, map);
});
var unpackSync = (SdJwtPayload, disclosures, hasher) => {
const { _sd_alg, payload } = getSDAlgAndPayload(SdJwtPayload);
const hash = { hasher, alg: _sd_alg };
const map = createHashMappingSync(disclosures, hash);
return unpackObj(payload, map);
};
// src/flattenJSON.ts
var FlattenJSON = class _FlattenJSON {
constructor(data) {
this.disclosures = data.disclosures;
this.kb_jwt = data.kb_jwt;
this.payload = data.jwtData.payload;
this.signature = data.jwtData.signature;
this.protected = data.jwtData.protected;
}
static fromEncode(encodedSdJwt) {
const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);
const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split(".");
if (!protectedHeader || !payload || !signature) {
throw new SDJWTException("Invalid JWT");
}
return new _FlattenJSON({
jwtData: {
protected: protectedHeader,
payload,
signature
},
disclosures,
kb_jwt: kbJwt
});
}
static fromSerialized(json) {
return new _FlattenJSON({
jwtData: {
protected: json.protected,
payload: json.payload,
signature: json.signature
},
disclosures: json.header.disclosures,
kb_jwt: json.header.kb_jwt
});
}
toJson() {
return {
payload: this.payload,
signature: this.signature,
protected: this.protected,
header: {
disclosures: this.disclosures,
kb_jwt: this.kb_jwt
}
};
}
toEncoded() {
var _a;
const data = [];
const jwt = `${this.protected}.${this.payload}.${this.signature}`;
data.push(jwt);
if (this.disclosures && this.disclosures.length > 0) {
const disclosures = this.disclosures.join(SD_SEPARATOR);
data.push(disclosures);
}
const kb_jwt = (_a = this.kb_jwt) != null ? _a : "";
data.push(kb_jwt);
return data.join(SD_SEPARATOR);
}
};
// src/generalJSON.ts
var GeneralJSON = class _GeneralJSON {
constructor(data) {
this.payload = data.payload;
this.disclosures = data.disclosures;
this.kb_jwt = data.kb_jwt;
this.signatures = data.signatures;
}
static fromEncode(encodedSdJwt) {
const { jwt, disclosures, kbJwt } = splitSdJwt(encodedSdJwt);
const { 0: protectedHeader, 1: payload, 2: signature } = jwt.split(".");
if (!protectedHeader || !payload || !signature) {
throw new SDJWTException("Invalid JWT");
}
return new _GeneralJSON({
payload,
disclosures,
kb_jwt: kbJwt,
signatures: [
{
protected: protectedHeader,
signature
}
]
});
}
static fromSerialized(json) {
var _a, _b, _c;
if (!json.signatures[0]) {
throw new SDJWTException("Invalid JSON");
}
const disclosures = (_b = (_a = json.signatures[0].header) == null ? void 0 : _a.disclosures) != null ? _b : [];
const kb_jwt = (_c = json.signatures[0].header) == null ? void 0 : _c.kb_jwt;
return new _GeneralJSON({
payload: json.payload,
disclosures,
kb_jwt,
signatures: json.signatures.map((s) => {
var _a2;
return {
protected: s.protected,
signature: s.signature,
kid: (_a2 = s.header) == null ? void 0 : _a2.kid
};
})
});
}
toJson() {
return {
payload: this.payload,
signatures: this.signatures.map((s, i) => {
if (i !== 0) {
return {
header: {
kid: s.kid
},
protected: s.protected,
signature: s.signature
};
}
return {
header: {
disclosures: this.disclosures,
kid: s.kid,
kb_jwt: this.kb_jwt
},
protected: s.protected,
signature: s.signature
};
})
};
}
toEncoded(index) {
var _a;
if (index < 0 || index >= this.signatures.length) {
throw new SDJWTException("Index out of bounds");
}
const data = [];
const { protected: protectedHeader, signature } = this.signatures[index];
const jwt = `${protectedHeader}.${this.payload}.${signature}`;
data.push(jwt);
if (this.disclosures && this.disclosures.length > 0) {
const disclosures = this.disclosures.join(SD_SEPARATOR);
data.push(disclosures);
}
const kb = (_a = this.kb_jwt) != null ? _a : "";
data.push(kb);
return data.join(SD_SEPARATOR);
}
addSignature(protectedHeader, signer, kid) {
return __async(this, null, function* () {
const header = base64urlEncode(JSON.stringify(protectedHeader));
const signature = yield signer(`${header}.${this.payload}`);
this.signatures.push({
protected: header,
signature,
kid
});
});
}
};
// src/jwt.ts
var Jwt = class _Jwt {
constructor(data) {
this.header = data == null ? void 0 : data.header;
this.payload = data == null ? void 0 : data.payload;
this.signature = data == null ? void 0 : data.signature;
this.encoded = data == null ? void 0 : data.encoded;
}
static decodeJWT(jwt) {
return decodeJwt(jwt);
}
static fromEncode(encodedJwt) {
const { header, payload, signature } = _Jwt.decodeJWT(
encodedJwt
);
const jwt = new _Jwt({
header,
payload,
signature,
encoded: encodedJwt
});
return jwt;
}
setHeader(header) {
this.header = header;
this.encoded = void 0;
return this;
}
setPayload(payload) {
this.payload = payload;
this.encoded = void 0;
return this;
}
getUnsignedToken() {
if (!this.header || !this.payload) {
throw new SDJWTException("Serialize Error: Invalid JWT");
}
if (this.encoded) {
const parts = this.encoded.split(".");
if (parts.length !== 3) {
throw new SDJWTException(`Invalid JWT format: ${this.encoded}`);
}
const unsignedToken = parts.slice(0, 2).join(".");
return unsignedToken;
}
const header = base64urlEncode(JSON.stringify(this.header));
const payload = base64urlEncode(JSON.stringify(this.payload));
return `${header}.${payload}`;
}
sign(signer) {
return __async(this, null, function* () {
const data = this.getUnsignedToken();
this.signature = yield signer(data);
return this.encodeJwt();
});
}
encodeJwt() {
if (this.encoded) {
return this.encoded;
}
if (!this.header || !this.payload || !this.signature) {
throw new SDJWTException("Serialize Error: Invalid JWT");
}
const header = base64urlEncode(JSON.stringify(this.header));
const payload = base64urlEncode(JSON.stringify(this.payload));
const signature = this.signature;
const compact = `${header}.${payload}.${signature}`;
this.encoded = compact;
return compact;
}
/**
* Verify the JWT using the provided verifier function.
* It checks the signature and validates the iat, nbf, and exp claims if they are present.
* @param verifier
* @param options - Options for verification, such as current date and skew seconds
* @returns
*/
verify(verifier, options) {
return __async(this, null, function* () {
var _a, _b, _c, _d;
const skew = (options == null ? void 0 : options.skewSeconds) ? options.skewSeconds : 0;
const currentDate = (_a = options == null ? void 0 : options.currentDate) != null ? _a : Math.floor(Date.now() / 1e3);
const iat = (_b = this.payload) == null ? void 0 : _b.iat;
const nbf = (_c = this.payload) == null ? void 0 : _c.nbf;
const exp = (_d = this.payload) == null ? void 0 : _d.exp;
if (typeof iat === "number" && iat - skew > currentDate) {
throw new SDJWTException("Verify Error: JWT is not yet valid");
}
if (typeof nbf === "number" && nbf - skew > currentDate) {
throw new SDJWTException("Verify Error: JWT is not yet valid");
}
if (typeof exp === "number" && exp + skew < currentDate) {
throw new SDJWTException("Verify Error: JWT is expired");
}
if (!this.signature) {
throw new SDJWTException("Verify Error: no signature in JWT");
}
const data = this.getUnsignedToken();
const verified = yield verifier(data, this.signature, options);
if (!verified) {
throw new SDJWTException("Verify Error: Invalid JWT Signature");
}
return { payload: this.payload, header: this.header };
});
}
};
// src/kbjwt.ts
var KBJwt = class _KBJwt extends Jwt {
// Checking the validity of the key binding jwt
// the type unknown is not good, but we don't know at this point how to get the public key of the signer, this is defined in the kbVerifier
verifyKB(values) {
return __async(this, null, function* () {
if (!this.header || !this.payload || !this.signature) {
throw new SDJWTException("Verify Error: Invalid JWT");
}
if (!this.header.alg || this.header.alg === "none" || !this.header.typ || this.header.typ !== KB_JWT_TYP || !this.payload.iat || !this.payload.aud || !this.payload.nonce || // this is for backward compatibility with version 06
!(this.payload.sd_hash || "_sd_hash" in this.payload && this.payload._sd_hash)) {
throw new SDJWTException("Invalid Key Binding Jwt");
}
if (this.payload.nonce !== values.nonce) {
throw new SDJWTException("Verify Error: Invalid Nonce");
}
yield this.verify(
(data, sig) => values.verifier(data, sig, values.payload),
values.options
);
return { payload: this.payload, header: this.header };
});
}
// This function is for creating KBJwt object for verify properly
static fromKBEncode(encodedJwt) {
const { header, payload, signature } = Jwt.decodeJWT(
encodedJwt
);
const jwt = new _KBJwt({
header,
payload,
signature,
encoded: encodedJwt
});
return jwt;
}
};
// src/decoy.ts
var createDecoy = (hash, saltGenerator) => __async(null, null, function* () {
const { hasher, alg } = hash;
const salt = yield saltGenerator(16);
const decoy = yield hasher(salt, alg);
return uint8ArrayToBase64Url(decoy);
});
// src/present/present.ts
var presentableKeys = (rawPayload, disclosures, hasher) => __async(null, null, function* () {
const { disclosureKeymap } = yield unpack(rawPayload, disclosures, hasher);
return Object.keys(disclosureKeymap).sort();
});
var presentableKeysSync = (rawPayload, disclosures, hasher) => {
const { disclosureKeymap } = unpackSync(rawPayload, disclosures, hasher);
return Object.keys(disclosureKeymap).sort();
};
var present = (sdJwt, presentFrame, hasher) => __async(null, null, function* () {
const { jwt, kbJwt } = splitSdJwt(sdJwt);
const {
jwt: { payload },
disclosures
} = yield decodeSdJwt(sdJwt, hasher);
const { _sd_alg: alg } = getSDAlgAndPayload(payload);
const hash = { alg, hasher };
const keys = transformPresentationFrame(presentFrame);
const hashmap = yield createHashMapping(disclosures, hash);
const { disclosureKeymap } = yield unpack(payload, disclosures, hasher);
const presentedDisclosures = keys.map((k) => hashmap[disclosureKeymap[k]]).filter((d) => d !== void 0);
return [
jwt,
...presentedDisclosures.map((d) => d.encode()),
kbJwt != null ? kbJwt : ""
].join(SD_SEPARATOR);
});
var presentSync = (sdJwt, presentFrame, hasher) => {
const { jwt, kbJwt } = splitSdJwt(sdJwt);
const {
jwt: { payload },
disclosures
} = decodeSdJwtSync(sdJwt, hasher);
const { _sd_alg: alg } = getSDAlgAndPayload(payload);
const hash = { alg, hasher };
const keys = transformPresentationFrame(presentFrame);
const hashmap = createHashMappingSync(disclosures, hash);
const { disclosureKeymap } = unpackSync(payload, disclosures, hasher);
const presentedDisclosures = keys.map((k) => hashmap[disclosureKeymap[k]]).filter((d) => d !== void 0);
return [
jwt,
...presentedDisclosures.map((d) => d.encode()),
kbJwt != null ? kbJwt : ""
].join(SD_SEPARATOR);
};
var transformPresentationFrame = (obj, prefix = "") => {
return Object.entries(obj).reduce((acc, [key, value]) => {
const newPrefix = prefix ? `${prefix}.${key}` : key;
if (typeof value === "boolean") {
if (value) {
acc.push(newPrefix);
}
} else if (typeof value === "object" && value !== null) {
acc.push(
newPrefix,
...transformPresentationFrame(
value,
newPrefix
)
);
}
return acc;
}, []);
};
var createHashMappingForSerializedDisclosure = (disclosures) => {
const map = {};
for (let i = 0; i < disclosures.length; i++) {
const disclosure = disclosures[i];
const { digest, encoded, key, salt, value } = disclosure;
map[digest] = Disclosure.fromArray(
key ? [salt, key, value] : [salt, value],
{ digest, encoded }
);
}
return map;
};
var selectDisclosures = (payload, disclosures, presentationFrame) => {
if (disclosures.length === 0) {
return [];
}
const hashmap = createHashMappingForSerializedDisclosure(disclosures);
const { disclosureKeymap } = unpackObj(payload, hashmap);
const keys = transformPresentationFrame(presentationFrame);
const presentedDisclosures = keys.map((k) => hashmap[disclosureKeymap[k]]).filter((d) => d !== void 0);
const selectedDisclosures = presentedDisclosures.map(
(d) => {
const { salt, key, value, _digest } = d;
if (!_digest) {
throw new SDJWTException(
"Implementation error: _digest is not defined"
);
}
return {
digest: _digest,
encoded: d.encode(),
salt,
key,
value
};
}
);
return selectedDisclosures;
};
// src/sdjwt.ts
var SDJwt = class _SDJwt {
constructor(data) {
this.jwt = data == null ? void 0 : data.jwt;
this.disclosures = data == null ? void 0 : data.disclosures;
this.kbJwt = data == null ? void 0 : data.kbJwt;
}
static decodeSDJwt(sdjwt, hasher) {
return __async(this, null, function* () {
const [encodedJwt, ...encodedDisclosures] = sdjwt.split(SD_SEPARATOR);
const jwt = Jwt.fromEncode(encodedJwt);
if (!jwt.payload) {
throw new Error("Payload is undefined on the JWT. Invalid state reached");
}
if (encodedDisclosures.length === 0) {
return {
jwt,
disclosures: []
};
}
const encodedKeyBindingJwt = encodedDisclosures.pop();
const kbJwt = encodedKeyBindingJwt ? KBJwt.fromKBEncode(encodedKeyBindingJwt) : void 0;
const { _sd_alg } = getSDAlgAndPayload(jwt.payload);
const disclosures = yield Promise.all(
encodedDisclosures.map(
(ed) => Disclosure.fromEncode(ed, { alg: _sd_alg, hasher })
)
);
return {
jwt,
disclosures,
kbJwt
};
});
}
static extractJwt(encodedSdJwt) {
return __async(this, null, function* () {
const [encodedJwt, ..._encodedDisclosures] = encodedSdJwt.split(SD_SEPARATOR);
return Jwt.fromEncode(encodedJwt);
});
}
static fromEncode(encodedSdJwt, hasher) {
return __async(this, null, function* () {
const { jwt, disclosures, kbJwt } = yield _SDJwt.decodeSDJwt(encodedSdJwt, hasher);
return new _SDJwt({
jwt,
disclosures,
kbJwt
});
});
}
present(presentFrame, hasher) {
return __async(this, null, function* () {
const disclosures = yield this.getPresentDisclosures(presentFrame, hasher);
const presentSDJwt = new _SDJwt({
jwt: this.jwt,
disclosures,
kbJwt: this.kbJwt
});
return presentSDJwt.encodeSDJwt();
});
}
getPresentDisclosures(presentFrame, hasher) {
return __async(this, null, function* () {
var _a;
if (!((_a = this.jwt) == null ? void 0 : _a.payload) || !this.disclosures) {
throw new SDJWTException("Invalid sd-jwt: jwt or disclosures is missing");
}
const { _sd_alg: alg } = getSDAlgAndPayload(this.jwt.payload);
const hash = { alg, hasher };
const hashmap = yield createHashMapping(this.disclosures, hash);
const { disclosureKeymap } = yield unpack(
this.jwt.payload,
this.disclosures,
hasher
);
const keys = presentFrame ? transformPresentationFrame(presentFrame) : yield this.presentableKeys(hasher);
const disclosures = keys.map((k) => hashmap[disclosureKeymap[k]]).filter((d) => d !== void 0);
return disclosures;
});
}
encodeSDJwt() {
const data = [];
if (!this.jwt) {
throw new SDJWTException("Invalid sd-jwt: jwt is missing");
}
const encodedJwt = this.jwt.encodeJwt();
data.push(encodedJwt);
if (this.disclosures && this.disclosures.length > 0) {
const encodeddisclosures = this.disclosures.map((dc) => dc.encode()).join(SD_SEPARATOR);
data.push(encodeddisclosures);
}
data.push(this.kbJwt ? this.kbJwt.encodeJwt() : "");
return data.join(SD_SEPARATOR);
}
keys(hasher) {
return __async(this, null, function* () {
return listKeys(yield this.getClaims(hasher)).sort();
});
}
presentableKeys(hasher) {
return __async(this, null, function* () {
var _a, _b;
if (!((_a = this.jwt) == null ? void 0 : _a.payload) || !this.disclosures) {
throw new SDJWTException("Invalid sd-jwt: jwt or disclosures is missing");
}
const { disclosureKeymap } = yield unpack(
(_b = this.jwt) == null ? void 0 : _b.payload,
this.disclosures,
hasher
);
return Object.keys(disclosureKeymap).sort();
});
}
getClaims(hasher) {
return __async(this, null, function* () {
var _a;
if (!((_a = this.jwt) == null ? void 0 : _a.payload) || !this.disclosures) {
throw new SDJWTException("Invalid sd-jwt: jwt or disclosures is missing");
}
const { unpackedObj } = yield unpack(
this.jwt.payload,
this.disclosures,
hasher
);
return unpackedObj;
});
}
};
var listKeys = (obj, prefix = "") => {
const keys = [];
for (const key in obj) {
if (obj[key] === void 0) continue;
const newKey = prefix ? `${prefix}.${key}` : key;
keys.push(newKey);
const value = obj[key];
if (value && typeof value === "object") {
keys.push(...listKeys(value, newKey));
}
}
return keys;
};
var pack = (claims, disclosureFrame, hash, saltGenerator) => __async(null, null, function* () {
var _a, _b;
if (!disclosureFrame) {
return {
packedClaims: claims,
disclosures: []
};
}
const sd = (_a = disclosureFrame[SD_DIGEST]) != null ? _a : [];
const decoyCount = (_b = disclosureFrame[SD_DECOY]) != null ? _b : 0;
if (Array.isArray(claims)) {
const packedClaims2 = [];
const disclosures2 = [];
const recursivePackedClaims2 = {};
for (const key in disclosureFrame) {
if (key !== SD_DIGEST) {
const idx = Number.parseInt(key, 10);
const packed = yield pack(
claims[idx],
disclosureFrame[idx],
hash,
saltGenerator
);
recursivePackedClaims2[idx] = packed.packedClaims;
disclosures2.push(...packed.disclosures);
}
}
for (let i = 0; i < claims.length; i++) {
const claim = recursivePackedClaims2[i] ? recursivePackedClaims2[i] : claims[i];
if (sd.includes(i)) {
const salt = yield saltGenerator(16);
const disclosure = new Disclosure([salt, claim]);
const digest = yield disclosure.digest(hash);
packedClaims2.push({ [SD_LIST_KEY]: digest });
disclosures2.push(disclosure);
} else {
packedClaims2.push(claim);
}
}
for (let j = 0; j < decoyCount; j++) {
const decoyDigest = yield createDecoy(hash, saltGenerator);
packedClaims2.push({ [SD_LIST_KEY]: decoyDigest });
}
return { packedClaims: packedClaims2, disclosures: disclosures2 };
}
const packedClaims = {};
const disclosures = [];
const recursivePackedClaims = {};
for (const key in disclosureFrame) {
if (key !== SD_DIGEST) {
const packed = yield pack(
// @ts-expect-error
claims[key],
disclosureFrame[key],
hash,
saltGenerator
);
recursivePackedClaims[key] = packed.packedClaims;
disclosures.push(...packed.disclosures);
}
}
const _sd = [];
for (const key in claims) {
const claim = recursivePackedClaims[key] ? recursivePackedClaims[key] : claims[key];
if (sd.includes(key)) {
const salt = yield saltGenerator(16);
const disclosure = new Disclosure([salt, key, claim]);
const digest = yield disclosure.digest(hash);
_sd.push(digest);
disclosures.push(disclosure);
} else {
packedClaims[key] = claim;
}
}
for (let j = 0; j < decoyCount; j++) {
const decoyDigest = yield createDecoy(hash, saltGenerator);
_sd.push(decoyDigest);
}
if (_sd.length > 0) {
packedClaims[SD_DIGEST] = _sd.sort();
}
return { packedClaims, disclosures };
});
// src/index.ts
var _SDJwtInstance = class _SDJwtInstance {
constructor(userConfig) {
this.userConfig = {};
if (userConfig) {
if (userConfig.hashAlg && !IANA_HASH_ALGORITHMS.includes(userConfig.hashAlg)) {
throw new SDJWTException(
`Invalid hash algorithm: ${userConfig.hashAlg}`
);
}
this.userConfig = userConfig;
}
}
createKBJwt(options, sdHash) {
return __async(this, null, function* () {
if (!this.userConfig.kbSigner) {
throw new SDJWTException("Key Binding Signer not found");
}
if (!this.userConfig.kbSignAlg) {
throw new SDJWTException("Key Binding sign algorithm not specified");
}
const { payload } = options;
const kbJwt = new KBJwt({
header: {
typ: KB_JWT_TYP,
alg: this.userConfig.kbSignAlg
},
payload: __spreadProps(__spreadValues({}, payload), { sd_hash: sdHash })
});
yield kbJwt.sign(this.userConfig.kbSigner);
return kbJwt;
});
}
SignJwt(jwt) {
return __async(this, null, function* () {
if (!this.userConfig.signer) {
throw new SDJWTException("Signer not found");
}
yield jwt.sign(this.userConfig.signer);
return jwt;
});
}
VerifyJwt(jwt, options) {
return __async(this, null, function* () {
if (!this.userConfig.verifier) {
throw new SDJWTException("Verifier not found");
}
return jwt.verify(this.userConfig.verifier, options);
});
}
issue(payload, disclosureFrame, options) {
return __async(this, null, function* () {
var _a, _b;
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
if (!this.userConfig.saltGenerator) {
throw new SDJWTException("SaltGenerator not found");
}
if (!this.userConfig.signAlg) {
throw new SDJWTException("sign alogrithm not specified");
}
this.validateReservedFields(payload);
this.validateDisclosureFrame(disclosureFrame);
const hasher = this.userConfig.hasher;
const hashAlg = (_a = this.userConfig.hashAlg) != null ? _a : _SDJwtInstance.DEFAULT_hashAlg;
const { packedClaims, disclosures } = yield pack(
payload,
disclosureFrame,
{ hasher, alg: hashAlg },
this.userConfig.saltGenerator
);
const alg = this.userConfig.signAlg;
const OptionHeader = (_b = options == null ? void 0 : options.header) != null ? _b : {};
const CustomHeader = this.userConfig.omitTyp ? OptionHeader : __spreadValues({ typ: this.type }, OptionHeader);
const header = __spreadProps(__spreadValues({}, CustomHeader), { alg });
const jwt = new Jwt({
header,
payload: __spreadProps(__spreadValues({}, packedClaims), {
_sd_alg: disclosureFrame ? hashAlg : void 0
})
});
yield this.SignJwt(jwt);
const sdJwt = new SDJwt({
jwt,
disclosures
});
return sdJwt.encodeSDJwt();
});
}
/**
* Validates if the payload contains any reserved claim names. If so it will throw an error.
* @param payload
* @returns
*/
validateReservedFields(payload) {
const reservedFields = /* @__PURE__ */ new Set([SD_DIGEST, "_sd_alg", SD_DECOY]);
const visit = (node) => {
if (!node || typeof node !== "object") {
return;
}
for (const [key, value] of Object.entries(
node
)) {
if (reservedFields.has(key)) {
throw new SDJWTException(
`Reserved field name "${key}" is not allowed`
);
}
visit(value);
}
};
visit(payload);
}
validateDisclosureFrame(_disclosureFrame) {
return;
}
present(encodedSDJwt, presentationFrame, options) {
return __async(this, null, function* () {
var _a;
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const hasher = this.userConfig.hasher;
const sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
if (!((_a = sdjwt.jwt) == null ? void 0 : _a.payload)) throw new SDJWTException("Payload not found");
const presentSdJwtWithoutKb = yield sdjwt.present(
presentationFrame,
hasher
);
if (!(options == null ? void 0 : options.kb)) {
return presentSdJwtWithoutKb;
}
const sdHashStr = yield this.calculateSDHash(
presentSdJwtWithoutKb,
sdjwt,
hasher
);
sdjwt.kbJwt = yield this.createKBJwt(options.kb, sdHashStr);
return sdjwt.present(presentationFrame, hasher);
});
}
// This function is for verifying the SD JWT
// If requiredClaimKeys is provided, it will check if the required claim keys are presentation in the SD JWT
// If requireKeyBindings is true, it will check if the key binding JWT is presentation and verify it
verify(encodedSDJwt, options) {
return __async(this, null, function* () {
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const hasher = this.userConfig.hasher;
const sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
throw new SDJWTException("Invalid SD JWT");
}
const { payload, header } = yield this.validate(encodedSDJwt, options);
if (options == null ? void 0 : options.requiredClaimKeys) {
const keys = yield sdjwt.keys(hasher);
const missingKeys = options.requiredClaimKeys.filter(
(k) => !keys.includes(k)
);
if (missingKeys.length > 0) {
throw new SDJWTException(
`Missing required claim keys: ${missingKeys.join(", ")}`
);
}
}
if (!(options == null ? void 0 : options.keyBindingNonce)) {
return { payload, header };
}
if (!sdjwt.kbJwt) {
throw new SDJWTException("Key Binding JWT not exist");
}
if (!this.userConfig.kbVerifier) {
throw new SDJWTException("Key Binding Verifier not found");
}
const kb = yield sdjwt.kbJwt.verifyKB({
verifier: this.userConfig.kbVerifier,
payload,
nonce: options.keyBindingNonce,
options
});
if (!kb) {
throw new Error("signature is not valid");
}
const sdHashfromKb = kb.payload.sd_hash;
const sdjwtWithoutKb = new SDJwt({
jwt: sdjwt.jwt,
disclosures: sdjwt.disclosures
});
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
const sdHashStr = yield this.calculateSDHash(
presentSdJwtWithoutKb,
sdjwt,
hasher
);
if (sdHashStr !== sdHashfromKb) {
throw new SDJWTException("Invalid sd_hash in Key Binding JWT");
}
return { payload, header, kb };
});
}
/**
* Safe verification that collects all errors instead of failing fast.
* Returns a result object with either the verified data or an array of all errors.
*
* @param encodedSDJwt - The encoded SD-JWT to verify
* @param options - Verification options
* @returns A SafeVerifyResult containing either success data or collected errors
*/
safeVerify(encodedSDJwt, options) {
return __async(this, null, function* () {
const errors = [];
const addError = (code, message, details) => {
errors.push({ code, message, details });
};
const exceptionToCode = (error) => {
const message = error.message.toLowerCase();
if (message.includes("hasher not found")) return "HASHER_NOT_FOUND";
if (message.includes("verifier not found")) return "VERIFIER_NOT_FOUND";
if (message.includes("invalid sd jwt") || message.includes("invalid jwt"))
return "INVALID_SD_JWT";
if (message.includes("not yet valid")) return "JWT_NOT_YET_VALID";
if (message.includes("expired")) return "JWT_EXPIRED";
if (message.includes("signature")) return "INVALID_JWT_SIGNATURE";
if (message.includes("missing required claim"))
return "MISSING_REQUIRED_CLAIMS";
if (message.includes("key binding jwt not exist"))
return "KEY_BINDING_JWT_MISSING";
if (message.includes("key binding verifier not found"))
return "KEY_BINDING_VERIFIER_NOT_FOUND";
if (message.includes("sd_hash")) return "KEY_BINDING_SD_HASH_INVALID";
return "UNKNOWN_ERROR";
};
if (!this.userConfig.hasher) {
addError("HASHER_NOT_FOUND", "Hasher not found");
}
if (!this.userConfig.verifier) {
addError("VERIFIER_NOT_FOUND", "Verifier not found");
}
if (errors.length > 0) {
return { success: false, errors };
}
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const hasher = this.userConfig.hasher;
let sdjwt;
let payload;
let header;
try {
sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
addError("INVALID_SD_JWT", "Invalid SD JWT: missing JWT or payload");
}
} catch (e) {
const error = ensureError(e);
addError(
"INVALID_SD_JWT",
`Failed to decode SD-JWT: ${error.message}`,
error
);
}
if (sdjwt == null ? void 0 : sdjwt.jwt) {
try {
const result = yield this.VerifyJwt(sdjwt.jwt, options);
header = result.header;
const claims = yield sdjwt.getClaims(hasher);
payload = claims;
} catch (e) {
const error = ensureError(e);
const code = exceptionToCode(error);
addError(code, error.message, error);
}
}
if (sdjwt && (options == null ? void 0 : options.requiredClaimKeys)) {
try {
const keys = yield sdjwt.keys(hasher);
const missingKeys = options.requiredClaimKeys.filter(
(k) => !keys.includes(k)
);
if (missingKeys.length > 0) {
addError(
"MISSING_REQUIRED_CLAIMS",
`Missing required claim keys: ${missingKeys.join(", ")}`,
{ missingKeys }
);
}
} catch (e) {
const error = ensureError(e);
addError(
"UNKNOWN_ERROR",
`Failed to check required claims: ${error.message}`,
error
);
}
}
let kb;
if ((options == null ? void 0 : options.keyBindingNonce) && sdjwt) {
if (!sdjwt.kbJwt) {
addError("KEY_BINDING_JWT_MISSING", "Key Binding JWT not exist");
} else if (!this.userConfig.kbVerifier) {
addError(
"KEY_BINDING_VERIFIER_NOT_FOUND",
"Key Binding Verifier not found"
);
} else if (payload) {
try {
const kbResult = yield sdjwt.kbJwt.verifyKB({
verifier: this.userConfig.kbVerifier,
payload,
nonce: options.keyBindingNonce,
options
});
if (!kbResult) {
addError(
"KEY_BINDING_SIGNATURE_INVALID",
"Key binding signature is not valid"
);
} else {
kb = kbResult;
const sdjwtWithoutKb = new SDJwt({
jwt: sdjwt.jwt,
disclosures: sdjwt.disclosures
});
const presentSdJwtWithoutKb = sdjwtWithoutKb.encodeSDJwt();
const sdHashStr = yield this.calculateSDHash(
presentSdJwtWithoutKb,
sdjwt,
hasher
);
if (sdHashStr !== kbResult.payload.sd_hash) {
addError(
"KEY_BINDING_SD_HASH_INVALID",
"Invalid sd_hash in Key Binding JWT",
{
expected: sdHashStr,
received: kbResult.payload.sd_hash
}
);
}
}
} catch (e) {
const error = ensureError(e);
addError(
"KEY_BINDING_SIGNATURE_INVALID",
`Key binding verification failed: ${error.message}`,
error
);
}
}
}
if (errors.length > 0) {
return { success: false, errors };
}
return {
success: true,
data: {
payload,
header,
kb
}
};
});
}
calculateSDHash(presentSdJwtWithoutKb, sdjwt, hasher) {
return __async(this, null, function* () {
if (!sdjwt.jwt || !sdjwt.jwt.payload) {
throw new SDJWTException("Invalid SD JWT");
}
const { _sd_alg } = getSDAlgAndPayload(sdjwt.jwt.payload);
const sdHash = yield hasher(presentSdJwtWithoutKb, _sd_alg);
const sdHashStr = uint8ArrayToBase64Url(sdHash);
return sdHashStr;
});
}
/**
* This function is for validating the SD JWT
* Checking signature, if provided the iat and exp when provided and return its the claims
* @param encodedSDJwt
* @param options
* @returns
*/
validate(encodedSDJwt, options) {
return __async(this, null, function* () {
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const hasher = this.userConfig.hasher;
const sdjwt = yield SDJwt.fromEncode(encodedSDJwt, hasher);
if (!sdjwt.jwt) {
throw new SDJWTException("Invalid SD JWT");
}
const verifiedPayloads = yield this.VerifyJwt(sdjwt.jwt, options);
const claims = yield sdjwt.getClaims(hasher);
return { payload: claims, header: verifiedPayloads.header };
});
}
config(newConfig) {
this.userConfig = __spreadValues(__spreadValues({}, this.userConfig), newConfig);
}
encode(sdJwt) {
return sdJwt.encodeSDJwt();
}
decode(endcodedSDJwt) {
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
return SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
}
keys(endcodedSDJwt) {
return __async(this, null, function* () {
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const sdjwt = yield SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
return sdjwt.keys(this.userConfig.hasher);
});
}
presentableKeys(endcodedSDJwt) {
return __async(this, null, function* () {
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const sdjwt = yield SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
return sdjwt.presentableKeys(this.userConfig.hasher);
});
}
getClaims(endcodedSDJwt) {
return __async(this, null, function* () {
if (!this.userConfig.hasher) {
throw new SDJWTException("Hasher not found");
}
const sdjwt = yield SDJwt.fromEncode(endcodedSDJwt, this.userConfig.hasher);
return sdjwt.getClaims(this.userConfig.hasher);
});
}
toFlattenJSON(endcodedSDJwt) {
return FlattenJSON.fromEncode(endcodedSDJwt);
}
toGeneralJSON(endcodedSDJwt) {
return GeneralJSON.fromEncode(endcodedSDJwt);
}
};
_SDJwtInstance.DEFAULT_hashAlg = "sha-256";
var SDJwtInstance = _SDJwtInstance;
var SDJwtGeneralJSONInstance = class {
constructor(userConfig) {
this.userConfig = {};
if (userConfig) {
if (userConfig.hashAlg && !IANA_HASH_ALGORITHMS.includes(userConfig.hashAlg)) {