Skip to content

Commit

Permalink
fix: the bytes of the output of the hash function must be base64url-e…
Browse files Browse the repository at this point in the history
…ncoded. (#57)

Signed-off-by: JOYE LIN <[email protected]>
Co-authored-by: Lukas.J.Han <[email protected]>
  • Loading branch information
y12studio and lukasjhan authored Feb 8, 2024
1 parent b4d610f commit 025786b
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 17 deletions.
6 changes: 6 additions & 0 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SDJWTException } from './error';
import { Base64Url } from './base64url';

export const generateSalt = (length: number): string => {
if (length <= 0) {
Expand All @@ -25,5 +26,10 @@ export const getHasher = (algorithm: string = 'SHA-256') => {
return (data: string) => digest(data, algorithm);
};

export const hexToB64Url = (hexString: string) => {
const theBytes = Buffer.from(hexString,'hex')
return Base64Url.encode(theBytes)
}

const toNodeCryptoAlg = (hashAlg: string): string =>
hashAlg.replace('-', '').toLowerCase();
6 changes: 3 additions & 3 deletions src/decoy.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Base64Url } from './base64url';
import { generateSalt, digest } from './crypto';
import { generateSalt, digest , hexToB64Url} from './crypto';
import { Hasher, SaltGenerator } from './type';

export const createDecoy = async (
hasher: Hasher = digest,
saltGenerator: SaltGenerator = generateSalt,
): Promise<string> => {
const salt = saltGenerator(16);
const digest = await hasher(salt);
const decoy = Base64Url.encode(digest);
const decoyHexString = await hasher(salt);
const decoy = hexToB64Url(decoyHexString)
return decoy;
};
26 changes: 19 additions & 7 deletions src/disclosure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Base64Url } from './base64url';
import { SDJWTException } from './error';
import { Hasher } from './type';
import { hexToB64Url} from './crypto';

export type DisclosureData<T> = [string, string, T] | [string, T];

Expand Down Expand Up @@ -35,7 +36,11 @@ export class Disclosure<T> {
}

public encode() {
return Base64Url.encode(JSON.stringify(this.decode()));
return this.encodeRaw(JSON.stringify(this.decode()));
}

public encodeRaw(s: string) {
return Base64Url.encode(s);
}

