From 9befdf2b63742eba0a678388848bcf85d2e9753e Mon Sep 17 00:00:00 2001 From: Orie Steele Date: Sat, 22 Jun 2024 10:40:23 -0500 Subject: [PATCH] factoring --- src/jose-hpke/jwe/compact.ts | 31 ++++++++++----- src/jose-hpke/jwk.ts | 48 ++--------------------- src/jose-hpke/jwt/index.ts | 10 ++--- src/jose-hpke/types.ts | 21 +++++++--- tests/jwt-auth-psk.test.ts | 41 ++++++++++++++++++++ tests/jwt.test.ts | 74 ++++++++++++++++++++---------------- 6 files changed, 128 insertions(+), 97 deletions(-) create mode 100644 tests/jwt-auth-psk.test.ts diff --git a/src/jose-hpke/jwe/compact.ts b/src/jose-hpke/jwe/compact.ts index 44032e4..100f516 100644 --- a/src/jose-hpke/jwe/compact.ts +++ b/src/jose-hpke/jwe/compact.ts @@ -1,16 +1,21 @@ import { base64url } from "jose"; import { privateKeyFromJwk, publicKeyFromJwk } from "../../crypto/keys"; -import { isKeyAlgorithmSupported, suites } from "../jwk"; +import { isKeyAlgorithmSupported } from "../jwk"; +import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js"; -import { HPKE_JWT_OPTIONS } from '../types' +import { HPKE_JWT_ENCRYPT_OPTIONS } from '../types' -export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any, options?: HPKE_JWT_OPTIONS): Promise => { +export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any, options?: HPKE_JWT_ENCRYPT_OPTIONS): Promise => { if (!isKeyAlgorithmSupported(publicKeyJwk)) { throw new Error('Public key algorithm is not supported') } - const suite = suites[publicKeyJwk.alg] + const suite = new CipherSuite({ + kem: KemId.DhkemP256HkdfSha256, + kdf: KdfId.HkdfSha256, + aead: AeadId.Aes128Gcm, + }) const sender = await suite.createSenderContext({ recipientPublicKey: await publicKeyFromJwk(publicKeyJwk), }); @@ -19,11 +24,13 @@ export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any, options? 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) + if (options?.keyManagementParameters){ + 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) @@ -41,7 +48,11 @@ export const decrypt = async (compact: string, privateKeyJwk: any): Promise - -export const isKeyAlgorithmSupported = (recipient: JWK) => { - const supported_alg = Object.keys(suites) as string [] - return supported_alg.includes(`${recipient.alg}`) +export const isKeyAlgorithmSupported = (recipient: Record) => { + return recipient.alg === 'HPKE-Base-P256-SHA256-A128GCM' } export const formatJWK = (jwk: any) => { @@ -58,10 +19,7 @@ export const publicFromPrivate = (privateKeyJwk: any) => { } } -export const generate = async (alg: JOSE_HPKE_ALG) => { - if (!suites[alg]){ - throw new Error('Algorithm not supported') - } +export const generate = async (alg: 'HPKE-Base-P256-SHA256-A128GCM') => { let kp; if (alg.includes('P256')){ kp = await generateKeyPair('ECDH-ES+A256KW', { crv: 'P-256', extractable: true }) diff --git a/src/jose-hpke/jwt/index.ts b/src/jose-hpke/jwt/index.ts index 8954821..9ca637f 100644 --- a/src/jose-hpke/jwt/index.ts +++ b/src/jose-hpke/jwt/index.ts @@ -1,19 +1,19 @@ import { base64url } from 'jose' -import { HPKE_JWT_OPTIONS } from '../types' +import { HPKE_JWT_ENCRYPT_OPTIONS, HPKE_JWT_DECRYPT_OPTIONS } from '../types' import * as jwe from '../jwe' const encoder = new TextEncoder() const decoder = new TextDecoder() -export const encryptJWT = async (claims: Record, publicKey: Record, options?: HPKE_JWT_OPTIONS) => { +export const encryptJWT = async (claims: Record, options?: HPKE_JWT_ENCRYPT_OPTIONS) => { const plaintext = encoder.encode(JSON.stringify(claims)) - return jwe.compact.encrypt(plaintext, publicKey, options) + return jwe.compact.encrypt(plaintext, options?.recipientPublicKey, options) } -export const decryptJWT = async (jwt: string, privateKey: Record) => { - const plaintext = await jwe.compact.decrypt(jwt, privateKey) +export const decryptJWT = async (jwt: string, options?: HPKE_JWT_DECRYPT_OPTIONS) => { + const plaintext = await jwe.compact.decrypt(jwt, options?.recipientPrivateKey) const [protectedHeader] = jwt.split('.') return { protectedHeader: JSON.parse(decoder.decode(base64url.decode(protectedHeader))), diff --git a/src/jose-hpke/types.ts b/src/jose-hpke/types.ts index 829668e..1f463fd 100644 --- a/src/jose-hpke/types.ts +++ b/src/jose-hpke/types.ts @@ -1,6 +1,17 @@ -export type HPKE_JWT_OPTIONS = { - keyManagementParameters: { - apu: Uint8Array, - apv: Uint8Array + +export type HPKE_JWT_ENCRYPT_OPTIONS = { + senderPrivateKey?: Record, + recipientPublicKey?: Record, + keyManagementParameters?: { + id?: Uint8Array, + key?: Uint8Array + apu?: Uint8Array, + apv?: Uint8Array } -} \ No newline at end of file +} + +export type HPKE_JWT_DECRYPT_OPTIONS = { + senderPublicKey: Record, + recipientPrivateKey: Record, +} + diff --git a/tests/jwt-auth-psk.test.ts b/tests/jwt-auth-psk.test.ts new file mode 100644 index 0000000..b806560 --- /dev/null +++ b/tests/jwt-auth-psk.test.ts @@ -0,0 +1,41 @@ +import moment from 'moment' + +import { jose as hpke } from '../src' + +const claims = { 'urn:example:claim': true } + +it('Encrypted JWT with HPKE-Base-P256-SHA256-A128GCM, and pre shared key', 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, + }, + { + senderPrivateKey: privateKey, + recipientPublicKey: publicKey, + keyManagementParameters: { + id: new TextEncoder().encode("our-pre-shared-key-id"), + // a PSK MUST have at least 32 bytes. + key: new TextEncoder().encode("jugemujugemugokounosurikirekaija"), + } + }) + // protected.encapsulated_key..ciphertext. + const result = await hpke.jwt.decryptJWT(jwe, { + senderPublicKey: publicKey, + recipientPrivateKey: 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') + +}) \ No newline at end of file diff --git a/tests/jwt.test.ts b/tests/jwt.test.ts index 7b9fdb5..87fdfca 100644 --- a/tests/jwt.test.ts +++ b/tests/jwt.test.ts @@ -1,19 +1,19 @@ import moment from 'moment' import * as jose from 'jose' -import { jose as hpke } from '../src' +import { jose as hpke } from '../src' const claims = { 'urn:example:claim': true } it('Encrypted JWT with ECDH-ES+A128KW and A128GCM', async () => { const keys = await jose.generateKeyPair('ECDH-ES+A128KW', { extractable: true }) const jwt = await new jose.EncryptJWT(claims) - .setProtectedHeader({ alg: 'ECDH-ES+A128KW', enc: 'A128GCM' }) - .setIssuedAt() - .setIssuer('urn:example:issuer') - .setAudience('urn:example:audience') - .setExpirationTime('2h') - .encrypt(keys.privateKey) + .setProtectedHeader({ alg: 'ECDH-ES+A128KW', enc: 'A128GCM' }) + .setIssuedAt() + .setIssuer('urn:example:issuer') + .setAudience('urn:example:audience') + .setExpirationTime('2h') + .encrypt(keys.privateKey) // protected.encrypted_key.iv.ciphertext.tag const result = await jose.jwtDecrypt(jwt, keys.privateKey) expect(result.payload['urn:example:claim']).toBe(true) @@ -37,9 +37,15 @@ it('Encrypted JWT with HPKE-Base-P256-SHA256-A128GCM', async () => { aud: 'urn:example:audience', iat, exp, - }, publicKey) + }, { + recipientPublicKey: publicKey + }) // protected.encapsulated_key..ciphertext. - const result = await hpke.jwt.decryptJWT(jwe, privateKey) + const result = await hpke.jwt.decryptJWT( + jwe, + { + recipientPrivateKey: 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') @@ -48,26 +54,26 @@ it('Encrypted JWT with HPKE-Base-P256-SHA256-A128GCM', async () => { expect(result.protectedHeader['alg']).toBe('HPKE-Base-P256-SHA256-A128GCM') expect(result.protectedHeader['enc']).toBe('A128GCM') // protected header does not contain epk - expect(result.protectedHeader.epk).toBeUndefined() + expect(result.protectedHeader.epk).toBeUndefined() // encapsulated key is transported through "encrypted_key" }) it('Encrypted JWT with ECDH-ES+A128KW and A128GCM, and party info', async () => { const keys = await jose.generateKeyPair('ECDH-ES+A128KW', { extractable: true }) const jwt = await new jose.EncryptJWT(claims) - .setKeyManagementParameters({ - "apu": jose.base64url.decode("QWxpY2U"), - "apv": jose.base64url.decode("Qm9i"), - }) - .setProtectedHeader({ - alg: 'ECDH-ES+A128KW', - enc: 'A128GCM' - }) - .setIssuedAt() - .setIssuer('urn:example:issuer') - .setAudience('urn:example:audience') - .setExpirationTime('2h') - .encrypt(keys.privateKey) + .setKeyManagementParameters({ + "apu": jose.base64url.decode("QWxpY2U"), + "apv": jose.base64url.decode("Qm9i"), + }) + .setProtectedHeader({ + alg: 'ECDH-ES+A128KW', + enc: 'A128GCM' + }) + .setIssuedAt() + .setIssuer('urn:example:issuer') + .setAudience('urn:example:audience') + .setExpirationTime('2h') + .encrypt(keys.privateKey) // protected.encrypted_key.iv.ciphertext.tag const result = await jose.jwtDecrypt(jwt, keys.privateKey) expect(result.payload['urn:example:claim']).toBe(true) @@ -93,16 +99,20 @@ it('Encrypted JWT with HPKE-Base-P256-SHA256-A128GCM, and party info ', async () aud: 'urn:example:audience', iat, exp, - }, - publicKey, - { - keyManagementParameters: { - "apu": jose.base64url.decode("QWxpY2U"), - "apv": jose.base64url.decode("Qm9i"), - } - }) + }, + { + recipientPublicKey: publicKey, + keyManagementParameters: { + "apu": jose.base64url.decode("QWxpY2U"), + "apv": jose.base64url.decode("Qm9i"), + } + }) // protected.encapsulated_key..ciphertext. - const result = await hpke.jwt.decryptJWT(jwe, privateKey) + const result = await hpke.jwt.decryptJWT( + jwe, + { + recipientPrivateKey: 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')