diff --git a/README.md b/README.md index 1e177d4..9e4d317 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ 🚧 Experimental 🔥 -This is yet another HPKE implementation for JOSE and COSE. +This is yet another HPKE implementation for JOSE. The purpose of this experimental implementation is to address open questions regarding the IETF drafts. -- [x] Integrated Encryption - how is it signaled in JOSE and COSE, what values are used for "alg" and "enc". +- [x] Integrated Encryption - how is it signaled in JOSE, what values are used for "alg" and "enc". - [ ] Party U / Party V - identity information in key establishment. -- [ ] Cross Mode Attacks - How are they mitigated in COSE and JOSE? + diff --git a/src/jose-hpke/jwe/compact.ts b/src/jose-hpke/jwe/compact.ts index 396fa5b..44032e4 100644 --- a/src/jose-hpke/jwe/compact.ts +++ b/src/jose-hpke/jwe/compact.ts @@ -3,7 +3,10 @@ import { base64url } from "jose"; import { privateKeyFromJwk, publicKeyFromJwk } from "../../crypto/keys"; import { isKeyAlgorithmSupported, suites } from "../jwk"; -export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any): Promise => { + +import { HPKE_JWT_OPTIONS } from '../types' + +export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any, options?: HPKE_JWT_OPTIONS): Promise => { if (!isKeyAlgorithmSupported(publicKeyJwk)) { throw new Error('Public key algorithm is not supported') } @@ -12,11 +15,19 @@ export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any): Promise recipientPublicKey: await publicKeyFromJwk(publicKeyJwk), }); const encodedEncapsulatedKey = base64url.encode(new Uint8Array(sender.enc)) - const protectedHeader = base64url.encode(JSON.stringify({ + const headerParams = { alg: publicKeyJwk.alg, enc: publicKeyJwk.alg.split('-').pop() // HPKE algorithms always end in an AEAD. - })) + } as Record + if (options?.keyManagementParameters.apu){ + headerParams.apu = base64url.encode(options?.keyManagementParameters.apu) + } + if (options?.keyManagementParameters.apv){ + headerParams.apv = base64url.encode(options?.keyManagementParameters.apv) + } + const protectedHeader = base64url.encode(JSON.stringify(headerParams)) const aad = new TextEncoder().encode(protectedHeader) + // apu / apv are protected by aad, not as part of kdf const ciphertext = base64url.encode(new Uint8Array(await sender.seal(plaintext, aad))); const encrypted_key = encodedEncapsulatedKey const iv = `` diff --git a/src/jose-hpke/jwt/index.ts b/src/jose-hpke/jwt/index.ts index f08594a..8954821 100644 --- a/src/jose-hpke/jwt/index.ts +++ b/src/jose-hpke/jwt/index.ts @@ -1,15 +1,15 @@ - -import * as jwe from '../jwe' - import { base64url } from 'jose' +import { HPKE_JWT_OPTIONS } from '../types' + +import * as jwe from '../jwe' const encoder = new TextEncoder() const decoder = new TextDecoder() -export const encryptJWT = async (claims: Record, publicKey: Record) => { +export const encryptJWT = async (claims: Record, publicKey: Record, options?: HPKE_JWT_OPTIONS) => { const plaintext = encoder.encode(JSON.stringify(claims)) - return jwe.compact.encrypt(plaintext, publicKey) + return jwe.compact.encrypt(plaintext, publicKey, options) } export const decryptJWT = async (jwt: string, privateKey: Record) => { diff --git a/src/jose-hpke/types.ts b/src/jose-hpke/types.ts new file mode 100644 index 0000000..829668e --- /dev/null +++ b/src/jose-hpke/types.ts @@ -0,0 +1,6 @@ +export type HPKE_JWT_OPTIONS = { + keyManagementParameters: { + apu: Uint8Array, + apv: Uint8Array + } +} \ No newline at end of file diff --git a/tests/jwt.test.ts b/tests/jwt.test.ts index 701aae2..7b9fdb5 100644 --- a/tests/jwt.test.ts +++ b/tests/jwt.test.ts @@ -80,4 +80,36 @@ it('Encrypted JWT with ECDH-ES+A128KW and A128GCM, and party info', async () => expect(result.protectedHeader.epk).toBeDefined() expect(result.protectedHeader.apu).toBe("QWxpY2U") expect(result.protectedHeader.apv).toBe("Qm9i") +}) + +it('Encrypted JWT with HPKE-Base-P256-SHA256-A128GCM, and party info ', async () => { + const privateKey = await hpke.jwk.generate('HPKE-Base-P256-SHA256-A128GCM') + const publicKey = await hpke.jwk.publicFromPrivate(privateKey) + const iat = moment().unix() + const exp = moment().add(2, 'hours').unix() + const jwe = await hpke.jwt.encryptJWT({ + ...claims, + iss: 'urn:example:issuer', + aud: 'urn:example:audience', + iat, + exp, + }, + publicKey, + { + keyManagementParameters: { + "apu": jose.base64url.decode("QWxpY2U"), + "apv": jose.base64url.decode("Qm9i"), + } + }) + // protected.encapsulated_key..ciphertext. + const result = await hpke.jwt.decryptJWT(jwe, privateKey) + expect(result.payload['urn:example:claim']).toBe(true) + expect(result.payload['iss']).toBe('urn:example:issuer') + expect(result.payload['aud']).toBe('urn:example:audience') + expect(result.payload.iat).toBeDefined() + expect(result.payload.exp).toBeDefined() + expect(result.protectedHeader['alg']).toBe('HPKE-Base-P256-SHA256-A128GCM') + expect(result.protectedHeader['enc']).toBe('A128GCM') + expect(result.protectedHeader.apu).toBe("QWxpY2U") + expect(result.protectedHeader.apv).toBe("Qm9i") }) \ No newline at end of file