Skip to content

Commit

Permalink
add HPKE Wrap
Browse files Browse the repository at this point in the history
  • Loading branch information
OR13 committed Feb 23, 2024
1 parent b7389d9 commit 274bfd9
Show file tree
Hide file tree
Showing 5 changed files with 306 additions and 18 deletions.
229 changes: 229 additions & 0 deletions src/cose/encrypt/hpke.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@

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

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

import crypto from 'crypto';

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

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

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


import * as aes from './aes'
import { encode } from 'cbor-web';

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-AES128GCM']: 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,
})
}

export const isKeyAlgorithmSupported = (recipient: JWK) => {
const supported_alg = Object.keys(suites) as string[]
return supported_alg.includes(`${recipient.alg}`)
}

export const formatJWK = (jwk: any) => {
const { kid, alg, kty, crv, x, y, d } = jwk
return JSON.parse(JSON.stringify({
kid, alg, kty, crv, x, y, d
}))
}

export const publicFromPrivate = (privateKeyJwk: any) => {
const { kid, alg, kty, crv, x, y, ...rest } = privateKeyJwk
return {
kid, alg, kty, crv, x, y
}
}

export const publicKeyFromJwk = async (publicKeyJwk: any) => {
const publicKey = await crypto.subtle.importKey(
'jwk',
publicKeyJwk,
{
name: 'ECDH',
namedCurve: publicKeyJwk.crv,
},
true,
[],
)
return publicKey;
}

export const privateKeyFromJwk = async (privateKeyJwk: any) => {
const privateKey = await crypto.subtle.importKey(
'jwk',
privateKeyJwk,
{
name: 'ECDH',
namedCurve: privateKeyJwk.crv,
},
true,
['deriveBits', 'deriveKey'],
)
return privateKey
}

export const generate = async (alg: JOSE_HPKE_ALG) => {
if (!suites[alg]) {
throw new Error('Algorithm not supported')
}
let kp;
if (alg.includes('P256')) {
kp = await generateKeyPair('ECDH-ES+A256KW', { crv: 'P-256', extractable: true })
} else if (alg.includes('P384')) {
kp = await generateKeyPair('ECDH-ES+A256KW', { crv: 'P-384', extractable: true })
} else {
throw new Error('Could not generate private key for ' + alg)
}
const privateKeyJwk = await exportJWK(kp.privateKey);
privateKeyJwk.kid = await calculateJwkThumbprintUri(privateKeyJwk)
privateKeyJwk.alg = alg;
return formatJWK(privateKeyJwk)
}

export const primaryAlgorithm = {
'label': `HPKE-Base-P256-SHA256-AES128GCM`,
'value': 35
}

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

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

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

const encryptWrap = async (req: RequestWrapEncryption) => {
const alg = req.protectedHeader.get(1)
if (alg !== 1) {
throw new Error('Only A128GCM is supported at this time')
}
const unprotectedHeader = req.unprotectedHeader;
const encodedProtectedHeader = encode(req.protectedHeader)
const cek = await aes.generateKey(alg);
const iv = await aes.getIv(alg);
unprotectedHeader.set(5, iv); // set IV
const senderRecipients = []
for (const recipient of req.recipients.keys) {
const suite = suites[recipient.alg as JOSE_HPKE_ALG]
const sender = await suite.createSenderContext({
recipientPublicKey: await publicKeyFromJwk(recipient),
});
const recipientProtectedHeader = encode(new Map([[
1, 35
]]))
const hpkeSealAad = computeHPKEAad(encodedProtectedHeader, recipientProtectedHeader)
const encryptedKey = await sender.seal(cek, hpkeSealAad)
const encapsulatedKey = Buffer.from(sender.enc);
const recipientCoseKey = new Map<any, any>([
[1, 5], // kty: EK
[- 1, encapsulatedKey]
])
const recipientUnprotectedHeader = new Map([
[4, recipient.kid], // kid
[-1, recipientCoseKey], // epk
])
senderRecipients.push([
recipientProtectedHeader,
recipientUnprotectedHeader,
encryptedKey
])
}
const aad = await createAAD(encodedProtectedHeader, 'Encrypt', EMPTY_BUFFER)
const ciphertext = await aes.encrypt(
alg,
new Uint8Array(req.plaintext),
new Uint8Array(iv),
new Uint8Array(aad),
new Uint8Array(cek)
)
const COSE_Encrypt = [
encodedProtectedHeader,
unprotectedHeader,
ciphertext,
senderRecipients
]
return encodeAsync(new Tagged(COSE_Encrypt_Tag, COSE_Encrypt), { canonical: true })
}

export const encrypt = {
wrap: encryptWrap
}

export const decryptWrap = async (req: RequestWrapDecryption) => {
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)
const contentEncryptionKey = await hpkeRecipient.open(recipientCipherText, hpkeSealAad)
const iv = unprotectedHeader.get(5)
const aad = await createAAD(protectedHeader, 'Encrypt', EMPTY_BUFFER) // good
const decodedProtectedHeader = await decodeFirst(protectedHeader)
const alg = decodedProtectedHeader.get(1)
return aes.decrypt(alg, ciphertext, new Uint8Array(iv), new Uint8Array(aad), new Uint8Array(contentEncryptionKey))
}

