Skip to content

Commit

Permalink
feat: enable remote ECDH for JWE [de]encrypters
Browse files Browse the repository at this point in the history
fixes #183
  • Loading branch information
mirceanis committed Jun 8, 2021
1 parent 3b63138 commit b018794
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 18 deletions.
25 changes: 25 additions & 0 deletions src/ECDH.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { sharedKey } from '@stablelib/x25519'

/**
* A wrapper around `mySecretKey` that can compute a shared secret using `theirPublicKey`
* The promise should resolve to a Uint8Array containing the raw shared secret.
*
*/
export type ECDH = (theirPublicKey: Uint8Array) => Promise<Uint8Array>

/**
* Wraps an X25519 secretKey into an ECDH method that can be used to compute a shared secret with a publicKey.
* @param mySecretKey A `Uint8Array` representing the bytes of my secret key
* @returns an `ECDH` method with the signature `(theirPublicKey: Uint8Array) => Promise<Uint8Array>`
*/
export function createX25519ECDH(mySecretKey: Uint8Array): ECDH {
if (mySecretKey.length !== 32) {
throw new Error('invalid_argument: incorrect secret key length for X25519')
}
return async (theirPublicKey: Uint8Array): Promise<Uint8Array> => {
if (theirPublicKey.length !== 32) {
throw new Error('invalid_argument: incorrect publicKey key length for X25519')
}
return sharedKey(mySecretKey, theirPublicKey)
}
}
50 changes: 48 additions & 2 deletions src/__tests__/JWE.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { decryptJWE, createJWE, Encrypter } from '../JWE'
import { decryptJWE, createJWE, Encrypter, JWE } from '../JWE'
import vectors from './jwe-vectors.js'
import {
xc20pDirEncrypter,
xc20pDirDecrypter,
x25519Encrypter,
x25519Decrypter,
xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2,
xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2
xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2,
createAnonEncrypter,
createAnonDecrypter,
createAuthEncrypter,
createAuthDecrypter
} from '../xc20pEncryption'
import { decodeBase64url, encodeBase64url } from '../util'
import * as u8a from 'uint8arrays'
import { randomBytes } from '@stablelib/random'
import { generateKeyPairFromSeed } from '@stablelib/x25519'
import { createX25519ECDH, ECDH } from '../ECDH'

