Skip to content

Commit

Permalink
add hpke direct
Browse files Browse the repository at this point in the history
  • Loading branch information
OR13 committed Feb 23, 2024
1 parent 274bfd9 commit f9252f2
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 24 deletions.
32 changes: 13 additions & 19 deletions src/cose/encrypt/direct.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

import { convertCoseKeyToJsonWebKey, convertJsonWebKeyToCoseKey, generate, publicFromPrivate } from "../key"
import { JsonWebKey } from "../key"

import { Tagged, decode, decodeFirst, encodeAsync } from "cbor-web"

import { EMPTY_BUFFER } from "../../cbor"
Expand All @@ -9,34 +9,28 @@ import * as aes from './aes'
import * as ecdh from './ecdh'


import { createAAD, COSE_Encrypt_Tag } from './utils'
import { createAAD, COSE_Encrypt_Tag, RequestDirectEncryption, RequestDirectDecryption } from './utils'

export type JWKS = {
keys: JsonWebKey[]
}

export type RequestEncryption = {
protectedHeader: Map<any, any>
unprotectedHeader: Map<any, any>
plaintext: Uint8Array,
recipients: JWKS
}

import * as hpke from './hpke'

const getCoseAlgFromRecipientJwk = (jwk: any) => {
if (jwk.crv === 'P-256') {
return -25 // alg : ECDH-ES + HKDF-256
}
}

