Skip to content

Commit

Permalink
factoring
Browse files Browse the repository at this point in the history
  • Loading branch information
OR13 committed Jun 22, 2024
1 parent 3ae2b75 commit 9befdf2
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 97 deletions.
31 changes: 21 additions & 10 deletions src/jose-hpke/jwe/compact.ts
Original file line number Diff line number Diff line change
@@ -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<string> => {
export const encrypt = async (plaintext: Uint8Array, publicKeyJwk: any, options?: HPKE_JWT_ENCRYPT_OPTIONS): Promise<string> => {
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),
});
Expand All @@ -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<string, any>
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)
Expand All @@ -41,7 +48,11 @@ export const decrypt = async (compact: string, privateKeyJwk: any): Promise<Uint
if (!isKeyAlgorithmSupported(privateKeyJwk)) {
throw new Error('Public key algorithm is not supported')
}
const suite = suites[privateKeyJwk.alg]
const suite = new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
})
const [protectedHeader, encrypted_key, iv, ciphertext, tag] = compact.split('.');
const recipient = await suite.createRecipientContext({
recipientKey: await privateKeyFromJwk(privateKeyJwk),
Expand Down
48 changes: 3 additions & 45 deletions src/jose-hpke/jwk.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,8 @@

import { generateKeyPair, exportJWK, calculateJwkThumbprintUri } from "jose"

import { AeadId, CipherSuite, KdfId, KemId } from "hpke-js";

export type JOSE_HPKE_ALG = `HPKE-Base-P256-SHA256-A128GCM` | `HPKE-Base-P384-SHA256-AES128GCM`

export type JWK = {
kid?:string
alg?: string
kty: string
crv: string
}

export type JWKS = {
keys: JWK[]
}

export type HPKERecipient = {
encrypted_key: string
header: {
kid?: string
alg?: string
epk?: JWK
encapsulated_key: string,
}
}

export const suites = {
['HPKE-Base-P256-SHA256-A128GCM']: new CipherSuite({
kem: KemId.DhkemP256HkdfSha256,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
}),
['HPKE-Base-P384-SHA256-AES128GCM']: new CipherSuite({
kem: KemId.DhkemP384HkdfSha384,
kdf: KdfId.HkdfSha256,
aead: AeadId.Aes128Gcm,
})
} as Record<any, CipherSuite>

export const isKeyAlgorithmSupported = (recipient: JWK) => {
const supported_alg = Object.keys(suites) as string []
return supported_alg.includes(`${recipient.alg}`)
export const isKeyAlgorithmSupported = (recipient: Record<string, any>) => {
return recipient.alg === 'HPKE-Base-P256-SHA256-A128GCM'
}

export const formatJWK = (jwk: any) => {
Expand All @@ -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 })
Expand Down
10 changes: 5 additions & 5 deletions src/jose-hpke/jwt/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>, publicKey: Record<string, any>, options?: HPKE_JWT_OPTIONS) => {
export const encryptJWT = async (claims: Record<string, any>, 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<string, any>) => {
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))),
Expand Down
21 changes: 16 additions & 5 deletions src/jose-hpke/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
export type HPKE_JWT_OPTIONS = {
keyManagementParameters: {
apu: Uint8Array,
apv: Uint8Array

export type HPKE_JWT_ENCRYPT_OPTIONS = {
senderPrivateKey?: Record<string, any>,
recipientPublicKey?: Record<string, any>,
keyManagementParameters?: {
id?: Uint8Array,
key?: Uint8Array
apu?: Uint8Array,
apv?: Uint8Array
}
}
}

export type HPKE_JWT_DECRYPT_OPTIONS = {
senderPublicKey: Record<string, any>,
recipientPrivateKey: Record<string, any>,
}

41 changes: 41 additions & 0 deletions tests/jwt-auth-psk.test.ts
Original file line number Diff line number Diff line change
@@ -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.<no iv>.ciphertext.<no tag>
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')

})
74 changes: 42 additions & 32 deletions tests/jwt.test.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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.<no iv>.ciphertext.<no tag>
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')
Expand All @@ -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)
Expand All @@ -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.<no iv>.ciphertext.<no tag>
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')
Expand Down

0 comments on commit 9befdf2

Please sign in to comment.