Skip to content

Commit

Permalink
feat: enable remote ECDH for JWE [de]encrypters (#186)
Browse files Browse the repository at this point in the history
* feat: enable remote ECDH for JWE [de]encrypters

fixes #183

* docs: add some documentation regarding the use of remote ECDH
  • Loading branch information
mirceanis authored Jun 9, 2021
1 parent 3b63138 commit ff26440
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 30 deletions.
31 changes: 31 additions & 0 deletions src/ECDH.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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.
*
* This method is meant to be used when direct access to a secret key is impossible or not desired.
*
* @param theirPublicKey `Uint8Array` the other party's public key
* @returns a `Promise` that resolves to a `Uint8Array` representing the computed shared secret
*/
export type ECDH = (theirPublicKey: Uint8Array) => Promise<Uint8Array>

/**
* Wraps an X25519 secret key into an ECDH method that can be used to compute a shared secret with a public key.
* @param mySecretKey A `Uint8Array` of length 32 representing the bytes of my secret key
* @returns an `ECDH` method with the signature `(theirPublicKey: Uint8Array) => Promise<Uint8Array>`
*
* @throws 'invalid_argument:...' if the secret key size is wrong
*/
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
Loading

0 comments on commit ff26440

Please sign in to comment.