describe('JWE', () => {
describe('decryptJWE', () => {
Expand Down Expand Up @@ -326,6 +331,47 @@ describe('JWE', () => {
delete jwe.aad
await expect(decryptJWE(jwe, decrypter)).rejects.toThrowError('Failed to decrypt')
})

describe('using remote ECDH', () => {
const message = 'hello world'
const receiverPair = generateKeyPairFromSeed(randomBytes(32))
const receiverRemoteECDH = createX25519ECDH(receiverPair.secretKey)
const senderPair = generateKeyPairFromSeed(randomBytes(32))
const senderRemoteECDH: ECDH = createX25519ECDH(senderPair.secretKey)

it('creates anon JWE with remote ECDH', async () => {
const encrypter = createAnonEncrypter(receiverPair.publicKey)
const jwe: JWE = await createJWE(u8a.fromString(message), [encrypter])
const decrypter = createAnonDecrypter(receiverRemoteECDH)
const decryptedBytes = await decryptJWE(jwe, decrypter)
const receivedMessage = u8a.toString(decryptedBytes)
expect(receivedMessage).toEqual(message)
})

it('creates and decrypts auth JWE', async () => {
const encrypter = createAuthEncrypter(receiverPair.publicKey, senderRemoteECDH)
const jwe: JWE = await createJWE(u8a.fromString(message), [encrypter])
const decrypter = createAuthDecrypter(receiverRemoteECDH, senderPair.publicKey)
const decryptedBytes = await decryptJWE(jwe, decrypter)
const receivedMessage = u8a.toString(decryptedBytes)
expect(receivedMessage).toEqual(message)
})

it(`throws error when using bad secret key size`, async () => {
expect.assertions(1)
const badSecretKey = randomBytes(64)
expect(() => {
createX25519ECDH(badSecretKey)
}).toThrow('invalid_argument')
})

it(`throws error when using bad public key size`, async () => {
expect.assertions(1)
const ecdh: ECDH = createX25519ECDH(randomBytes(32))
const badPublicKey = randomBytes(64)
expect(ecdh(badPublicKey)).rejects.toThrow('invalid_argument')
})
})
})

describe('Multiple recipients', () => {
Expand Down
15 changes: 13 additions & 2 deletions src/__tests__/xc20pEncryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { decryptJWE, createJWE } from '../JWE'
import * as u8a from 'uint8arrays'
import { randomBytes } from '@stablelib/random'
import { generateKeyPair } from '@stablelib/x25519'
import { createX25519ECDH } from '../ECDH'

describe('xc20pEncryption', () => {
describe('resolveX25519Encrypters', () => {
Expand All @@ -13,6 +14,7 @@ describe('xc20pEncryption', () => {

let resolver
let decrypter1, decrypter2
let decrypter1remote, decrypter2remote

let didDocumentResult1, didDocumentResult2, didDocumentResult3, didDocumentResult4

Expand All @@ -22,6 +24,9 @@ describe('xc20pEncryption', () => {
decrypter1 = x25519Decrypter(kp1.secretKey)
decrypter2 = x25519Decrypter(kp2.secretKey)

decrypter1remote = x25519Decrypter(createX25519ECDH(kp1.secretKey))
decrypter2remote = x25519Decrypter(createX25519ECDH(kp2.secretKey))

didDocumentResult1 = {
didDocument: {
verificationMethod: [
Expand Down Expand Up @@ -75,7 +80,7 @@ describe('xc20pEncryption', () => {
})

it('correctly resolves encrypters for DIDs', async () => {
expect.assertions(4)
expect.assertions(6)
const encrypters = await resolveX25519Encrypters([did1, did2], resolver)
const cleartext = randomBytes(8)
const jwe = await createJWE(cleartext, encrypters)
Expand All @@ -84,6 +89,8 @@ describe('xc20pEncryption', () => {
expect(jwe.recipients[1].header.kid).toEqual(did2 + '#abc')
expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext)
expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext)
expect(await decryptJWE(jwe, decrypter1remote)).toEqual(cleartext)
expect(await decryptJWE(jwe, decrypter2remote)).toEqual(cleartext)
})

it('throws error if key is not found', async () => {
Expand All @@ -97,13 +104,15 @@ describe('xc20pEncryption', () => {
})

it('resolves encrypters for DIDs with multiple valid keys ', async () => {
expect.assertions(6)
expect.assertions(8)

const secondKp1 = generateKeyPair()
const secondKp2 = generateKeyPair()

const newDecrypter1 = x25519Decrypter(secondKp1.secretKey)
const newDecrypter2 = x25519Decrypter(secondKp2.secretKey)
const newDecrypter1remote = x25519Decrypter(createX25519ECDH(secondKp1.secretKey))
const newDecrypter2remote = x25519Decrypter(createX25519ECDH(secondKp2.secretKey))

didDocumentResult1.didDocument.verificationMethod.push({
id: did1 + '#def',
Expand All @@ -130,6 +139,8 @@ describe('xc20pEncryption', () => {
expect(jwe.recipients[3].header.kid).toEqual(did2 + '#def')
expect(await decryptJWE(jwe, newDecrypter1)).toEqual(cleartext)
expect(await decryptJWE(jwe, newDecrypter2)).toEqual(cleartext)
expect(await decryptJWE(jwe, newDecrypter1remote)).toEqual(cleartext)
expect(await decryptJWE(jwe, newDecrypter2remote)).toEqual(cleartext)
})
})
})
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from './JWT'
import { toEthereumAddress } from './Digest'
export { JWE, createJWE, decryptJWE, Encrypter, Decrypter } from './JWE'
export { ECDH, createX25519ECDH } from './ECDH'
export {
xc20pDirEncrypter,
xc20pDirDecrypter,
Expand Down
55 changes: 41 additions & 14 deletions src/xc20pEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { concatKDF } from './Digest'
import { bytesToBase64url, base58ToBytes, encodeBase64url, toSealed, base64ToBytes } from './util'
import { Recipient, EncryptionResult, Encrypter, Decrypter, ProtectedHeader } from './JWE'
import type { VerificationMethod, Resolvable } from 'did-resolver'
import { ECDH } from './ECDH'

export type AuthEncryptParams = {
kid?: string
Expand All @@ -27,13 +28,15 @@ export type AnonEncryptParams = {
*
* NOTE: ECDH-1PU and XC20PKW are proposed drafts in IETF and not a standard yet and
* are subject to change as new revisions or until the official CFRG specification are released.
*
* @beta
*/
export function createAuthEncrypter(
recipientPublicKey: Uint8Array,
senderSecretKey: Uint8Array,
senderSecret: Uint8Array | ECDH,
options: Partial<AuthEncryptParams> = {}
): Encrypter {
return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecretKey, options)
return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecret, options)
}

/**
Expand All @@ -42,9 +45,11 @@ export function createAuthEncrypter(
*
* NOTE: ECDH-ES+XC20PKW is a proposed draft in IETF and not a standard yet and
* is subject to change as new revisions or until the official CFRG specification is released.
*
* @beta
*/
export function createAnonEncrypter(publicKey: Uint8Array, options: Partial<AnonEncryptParams> = {}): Encrypter {
return options !== undefined ? x25519Encrypter(publicKey, options.kid) : x25519Encrypter(publicKey)
return x25519Encrypter(publicKey, options?.kid)
}

/**
Expand All @@ -55,9 +60,11 @@ export function createAnonEncrypter(publicKey: Uint8Array, options: Partial<Anon
*
* NOTE: ECDH-1PU and XC20PKW are proposed drafts in IETF and not a standard yet and
* are subject to change as new revisions or until the official CFRG specification are released.
*
* @beta
*/
export function createAuthDecrypter(recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter {
return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecretKey, senderPublicKey)
export function createAuthDecrypter(recipientSecret: Uint8Array | ECDH, senderPublicKey: Uint8Array): Decrypter {
return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecret, senderPublicKey)
}

/**
Expand All @@ -66,9 +73,11 @@ export function createAuthDecrypter(recipientSecretKey: Uint8Array, senderPublic
*
* NOTE: ECDH-ES+XC20PKW is a proposed draft in IETF and not a standard yet and
* is subject to change as new revisions or until the official CFRG specification is released.
*
* @beta
*/
export function createAnonDecrypter(secretKey: Uint8Array): Decrypter {
return x25519Decrypter(secretKey)
export function createAnonDecrypter(recipientSecret: Uint8Array | ECDH): Decrypter {
return x25519Decrypter(recipientSecret)
}

function xc20pEncrypter(key: Uint8Array): (cleartext: Uint8Array, aad?: Uint8Array) => EncryptionResult {
Expand Down Expand Up @@ -158,7 +167,7 @@ export function x25519Encrypter(publicKey: Uint8Array, kid?: string): Encrypter
*/
export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(
recipientPublicKey: Uint8Array,
senderSecretKey: Uint8Array,
senderSecret: Uint8Array | ECDH,
options: Partial<AuthEncryptParams> = {}
): Encrypter {
const alg = 'ECDH-1PU+XC20PKW'
Expand All @@ -176,7 +185,12 @@ export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(

// ECDH-1PU requires additional shared secret between
// static key of sender and static key of recipient
const zS = sharedKey(senderSecretKey, recipientPublicKey)
let zS
if (senderSecret instanceof Uint8Array) {
zS = sharedKey(senderSecret, recipientPublicKey)
} else {
zS = await senderSecret(recipientPublicKey)
}

const sharedSecret = new Uint8Array(zE.length + zS.length)
sharedSecret.set(zE)
Expand Down Expand Up @@ -258,7 +272,7 @@ function validateHeader(header?: ProtectedHeader) {
}
}

export function x25519Decrypter(secretKey: Uint8Array): Decrypter {
export function x25519Decrypter(receiverSecret: Uint8Array | ECDH): Decrypter {
const alg = 'ECDH-ES+XC20PKW'
const keyLen = 256
const crv = 'X25519'
Expand All @@ -272,7 +286,12 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter {
recipient = <Recipient>recipient
if (recipient.header.epk?.crv !== crv || typeof recipient.header.epk.x == 'undefined') return null
const publicKey = base64ToBytes(recipient.header.epk.x)
const sharedSecret = sharedKey(secretKey, publicKey)
let sharedSecret
if (receiverSecret instanceof Uint8Array) {
sharedSecret = sharedKey(receiverSecret, publicKey)
} else {
sharedSecret = await receiverSecret(publicKey)
}

// Key Encryption Key
const kek = concatKDF(sharedSecret, keyLen, alg)
Expand All @@ -292,7 +311,7 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter {
* - [ECDH-1PU](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03)
*/
export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
recipientSecretKey: Uint8Array,
recipientSecret: Uint8Array | ECDH,
senderPublicKey: Uint8Array
): Decrypter {
const alg = 'ECDH-1PU+XC20PKW'
Expand All @@ -310,8 +329,16 @@ export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
// ECDH-1PU requires additional shared secret between
// static key of sender and static key of recipient
const publicKey = base64ToBytes(recipient.header.epk.x)
const zE = sharedKey(recipientSecretKey, publicKey)
const zS = sharedKey(recipientSecretKey, senderPublicKey)
let zE: Uint8Array
let zS: Uint8Array

if (recipientSecret instanceof Uint8Array) {
zE = sharedKey(recipientSecret, publicKey)
zS = sharedKey(recipientSecret, senderPublicKey)
} else {
zE = await recipientSecret(publicKey)
zS = await recipientSecret(senderPublicKey)
}

const sharedSecret = new Uint8Array(zE.length + zS.length)
sharedSecret.set(zE)
Expand Down

0 comments on commit b018794

Please sign in to comment.