Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: the bytes of the output of the hash function must be base64url-encoded. #57

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
});

});