public decode(): DisclosureData<T> {
Expand All @@ -44,12 +49,19 @@ export class Disclosure<T> {
: [this.salt, this.value];
}

public async digest(hasher: Hasher): Promise<string> {
if (!this._digest) {
const hash = await hasher(this.encode());
this._digest = Base64Url.encode(hash);
}

public async digestRaw(hasher: Hasher, encodeString: string): Promise<string> {
//
// draft-ietf-oauth-selective-disclosure-jwt-07
//
// The bytes of the output of the hash function MUST be base64url-encoded, and are not the bytes making up the (often used) hex
// representation of the bytes of the digest.
//
const hexString = await hasher(encodeString);
this._digest = hexToB64Url(hexString);
return this._digest;
}

public async digest(hasher: Hasher): Promise<string> {
return await this.digestRaw(hasher, this.encode());
}
}
17 changes: 13 additions & 4 deletions src/test/decoy.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { createDecoy } from '../decoy';
import { Base64Url } from '../base64url';
import { digest } from '../crypto';

describe('Decoy', () => {
test('decoy', async () => {
const decoyValue = await createDecoy();
expect(decoyValue.length).toBe(86);
// base64url-encoded sha256 is a 43-octet URL safe string.
expect(decoyValue.length).toBe(43);
});

// ref https://datatracker.ietf.org/doc/draft-ietf-oauth-selective-disclosure-jwt/07/
// *Claim email*:
// * SHA-256 Hash: JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE
// * Disclosure: WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ
// * Contents: ["6Ij7tM-a5iVPGboS5tmvVA", "email", "[email protected]"]
test('apply hasher and saltGenerator', async () => {
const decoyValue = await createDecoy(
async (data) => data,
() => 'salt',
digest,
() => Base64Url.encode('["6Ij7tM-a5iVPGboS5tmvVA", "email", "[email protected]"]'),
);
expect(decoyValue).toBe('c2FsdA');
expect(decoyValue).toBe('JzYjH4svliH0R3PyEMfeZu6Jt69u5qehZo7F7EPYlSE');
});

});
70 changes: 67 additions & 3 deletions src/test/disclosure.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import { generateSalt, digest as hash } from '../crypto';
import { Disclosure } from '../disclosure';
import { generateSalt, digest as hashHex } from '../crypto';
import { Disclosure, DisclosureData } from '../disclosure';
import { SDJWTException } from '../error';
import { Base64Url } from '../base64url';

/*
ref draft-ietf-oauth-selective-disclosure-jwt-07
Claim given_name:
SHA-256 Hash: jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4
Disclosure: WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd
Contents: ["2GLC42sKQveCfGfryNRN9w", "given_name", "John"]
For example, the SHA-256 digest of the Disclosure
WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0 would be uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY.
The SHA-256 digest of the Disclosure
WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0 would be w0I8EKcdCtUPkGCNUrfwVp2xEgNjtoIDlOxc9-PlOhs.
*/
const TestDataDraft7 = {
claimTests: [
{
contents: '["2GLC42sKQveCfGfryNRN9w", "given_name", "John"]',
digest: 'jsu9yVulwQQlhFlM_3JlzMaSFzglhQG0DpfayQwLUK4',
disclosure: 'WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd'
},
],
sha256Tests: [
{
digest: 'uutlBuYeMDyjLLTpf6Jxi7yNkEF35jdyWMn9U7b_RYY',
disclosure: 'WyI2cU1RdlJMNWhhaiIsICJmYW1pbHlfbmFtZSIsICJNw7ZiaXVzIl0'
},
{
digest: 'w0I8EKcdCtUPkGCNUrfwVp2xEgNjtoIDlOxc9-PlOhs',
disclosure: 'WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgIkZSIl0'
},
]
}

describe('Disclosure', () => {
test('create object disclosure', async () => {
Expand Down Expand Up @@ -57,8 +89,40 @@ describe('Disclosure', () => {
test('digest disclosure', async () => {
const salt = generateSalt(16);
const disclosure = new Disclosure([salt, 'name', 'James']);
const digest = await disclosure.digest(hash);
const digest = await disclosure.digest(hashHex);
expect(digest).toBeDefined();
expect(typeof digest).toBe('string');
});


test('should return a digest after calling digest method', async () => {
const givenData: DisclosureData<string> = ["2GLC42sKQveCfGfryNRN9w", "given_name", "John"];
const theDisclosure = new Disclosure(givenData);
//
// JSON.stringify() version
// SHA-256 Hash : 8VHiz7qTXavxvpiTYDCSr_shkUO6qRcVXjkhEnt1os4
// Disclosure: WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ
// Contents: ["2GLC42sKQveCfGfryNRN9w","given_name","John"]
//
// Testing encoding of the data using encodeRaw and encode functions.
// The differences in the output of encodeRaw and encode methods
// arise from the formatting during JSON.stringify operation. encodeRaw retains whitespace while encode does not.
expect(theDisclosure.encodeRaw(TestDataDraft7.claimTests[0].contents)).toBe(TestDataDraft7.claimTests[0].disclosure)
expect(theDisclosure.encode()).toBe('WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ')

//
// Testing digestRaw function. Testing against known digest and disclosure pairs.
// The digest is expected to be same as the known digest when passed with the corresponding disclosure.
//
await expect(theDisclosure.digestRaw(hashHex, TestDataDraft7.claimTests[0].disclosure)).resolves.toBe(TestDataDraft7.claimTests[0].digest)
await expect(theDisclosure.digestRaw(hashHex, 'WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ')).resolves.toBe('8VHiz7qTXavxvpiTYDCSr_shkUO6qRcVXjkhEnt1os4')
await expect(theDisclosure.digest(hashHex)).resolves.toBe('8VHiz7qTXavxvpiTYDCSr_shkUO6qRcVXjkhEnt1os4');
//
// The result of digestRaw changes based on the hashing strategy used. In this test, we are using the test data from 'draft-ietf-oauth-selective-disclosure-jwt-07'.
//
for (const elem of TestDataDraft7.sha256Tests) {
await expect(theDisclosure.digestRaw(hashHex, elem.disclosure)).resolves.toBe(elem.digest)
}
});

});

0 comments on commit 025786b

Please sign in to comment.