Skip to content

Commit

Permalink
feat: add ECDH-1PU+A256KW support
Browse files Browse the repository at this point in the history
WARNING: still using 1PUv3 spec
  • Loading branch information
mirceanis committed Apr 19, 2023
1 parent a12e833 commit cca18f3
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 13 deletions.
8 changes: 4 additions & 4 deletions src/__tests__/JWE.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
} from '../encryption/xc20pEncryption.js'
import { base64ToBytes, decodeBase64url, encodeBase64url } from '../util.js'
import { createX25519ECDH, ECDH } from '../encryption/ECDH.js'
import { x25519DecrypterWithA256KW, x25519EncrypterWithA256KW } from '../encryption/aesEncryption.js'
import { xc20pAnonDecrypterX25519WithA256KW, xc20pAnonEncrypterX25519WithA256KW } from '../encryption/aesEncryption.js'
import { xc20pDirDecrypter, xc20pDirEncrypter } from '../encryption/xc20pDir'

const u8a = { toString, fromString }
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('JWE', () => {
async ({ key, cleartext, jwe }) => {
expect.assertions(1)
const receiverSecret = base64ToBytes(key)
const decrypter = x25519DecrypterWithA256KW(receiverSecret)
const decrypter = xc20pAnonDecrypterX25519WithA256KW(receiverSecret)
const cleartextU8a = await decryptJWE(jwe as any, decrypter)
expect(u8a.toString(cleartextU8a)).toEqual(cleartext)
}
Expand Down Expand Up @@ -201,8 +201,8 @@ describe('JWE', () => {
secretkey = randomBytes(32)
pubkey = generateKeyPairFromSeed(secretkey).publicKey
cleartext = u8a.fromString('hello world')
encrypter = x25519EncrypterWithA256KW(pubkey)
decrypter = x25519DecrypterWithA256KW(secretkey)
encrypter = xc20pAnonEncrypterX25519WithA256KW(pubkey)
decrypter = xc20pAnonDecrypterX25519WithA256KW(secretkey)
})

it('Creates with only ciphertext', async () => {
Expand Down
4 changes: 2 additions & 2 deletions src/encryption/X25519-ECDH-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export async function computeX25519EcdhEsKek(recipient: Recipient, receiverSecre

export function createX25519EcdhEsKek(
ephemeralKeyPair: EphemeralKeyPair | undefined,
publicKey: Uint8Array,
recipientPublicKey: Uint8Array,
apv: string | undefined,
alg: string
) {
Expand All @@ -37,7 +37,7 @@ export function createX25519EcdhEsKek(
? generateKeyPairFromSeed(ephemeralKeyPair.secretKey)
: generateKeyPair()
const epk = { kty: 'OKP', crv, x: bytesToBase64url(ephemeral.publicKey) }
const sharedSecret = sharedKey(ephemeral.secretKey, publicKey)
const sharedSecret = sharedKey(ephemeral.secretKey, recipientPublicKey)
// Key Encryption Key
const consumerInfo = base64ToBytes(apv ?? '')
const kek = concatKDF(sharedSecret, keyLen, alg, undefined, consumerInfo)
Expand Down
102 changes: 98 additions & 4 deletions src/encryption/aesEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { randomBytes } from '@stablelib/random'
import crypto from 'isomorphic-webcrypto'
import { Decrypter, Encrypter, EncryptionResult, EphemeralKeyPair, ProtectedHeader, Recipient } from './JWE.js'
import { base64ToBytes, bytesToBase64url } from '../util.js'
import { genX25519EphemeralKeyPair } from './xc20pEncryption.js'
import { AuthEncryptParams, genX25519EphemeralKeyPair } from './xc20pEncryption.js'
import { ECDH } from './ECDH.js'
import { KeyUnwrapper, KeyWrapper } from './KW.js'
import { xc20pDirDecrypter, xc20pDirEncrypter } from './xc20pDir.js'
import { computeX25519EcdhEsKek, createX25519EcdhEsKek } from './X25519-ECDH-ES.js'
import { computeX25519Ecdh1PUv3Kek, createX25519Ecdh1PUv3Kek } from './X25519-ECDH-1PU.js'

/**
* Creates a wrapper using AES-KW
Expand Down Expand Up @@ -64,12 +65,16 @@ export async function a256KeyUnwrapper(wrappingKey: Uint8Array): Promise<KeyUnwr
return { unwrap, alg: 'A256KW' }
}

export function x25519EncrypterWithA256KW(publicKey: Uint8Array, kid?: string, apv?: string): Encrypter {
export function xc20pAnonEncrypterX25519WithA256KW(
recipientPublicKey: Uint8Array,
kid?: string,
apv?: string
): Encrypter {
const alg = 'ECDH-ES+A256KW'
const enc = 'XC20P'

async function encryptCek(cek: Uint8Array, ephemeralKeyPair?: EphemeralKeyPair): Promise<Recipient> {
const { epk, kek } = createX25519EcdhEsKek(ephemeralKeyPair, publicKey, apv, alg)
const { epk, kek } = createX25519EcdhEsKek(ephemeralKeyPair, recipientPublicKey, apv, alg)
const wrapper = await a256KeyWrapper(kek)
const res = await wrapper.wrap(cek)
const recipient: Recipient = {
Expand Down Expand Up @@ -110,7 +115,7 @@ export function x25519EncrypterWithA256KW(publicKey: Uint8Array, kid?: string, a
return { alg, enc, encrypt, encryptCek, genEpk: genX25519EphemeralKeyPair }
}

export function x25519DecrypterWithA256KW(receiverSecret: Uint8Array | ECDH): Decrypter {
export function xc20pAnonDecrypterX25519WithA256KW(receiverSecret: Uint8Array | ECDH): Decrypter {
const alg = 'ECDH-ES+A256KW'
const enc = 'XC20P'

Expand All @@ -133,3 +138,92 @@ export function x25519DecrypterWithA256KW(receiverSecret: Uint8Array | ECDH): De

return { alg, enc, decrypt }
}

export function xc20pAuthEncrypterEcdh1PuV3x25519WithA256KW(
recipientPublicKey: Uint8Array,
senderSecret: Uint8Array | ECDH,
options: Partial<AuthEncryptParams> = {}
): Encrypter {
const alg = 'ECDH-ES+A256KW'
const enc = 'XC20P'

async function encryptCek(cek: Uint8Array, ephemeralKeyPair?: EphemeralKeyPair): Promise<Recipient> {
const { epk, kek } = await createX25519Ecdh1PUv3Kek(
ephemeralKeyPair,
recipientPublicKey,
senderSecret,
options.apu,
options.apv,
alg
)
const wrapper = await a256KeyWrapper(kek)
const res = await wrapper.wrap(cek)
const recipient: Recipient = {
encrypted_key: bytesToBase64url(res.ciphertext),
header: {},
}
if (res.iv) recipient.header.iv = bytesToBase64url(res.iv)
if (res.tag) recipient.header.tag = bytesToBase64url(res.tag)
if (options.kid) recipient.header.kid = options.kid
if (options.apu) recipient.header.apu = options.apu
if (options.apv) recipient.header.apv = options.apv
if (!ephemeralKeyPair) {
recipient.header.alg = alg
recipient.header.epk = epk
}

return recipient
}

async function encrypt(
cleartext: Uint8Array,
protectedHeader: ProtectedHeader = {},
aad?: Uint8Array,
ephemeralKeyPair?: EphemeralKeyPair
): Promise<EncryptionResult> {
// we won't want alg to be set to dir from xc20pDirEncrypter
Object.assign(protectedHeader, { alg: undefined })
// Content Encryption Key
const cek = randomBytes(32)
const recipient: Recipient = await encryptCek(cek, ephemeralKeyPair)
if (ephemeralKeyPair) {
protectedHeader.alg = alg
protectedHeader.epk = ephemeralKeyPair.publicKeyJWK
}
return {
...(await xc20pDirEncrypter(cek).encrypt(cleartext, protectedHeader, aad)),
recipient,
cek,
}
}

return { alg, enc, encrypt, encryptCek, genEpk: genX25519EphemeralKeyPair }
}

export function xc20pAuthDecrypterEcdh1PuV3x25519WithA256KW(
recipientSecret: Uint8Array | ECDH,
senderPublicKey: Uint8Array
): Decrypter {
const alg = 'ECDH-1PU+A256KW'
const enc = 'XC20P'

async function decrypt(
sealed: Uint8Array,
iv: Uint8Array,
aad?: Uint8Array,
recipient?: Recipient
): Promise<Uint8Array | null> {
recipient = <Recipient>recipient
recipient = <Recipient>recipient
const kek = await computeX25519Ecdh1PUv3Kek(recipient, recipientSecret, senderPublicKey, alg)
if (!kek) return null
// Content Encryption Key
const unwrapper = await a256KeyUnwrapper(kek)
const cek = await unwrapper.unwrap(base64ToBytes(recipient.encrypted_key))
if (cek === null) return null

return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad)
}

return { alg, enc, decrypt }
}
9 changes: 6 additions & 3 deletions src/encryption/xc20pEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(
options: Partial<AuthEncryptParams> = {}
): Encrypter {
const alg = 'ECDH-1PU+XC20PKW'
const enc = 'XC20P'

async function encryptCek(cek: Uint8Array, ephemeralKeyPair?: EphemeralKeyPair): Promise<Recipient> {
const { epk, kek } = await createX25519Ecdh1PUv3Kek(
Expand Down Expand Up @@ -247,7 +248,7 @@ export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(
}
}

return { alg, enc: 'XC20P', encrypt, encryptCek, genEpk: genX25519EphemeralKeyPair }
return { alg, enc, encrypt, encryptCek, genEpk: genX25519EphemeralKeyPair }
}

export async function resolveX25519Encrypters(dids: string[], resolver: Resolvable): Promise<Encrypter[]> {
Expand Down Expand Up @@ -301,6 +302,7 @@ export async function resolveX25519Encrypters(dids: string[], resolver: Resolvab

export function x25519Decrypter(receiverSecret: Uint8Array | ECDH): Decrypter {
const alg = 'ECDH-ES+XC20PKW'
const enc = 'XC20P'

async function decrypt(
sealed: Uint8Array,
Expand All @@ -321,7 +323,7 @@ export function x25519Decrypter(receiverSecret: Uint8Array | ECDH): Decrypter {
return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad)
}

return { alg, enc: 'XC20P', decrypt }
return { alg, enc, decrypt }
}

/**
Expand All @@ -334,6 +336,7 @@ export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
senderPublicKey: Uint8Array
): Decrypter {
const alg = 'ECDH-1PU+XC20PKW'
const enc = 'XC20P'

async function decrypt(
sealed: Uint8Array,
Expand All @@ -353,5 +356,5 @@ export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad)
}

return { alg, enc: 'XC20P', decrypt }
return { alg, enc, decrypt }
}

0 comments on commit cca18f3

Please sign in to comment.