diff --git a/packages/decode/src/decode.ts b/packages/decode/src/decode.ts index fa73929d..d339cd89 100644 --- a/packages/decode/src/decode.ts +++ b/packages/decode/src/decode.ts @@ -144,7 +144,7 @@ export const getClaimsSync = ( return unpackedObj as T; }; -export const unpackArray = ( +const unpackArray = ( arr: Array, map: Record, prefix = '', @@ -160,21 +160,15 @@ export const unpackArray = ( const presentKey = prefix ? `${prefix}.${idx}` : `${idx}`; keys[presentKey] = hash; - const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObj( - disclosed.value, - map, - presentKey, - ); + const { unpackedObj, disclosureKeymap: disclosureKeys } = + unpackObjInternal(disclosed.value, map, presentKey); unpackedArray.push(unpackedObj); Object.assign(keys, disclosureKeys); } } else { const newKey = prefix ? `${prefix}.${idx}` : `${idx}`; - const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObj( - item, - map, - newKey, - ); + const { unpackedObj, disclosureKeymap: disclosureKeys } = + unpackObjInternal(item, map, newKey); unpackedArray.push(unpackedObj); Object.assign(keys, disclosureKeys); } @@ -185,7 +179,12 @@ export const unpackArray = ( return { unpackedObj: unpackedArray, disclosureKeymap: keys }; }; -export const unpackObj = ( +export const unpackObj = (obj: unknown, map: Record) => { + const copiedObj = JSON.parse(JSON.stringify(obj)); + return unpackObjInternal(copiedObj, map); +}; + +const unpackObjInternal = ( obj: unknown, map: Record, prefix = '', @@ -203,11 +202,8 @@ export const unpackObj = ( typeof (obj as Record)[key] === 'object' ) { const newKey = prefix ? `${prefix}.${key}` : key; - const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObj( - (obj as Record)[key], - map, - newKey, - ); + const { unpackedObj, disclosureKeymap: disclosureKeys } = + unpackObjInternal((obj as Record)[key], map, newKey); (obj as Record)[key] = unpackedObj; Object.assign(keys, disclosureKeys); } @@ -226,11 +222,8 @@ export const unpackObj = ( : disclosed.key; keys[presentKey] = hash; - const { unpackedObj, disclosureKeymap: disclosureKeys } = unpackObj( - disclosed.value, - map, - presentKey, - ); + const { unpackedObj, disclosureKeymap: disclosureKeys } = + unpackObjInternal(disclosed.value, map, presentKey); claims[disclosed.key] = unpackedObj; Object.assign(keys, disclosureKeys); } @@ -281,7 +274,8 @@ export const getSDAlgAndPayload = (SdJwtPayload: Record) => { }; // Match the digests of the disclosures with the claims and extract the claims -// unpack function use unpackObj and unpackArray to recursively unpack the claims +// unpack function use unpackObjInternal and unpackArray to recursively unpack the claims +// Since getSDAlgAndPayload create new object So we don't need to clone it again export const unpack = async ( SdJwtPayload: Record, disclosures: Array, diff --git a/packages/decode/src/test/decode.spec.ts b/packages/decode/src/test/decode.spec.ts index 7d35a6f0..a4a07385 100644 --- a/packages/decode/src/test/decode.spec.ts +++ b/packages/decode/src/test/decode.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest'; import { + createHashMapping, decodeJwt, decodeSdJwt, decodeSdJwtSync, @@ -7,6 +8,7 @@ import { getClaimsSync, getSDAlgAndPayload, splitSdJwt, + unpackObj, } from '../index'; import { digest } from '@sd-jwt/crypto-nodejs'; @@ -93,6 +95,26 @@ describe('decode tests', () => { expect(decodedSdJwt.jwt).toBeDefined(); }); + test('decode sdjwt sync (with KB)', () => { + const sdjwt = + 'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg~WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA'; + const decodedSdJwt = decodeSdJwtSync(sdjwt, digest); + expect(decodedSdJwt).toBeDefined(); + expect(decodedSdJwt.kbJwt).toBeDefined(); + expect(decodedSdJwt.disclosures.length).toEqual(3); + expect(decodedSdJwt.jwt).toBeDefined(); + }); + + test('decode sdjwt (with KB)', async () => { + const sdjwt = + 'eyJ0eXAiOiJzZCtqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpZCI6IjEyMzQiLCJfc2QiOlsiYkRUUnZtNS1Zbi1IRzdjcXBWUjVPVlJJWHNTYUJrNTdKZ2lPcV9qMVZJNCIsImV0M1VmUnlsd1ZyZlhkUEt6Zzc5aGNqRDFJdHpvUTlvQm9YUkd0TW9zRmsiLCJ6V2ZaTlMxOUF0YlJTVGJvN3NKUm4wQlpRdldSZGNob0M3VVphYkZyalk4Il0sIl9zZF9hbGciOiJzaGEtMjU2In0.n27NCtnuwytlBYtUNjgkesDP_7gN7bhaLhWNL4SWT6MaHsOjZ2ZMp987GgQRL6ZkLbJ7Cd3hlePHS84GBXPuvg~WyI1ZWI4Yzg2MjM0MDJjZjJlIiwiZmlyc3RuYW1lIiwiSm9obiJd~WyJjNWMzMWY2ZWYzNTg4MWJjIiwibGFzdG5hbWUiLCJEb2UiXQ~WyJmYTlkYTUzZWJjOTk3OThlIiwic3NuIiwiMTIzLTQ1LTY3ODkiXQ~eyJ0eXAiOiJrYitqd3QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE3MTAwNjk3MjIsImF1ZCI6ImRpZDpleGFtcGxlOjEyMyIsIm5vbmNlIjoiazh2ZGYwbmQ2Iiwic2RfaGFzaCI6Il8tTmJWSzNmczl3VzNHaDNOUktSNEt1NmZDMUwzN0R2MFFfalBXd0ppRkUifQ.pqw2OB5IA5ya9Mxf60hE3nr2gsJEIoIlnuCa4qIisijHbwg3WzTDFmW2SuNvK_ORN0WU6RoGbJx5uYZh8k4EbA'; + const decodedSdJwt = await decodeSdJwt(sdjwt, digest); + expect(decodedSdJwt).toBeDefined(); + expect(decodedSdJwt.kbJwt).toBeDefined(); + expect(decodedSdJwt.disclosures.length).toEqual(3); + expect(decodedSdJwt.jwt).toBeDefined(); + }); + test('get claims', async () => { const sdjwt = 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJfc2QiOlsiaWQ1azZ1ZVplVTY4bExaMlU2YjJJbF9QR3ZKb1RDMlpkMkpwY0RwMzFIWSJdLCJfc2RfYWxnIjoic2hhLTI1NiJ9.GiLF_HhacrstqCJ223VvWOoJJWU8qk4dYQHklSMwxv36pPF_7ER53Wbty1qYRlQ6NeMUdBRRdj9JQLLCzz1gCQ~WyI2NTMxNDA2ZmVhZmU0YjBmIiwiZm9vIiwiYmFyIl0~'; @@ -161,4 +183,20 @@ describe('decode tests', () => { const { _sd_alg, payload } = getSDAlgAndPayload({}); expect(_sd_alg).toBe('sha-256'); }); + + test('unpackObj clone', async () => { + const sdjwt = + 'eyJ0eXAiOiJzZC1qd3QiLCJhbGciOiJFZERTQSJ9.eyJ0ZXN0Ijp7Il9zZCI6WyJqVEszMHNleDZhYV9kUk1KSWZDR056Q0FwbVB5MzRRNjNBa3QzS3hhSktzIl19LCJfc2QiOlsiME9nMi1ReG95eW1UOGNnVzZZUjVSSFpQLUJuR2tHUi1NM2otLV92RWlzSSIsIkcwZ3lHNnExVFMyUlQxMkZ3X2RRRDVVcjlZc1AwZlVWOXVtQWdGMC1jQ1EiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.ggEyE4SeDO2Hu3tol3VLmi7NQj56yKzKQDaafocgkLrUBdivghohtzrfcbrMN7CRufJ_Cnh0EL54kymXLGTdDQ~WyIwNGU0MjAzOWU4ZWFiOWRjIiwiYSIsIjEiXQ~WyIwOGE1Yjc5MjMyYjAzYzBhIiwiMSJd~WyJiNWE2YjUzZGQwYTFmMGIwIiwienp6IiwieHh4Il0~WyIxYzdmOTE4ZTE0MjA2NzZiIiwiZm9vIiwiYmFyIl0~WyJmZjYxYzQ5ZGU2NjFiYzMxIiwiYXJyIixbeyIuLi4iOiJTSG96VW5KNUpkd0ZtTjVCbXB5dXZCWGZfZWRjckVvcExPYThTVlBFUmg0In0sIjIiLHsiX3NkIjpbIkpuODNhZkp0OGx4NG1FMzZpRkZyS2U2R2VnN0dlVUQ4Z3UwdVo3NnRZcW8iXX1dXQ~'; + const decodedSdJwt = await decodeSdJwt(sdjwt, digest); + const jwtPayload = JSON.parse(JSON.stringify(decodedSdJwt.jwt.payload)); + + const { _sd_alg, payload } = getSDAlgAndPayload(decodedSdJwt.jwt.payload); + const hash = { hasher: digest, alg: _sd_alg }; + const map = await createHashMapping(decodedSdJwt.disclosures, hash); + + const { unpackedObj } = unpackObj(decodedSdJwt.jwt.payload, map); + + expect(unpackedObj).toBeDefined(); + expect(jwtPayload).toStrictEqual(decodedSdJwt.jwt.payload); + }); });