export const decrypt = {
wrap: decryptWrap
}
17 changes: 17 additions & 0 deletions src/cose/encrypt/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,20 @@ export async function createAAD(protectedHeader: BufferSource, context: any, ext
];
return encodeAsync(encStructure);
}

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


export type RequestWrapDecryption = {
ciphertext: any,
recipients: {
keys: any[]
}
}
32 changes: 15 additions & 17 deletions src/cose/encrypt/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,11 @@ import { Tagged, decodeFirst, encodeAsync } from "cbor-web"

import * as aes from './aes'
import * as ecdh from './ecdh'
import { createAAD, COSE_Encrypt_Tag } from './utils'
import { createAAD, COSE_Encrypt_Tag, RequestWrapDecryption, RequestWrapEncryption } from './utils'

import { EMPTY_BUFFER } from "../../cbor"

export type RequestWrapDecryption = {
ciphertext: any,
recipients: {
keys: any[]
}
}
import * as hpke from './hpke'

export const decrypt = async (req: RequestWrapDecryption) => {
const decoded = await decodeFirst(req.ciphertext)
Expand All @@ -28,6 +23,9 @@ export const decrypt = async (req: RequestWrapDecryption) => {
const receiverPrivateKeyJwk = req.recipients.keys.find((k) => {
return k.kid === kid
})
if (receiverPrivateKeyJwk.alg === 'HPKE-Base-P256-SHA256-AES128GCM') {
return hpke.decrypt.wrap(req)
}
const decodedRecipientProtectedHeader = await decodeFirst(recipientProtectedHeader)
const recipientAlgorithm = decodedRecipientProtectedHeader.get(1)
const epk = recipientUnprotectedHeader.get(-1)
Expand All @@ -40,22 +38,14 @@ export const decrypt = async (req: RequestWrapDecryption) => {
const aad = await createAAD(protectedHeader, 'Encrypt', EMPTY_BUFFER) // good
let kwAlg = -3
if (recipientAlgorithm === -29) { // ECDH-ES-A128KW
kwAlg = -3
kwAlg = -3 // A128KW
}
const cek = await aes.unwrap(kwAlg, recipientCipherText, new Uint8Array(kek))
const decodedProtectedHeader = await decodeFirst(protectedHeader)
const alg = decodedProtectedHeader.get(1)
return aes.decrypt(alg, ciphertext, new Uint8Array(iv), new Uint8Array(aad), new Uint8Array(cek))
}

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


const getCoseAlgFromRecipientJwk = (jwk: any) => {
Expand All @@ -73,7 +63,15 @@ export const encrypt = async (req: RequestWrapEncryption) => {
if (recipientPublicKeyJwk.crv !== 'P-256') {
throw new Error('Only P-256 is supported currently')
}

if (recipientPublicKeyJwk.alg === 'HPKE-Base-P256-SHA256-AES128GCM') {
return hpke.encrypt.wrap(req)
}

const alg = req.protectedHeader.get(1)
if (alg !== 1) {
throw new Error('Only A128GCM is supported currently')
}
const protectedHeader = await encodeAsync(req.protectedHeader)
const unprotectedHeader = req.unprotectedHeader;
const keyAgreementWithKeyWrappingAlgorithm = getCoseAlgFromRecipientJwk(recipientPublicKeyJwk)
Expand All @@ -87,7 +85,7 @@ export const encrypt = async (req: RequestWrapEncryption) => {
unprotectedHeader.set(5, iv)
let kwAlg = -3
if (keyAgreementWithKeyWrappingAlgorithm === -29) { // ECDH-ES-A128KW
kwAlg = -3
kwAlg = -3 // A128KW
}
const encryptedKey = await aes.wrap(kwAlg, cek, new Uint8Array(kek))
const senderPublicKeyJwk = publicFromPrivate<any>(senderPrivateKeyJwk)
Expand Down
3 changes: 2 additions & 1 deletion src/cose/key/convertCoseKeyToJsonWebKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export const convertCoseKeyToJsonWebKey = async <T>(coseKey: CoseKey): Promise<T
const kid = coseKey.get(2)
const alg = coseKey.get(3)
const crv = coseKey.get(-1)
if (![2].includes(kty)) {
// kty EC, kty: EK
if (![2, 5].includes(kty)) {
throw new Error('This library requires does not support the given key type')
}
const foundAlgorithm = algorithms.find((param) => {
Expand Down
43 changes: 43 additions & 0 deletions test/hpke.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as transmute from '../src'

it('wrap', async () => {
const protectedHeader = new Map<number, any>([
[1, 1], // alg : A128GCM
])
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.wrap({
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.wrap({
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 274bfd9

Please sign in to comment.