export const encrypt = async (req: RequestEncryption) => {
export const encrypt = async (req: RequestDirectEncryption) => {
if (req.recipients.keys.length !== 1) {
throw new Error('Direct encryption requires a single recipient')
}
const recipientPublicKeyJwk = req.recipients.keys[0]
if (recipientPublicKeyJwk.crv !== 'P-256') {
throw new Error('Only P-256 is supported currently')
}
if (recipientPublicKeyJwk.alg === hpke.primaryAlgorithm.label) {
return hpke.encrypt.direct(req)
}
const alg = req.protectedHeader.get(1)
const protectedHeader = await encodeAsync(req.protectedHeader)
const unprotectedHeader = req.unprotectedHeader;
Expand Down Expand Up @@ -67,17 +61,17 @@ export const encrypt = async (req: RequestEncryption) => {
return encodeAsync(new Tagged(COSE_Encrypt_Tag, COSE_Encrypt), { canonical: true })
}

export type RequestDecryption = {
ciphertext: any,
recipients: JWKS
}


export const decrypt = async (req: RequestDecryption) => {
export const decrypt = async (req: RequestDirectDecryption) => {
const decoded = await decodeFirst(req.ciphertext)
if (decoded.tag !== 96) {
throw new Error('Only tag 96 cose encrypt are supported')
}
const receiverPrivateKeyJwk = req.recipients.keys[0]
if (receiverPrivateKeyJwk.alg === hpke.primaryAlgorithm.label) {
return hpke.decrypt.direct(req)
}
const [protectedHeader, unprotectedHeader, ciphertext, recipients] = decoded.value
if (recipients.length !== 1) {
throw new Error('Expected a single recipient for direct decryption')
Expand All @@ -93,7 +87,7 @@ export const decrypt = async (req: RequestDecryption) => {
// ensure the epk has the algorithm that is set in the protected header
epk.set(3, recipientAlgorithm)
const senderPublicKeyJwk = await convertCoseKeyToJsonWebKey(epk)
const receiverPrivateKeyJwk = req.recipients.keys[0]

const cek = await ecdh.deriveKey(protectedHeader, recipientProtectedHeader, senderPublicKeyJwk, receiverPrivateKeyJwk)
const aad = await createAAD(protectedHeader, 'Encrypt', EMPTY_BUFFER)
const iv = unprotectedHeader.get(5)
Expand Down
75 changes: 71 additions & 4 deletions src/cose/encrypt/hpke.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

import { createAAD, COSE_Encrypt_Tag, RequestWrapDecryption, RequestWrapEncryption } from './utils'
import { createAAD, COSE_Encrypt_Tag, RequestWrapDecryption, RequestWrapEncryption, RequestDirectEncryption, RequestDirectDecryption } from './utils'
import { EMPTY_BUFFER } from "../../cbor"

import { Tagged, decodeFirst, encodeAsync } from "cbor-web"
Expand All @@ -16,6 +16,7 @@ export type JOSE_HPKE_ALG = `HPKE-Base-P256-SHA256-AES128GCM` | `HPKE-Base-P384-
import * as aes from './aes'
import { encode } from 'cbor-web';


export type JWK = {
kid?: string
alg?: string
Expand Down Expand Up @@ -123,15 +124,18 @@ export const primaryAlgorithm = {

export const secondaryAlgorithm = {
'label': `HPKE-Base-P384-SHA384-AES256GCM`,
'value': 35
'value': 37
}

export const directAlgorithm = {
'label': `HPKE-Direct`,
'value': 36
}

const computeHPKEAad = (protectedHeader: any, protectedRecipientHeader: any) => {
const computeHPKEAad = (protectedHeader: any, protectedRecipientHeader: any, direct = false) => {
if (direct) {
return protectedHeader
}
return encode([protectedHeader, protectedRecipientHeader])
}

Expand Down Expand Up @@ -188,10 +192,44 @@ const encryptWrap = async (req: RequestWrapEncryption) => {
return encodeAsync(new Tagged(COSE_Encrypt_Tag, COSE_Encrypt), { canonical: true })
}

export const encryptDirect = async (req: RequestDirectEncryption) => {
const protectedHeader = await encodeAsync(req.protectedHeader)
const unprotectedHeader = req.unprotectedHeader;
const [recipientPublicKeyJwk] = req.recipients.keys
const suite = suites[recipientPublicKeyJwk.alg as JOSE_HPKE_ALG]
const sender = await suite.createSenderContext({
recipientPublicKey: await publicKeyFromJwk(recipientPublicKeyJwk),
});
const recipientProtectedHeader = await encodeAsync(new Map<number, any>([
[1, primaryAlgorithm.value],
]))
const hpkeSealAad = computeHPKEAad(protectedHeader, recipientProtectedHeader, true)
const ciphertext = await sender.seal(req.plaintext, hpkeSealAad)
const recipientCoseKey = new Map<any, any>([
[1, 5], // kty: EK
[- 1, sender.enc]
])
const recipientUnprotectedHeader = new Map([
[4, recipientPublicKeyJwk.kid], // kid
[-1, recipientCoseKey], // epk
])
const recipients = [[recipientProtectedHeader, recipientUnprotectedHeader, EMPTY_BUFFER]]
const COSE_Encrypt = [
protectedHeader,
unprotectedHeader,
ciphertext,
recipients
]
return encodeAsync(new Tagged(COSE_Encrypt_Tag, COSE_Encrypt), { canonical: true })
}


export const encrypt = {
direct: encryptDirect,
wrap: encryptWrap
}


export const decryptWrap = async (req: RequestWrapDecryption) => {
const decoded = await decodeFirst(req.ciphertext)
if (decoded.tag !== 96) {
Expand Down Expand Up @@ -224,6 +262,35 @@ export const decryptWrap = async (req: RequestWrapDecryption) => {
return aes.decrypt(alg, ciphertext, new Uint8Array(iv), new Uint8Array(aad), new Uint8Array(contentEncryptionKey))
}

export const decryptDirect = async (req: RequestDirectDecryption) => {
const decoded = await decodeFirst(req.ciphertext)
if (decoded.tag !== 96) {
throw new Error('Only tag 96 cose encrypt are supported')
}
const [protectedHeader, unprotectedHeader, ciphertext, recipients] = decoded.value
const [recipient] = recipients

const [recipientProtectedHeader, recipientUnprotectedHeader, recipientCipherText] = recipient
const kid = recipientUnprotectedHeader.get(4).toString();
const receiverPrivateKeyJwk = req.recipients.keys.find((k) => {
return k.kid === kid
})
const decodedRecipientProtectedHeader = await decodeFirst(recipientProtectedHeader)
const recipientAlgorithm = decodedRecipientProtectedHeader.get(1)
const epk = recipientUnprotectedHeader.get(-1)
// ensure the epk has the algorithm that is set in the protected header
epk.set(3, recipientAlgorithm) // EPK is allowed to have an alg
const suite = suites[receiverPrivateKeyJwk.alg as JOSE_HPKE_ALG]
const hpkeRecipient = await suite.createRecipientContext({
recipientKey: await privateKeyFromJwk(receiverPrivateKeyJwk),
enc: epk.get(-1) // ek
})
const hpkeSealAad = computeHPKEAad(protectedHeader, recipientProtectedHeader, true)
const plaintext = await hpkeRecipient.open(ciphertext, hpkeSealAad)
return plaintext
}

export const decrypt = {
wrap: decryptWrap
wrap: decryptWrap,
direct: decryptDirect
}
25 changes: 24 additions & 1 deletion src/cose/encrypt/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { encodeAsync } from "cbor-web"
// @ts-ignore
// eslint-disable-next-line @typescript-eslint/no-empty-function
const nodeCrypto = import('crypto').catch(() => { }) as any
import { JsonWebKey } from "../key"

export const COSE_Encrypt_Tag = 96

Expand All @@ -16,7 +17,6 @@ export const getRandomBytes = async (byteLength = 16) => {
}
}


export async function createAAD(protectedHeader: BufferSource, context: any, externalAAD: BufferSource) {
const encStructure = [
context,
Expand All @@ -26,6 +26,11 @@ export async function createAAD(protectedHeader: BufferSource, context: any, ext
return encodeAsync(encStructure);
}


export type JWKS = {
keys: JsonWebKey[]
}

export type RequestWrapEncryption = {
protectedHeader: Map<any, any>
unprotectedHeader: Map<any, any>
Expand All @@ -42,3 +47,21 @@ export type RequestWrapDecryption = {
keys: any[]
}
}


export type RequestDirectEncryption = {
protectedHeader: Map<any, any>
unprotectedHeader: Map<any, any>
plaintext: Uint8Array,
recipients: {
keys: any[]
}
}

export type RequestDirectDecryption = {
ciphertext: any,
recipients: {
keys: any[]
}
}

43 changes: 43 additions & 0 deletions test/hpke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,47 @@ it('wrap', async () => {
}
})
expect(new TextDecoder().decode(decrypted)).toBe("πŸ’€ My lungs taste the air of Time Blown past falling sands βŒ›")
})


it('direct', async () => {
const protectedHeader = new Map<number, any>([
[1, 35], // alg : Direct || HPKE-Base-P256-SHA256-AES128GCM
])
const unprotectedHeader = new Map<number, any>([])
const plaintext = new TextEncoder().encode("πŸ’€ My lungs taste the air of Time Blown past falling sands βŒ›")
const ct = await transmute.encrypt.direct({
protectedHeader,
unprotectedHeader,
plaintext,
recipients: {
keys: [{
"kid": "[email protected]",
"alg": "HPKE-Base-P256-SHA256-AES128GCM",
"kty": "EC",
"crv": "P-256",
"x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0",
"y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw",
// encrypt to public keys only
// "d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8"
}]
}
})
const decoded = transmute.cbor.decodeFirstSync(ct);
expect(decoded.tag).toBe(96)
const decrypted = await transmute.decrypt.direct({
ciphertext: ct,
recipients: {
keys: [{
"kid": "[email protected]",
"alg": "HPKE-Base-P256-SHA256-AES128GCM",
"kty": "EC",
"crv": "P-256",
"x": "Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0",
"y": "HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw",
"d": "r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8"
}]
}
})
expect(new TextDecoder().decode(decrypted)).toBe("πŸ’€ My lungs taste the air of Time Blown past falling sands βŒ›")
})

0 comments on commit f9252f2

Please sign in to comment.