did-jwt
Version:
Library for Signing and Verifying JWTs that use DIDs as issuers and JWEs that use DIDs as recipients
465 lines (417 loc) • 21.4 kB
text/typescript
import { randomBytes } from '@noble/hashes/utils'
import { decodeBase64url, encodeBase64url, generateKeyPairFromSeed } from '../util.js'
import { createJWE, decryptJWE } from '../encryption/JWE.js'
import { vectors } from './jwe-vectors.js'
import {
createAnonDecrypter,
createAnonEncrypter,
createAuthDecrypter,
createAuthEncrypter,
x25519Decrypter,
x25519Encrypter,
xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2,
xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2,
} from '../encryption/xc20pEncryption.js'
import { createX25519ECDH } from '../encryption/ECDH.js'
import { xc20pDirDecrypter, xc20pDirEncrypter } from '../encryption/xc20pDir.js'
import { fromString, toString } from 'uint8arrays'
import type { Decrypter, ECDH, Encrypter, JWE } from '../encryption/types.js'
const u8a = { toString, fromString }
describe('JWE', () => {
describe('decryptJWE', () => {
describe('Direct encryption', () => {
test.each(vectors.dir.pass)('decrypts valid jwe', async ({ key, cleartext, jwe }) => {
expect.assertions(1)
const decrypter = xc20pDirDecrypter(u8a.fromString(key, 'base64pad'))
const cleartextU8a = await decryptJWE(jwe, decrypter)
expect(u8a.toString(cleartextU8a)).toEqual(cleartext)
})
test.each(vectors.dir.fail)('fails to decrypt bad jwe', async ({ key, jwe }) => {
expect.assertions(1)
const decrypter = xc20pDirDecrypter(u8a.fromString(key, 'base64pad'))
await expect(decryptJWE(jwe, decrypter)).rejects.toThrowError('Failed to decrypt')
})
test.each(vectors.dir.invalid)('throws on invalid jwe', async ({ jwe }) => {
expect.assertions(1)
const decrypter = xc20pDirDecrypter(randomBytes(32))
await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('bad_jwe: missing properties')
})
})
describe('X25519 key exchange', () => {
test.each(vectors.x25519.pass)('decrypts valid jwe', async ({ key, cleartext, jwe }) => {
expect.assertions(1)
const decrypter = x25519Decrypter(u8a.fromString(key, 'base64pad'))
const cleartextU8a = await decryptJWE(jwe as any, decrypter)
expect(u8a.toString(cleartextU8a)).toEqual(cleartext)
})
test.each(vectors.x25519.fail)('fails to decrypt bad jwe', async ({ key, jwe }) => {
expect.assertions(1)
const decrypter = x25519Decrypter(u8a.fromString(key, 'base64pad'))
await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('Failed to decrypt')
})
test.each(vectors.x25519.invalid)('throws on invalid jwe', async ({ jwe }) => {
expect.assertions(1)
const decrypter = x25519Decrypter(randomBytes(32))
await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('bad_jwe:')
})
})
describe('ECDH-1PU+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => {
test.each(vectors.ecdh1PuV3Xc20PkwV2.pass)(
'decrypts valid jwe',
async ({ senderkey, recipientkeys, cleartext, jwe }) => {
expect.assertions(recipientkeys.length)
for (const recipientkey of recipientkeys) {
const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
u8a.fromString(recipientkey, 'base64pad'),
u8a.fromString(senderkey, 'base64pad')
)
const cleartextU8a = await decryptJWE(jwe, decrypter)
expect(u8a.toString(cleartextU8a)).toEqual(cleartext)
}
}
)
test.each(vectors.ecdh1PuV3Xc20PkwV2.fail)(
'fails to decrypt bad jwe',
async ({ senderkey, recipientkeys, jwe }) => {
expect.assertions(recipientkeys.length)
for (const recipientkey of recipientkeys) {
const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
u8a.fromString(recipientkey, 'base64pad'),
u8a.fromString(senderkey, 'base64pad')
)
await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('Failed to decrypt')
}
}
)
test.each(vectors.ecdh1PuV3Xc20PkwV2.invalid)('throws on invalid jwe', async ({ jwe }) => {
expect.assertions(1)
const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(randomBytes(32), randomBytes(32))
await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('bad_jwe:')
})
})
})
describe('createJWE', () => {
describe('Direct encryption', () => {
let key: Uint8Array, cleartext: Uint8Array, encrypter: Encrypter, decrypter: Decrypter
beforeEach(() => {
key = randomBytes(32)
cleartext = u8a.fromString('my secret message')
encrypter = xc20pDirEncrypter(key)
decrypter = xc20pDirDecrypter(key)
})
it('Creates with only ciphertext', async () => {
expect.assertions(3)
const jwe = await createJWE(cleartext, [encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg: 'dir', enc: 'XC20P' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with data in protected header', async () => {
expect.assertions(3)
const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' })
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg: 'dir', enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with aad', async () => {
expect.assertions(4)
const aad = u8a.fromString('this data is authenticated')
const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }, aad)
expect(u8a.fromString(jwe.aad!!, 'base64url')).toEqual(aad)
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg: 'dir', enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
delete jwe.aad
await expect(decryptJWE(jwe, decrypter)).rejects.toThrowError('Failed to decrypt')
})
})
describe('ECDH-ES (X25519) key exchange encryption', () => {
describe('One recipient XC20PKW', () => {
let pubkey, secretkey, cleartext: Uint8Array, encrypter: Encrypter, decrypter: Decrypter
beforeEach(() => {
secretkey = randomBytes(32)
pubkey = generateKeyPairFromSeed(secretkey).publicKey
cleartext = u8a.fromString('my secret message')
encrypter = x25519Encrypter(pubkey)
decrypter = x25519Decrypter(secretkey)
})
it('Creates with only ciphertext', async () => {
expect.assertions(3)
const jwe = await createJWE(cleartext, [encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with data in protected header', async () => {
expect.assertions(3)
const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' })
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with aad', async () => {
expect.assertions(4)
const aad = u8a.fromString('this data is authenticated')
const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }, aad)
expect(u8a.fromString(jwe.aad!!, 'base64url')).toEqual(aad)
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
delete jwe.aad
await expect(decryptJWE(jwe, decrypter)).rejects.toThrowError('Failed to decrypt')
})
})
describe('Multiple recipients XC20PKW', () => {
let pubkey1, secretkey1, pubkey2, secretkey2, cleartext: Uint8Array
let encrypter1: Encrypter, decrypter1: Decrypter, encrypter2: Encrypter, decrypter2: Decrypter
beforeEach(() => {
secretkey1 = randomBytes(32)
pubkey1 = generateKeyPairFromSeed(secretkey1).publicKey
secretkey2 = randomBytes(32)
pubkey2 = generateKeyPairFromSeed(secretkey2).publicKey
cleartext = u8a.fromString('my secret message')
encrypter1 = x25519Encrypter(pubkey1)
decrypter1 = x25519Decrypter(secretkey1)
encrypter2 = x25519Encrypter(pubkey2)
decrypter2 = x25519Decrypter(secretkey2)
})
it('Creates with only ciphertext', async () => {
expect.assertions(4)
const jwe = await createJWE(cleartext, [encrypter1, encrypter2])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext)
expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext)
})
it('Creates with data in protected header', async () => {
expect.assertions(4)
const jwe = await createJWE(cleartext, [encrypter1, encrypter2], { more: 'protected' })
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext)
expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext)
})
it('Creates with aad', async () => {
expect.assertions(6)
const aad = u8a.fromString('this data is authenticated')
const jwe = await createJWE(cleartext, [encrypter1, encrypter2], { more: 'protected' }, aad)
expect(u8a.fromString(jwe.aad!!, 'base64url')).toEqual(aad)
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext)
expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext)
delete jwe.aad
await expect(decryptJWE(jwe, decrypter1)).rejects.toThrowError('Failed to decrypt')
await expect(decryptJWE(jwe, decrypter2)).rejects.toThrowError('Failed to decrypt')
})
it('Incompatible encrypters throw', async () => {
expect.assertions(1)
const enc1 = { enc: 'cool enc alg1' } as Encrypter
const enc2 = { enc: 'cool enc alg2' } as Encrypter
await expect(createJWE(cleartext, [enc1, enc2])).rejects.toThrowError('Incompatible encrypters passed')
})
})
})
})
describe('ECDH-1PU+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => {
describe('One recipient', () => {
let cleartext: Uint8Array, recipientKey: any, senderKey: any, decrypter: Decrypter
beforeEach(() => {
recipientKey = generateKeyPairFromSeed(randomBytes(32))
senderKey = generateKeyPairFromSeed(randomBytes(32))
cleartext = u8a.fromString('my secret message')
decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.secretKey, senderKey.publicKey)
})
it('Creates with only ciphertext', async () => {
const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey)
expect.assertions(3)
const jwe = await createJWE(cleartext, [encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with kid, no apu and no apv', async () => {
const kid = 'did:example:receiver#key-1'
const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, {
kid,
})
expect.assertions(6)
const jwe = await createJWE(cleartext, [encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(jwe.recipients!![0].header.kid).toEqual(kid)
expect(jwe.recipients!![0].header.apu).toBeUndefined()
expect(jwe.recipients!![0].header.apv).toBeUndefined()
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with no kid, apu and apv', async () => {
const apu = encodeBase64url('Alice')
const apv = encodeBase64url('Bob')
const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, {
apu,
apv,
})
expect.assertions(6)
const jwe = await createJWE(cleartext, [encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(jwe.recipients!![0].header.kid).toBeUndefined()
expect(jwe.recipients!![0].header.apu).toEqual(apu)
expect(jwe.recipients!![0].header.apv).toEqual(apv)
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with kid, apu and apv', async () => {
const kid = 'did:example:receiver#key-1'
const apu = encodeBase64url('Alice')
const apv = encodeBase64url('Bob')
const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, {
kid,
apu,
apv,
})
expect.assertions(6)
const jwe = await createJWE(cleartext, [encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(jwe.recipients!![0].header.kid).toEqual(kid)
expect(jwe.recipients!![0].header.apu).toEqual(apu)
expect(jwe.recipients!![0].header.apv).toEqual(apv)
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with data in protected header', async () => {
const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey)
const skid = 'did:example:sender#key-1'
expect.assertions(3)
const jwe = await createJWE(cleartext, [encrypter], { skid, more: 'protected' })
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', skid, more: 'protected' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
})
it('Creates with aad', async () => {
const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey)
expect.assertions(4)
const aad = u8a.fromString('this data is authenticated')
const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }, aad)
expect(u8a.fromString(jwe.aad!!, 'base64url')).toEqual(aad)
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext)
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', () => {
let cleartext: any, senderkey: any
const recipients: any[] = []
beforeEach(() => {
senderkey = generateKeyPairFromSeed(randomBytes(32))
cleartext = u8a.fromString('my secret message')
recipients[0] = { kid: 'did:example:receiver1#key-1', recipientkey: generateKeyPairFromSeed(randomBytes(32)) }
recipients[0] = {
...recipients[0],
...{
encrypter: xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(
recipients[0].recipientkey.publicKey,
senderkey.secretKey,
{ kid: recipients[0].kid }
),
decrypter: xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
recipients[0].recipientkey.secretKey,
senderkey.publicKey
),
},
}
recipients[1] = { kid: 'did:example:receiver2#key-1', recipientkey: generateKeyPairFromSeed(randomBytes(32)) }
recipients[1] = {
...recipients[1],
...{
encrypter: xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(
recipients[1].recipientkey.publicKey,
senderkey.secretKey,
{ kid: recipients[1].kid }
),
decrypter: xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(
recipients[1].recipientkey.secretKey,
senderkey.publicKey
),
},
}
})
it('Creates with only ciphertext', async () => {
expect.assertions(4)
const jwe = await createJWE(cleartext, [recipients[0].encrypter, recipients[1].encrypter])
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' })
expect(await decryptJWE(jwe, recipients[0].decrypter)).toEqual(cleartext)
expect(await decryptJWE(jwe, recipients[1].decrypter)).toEqual(cleartext)
})
it('Creates with data in protected header', async () => {
expect.assertions(4)
const skid = 'did:example:sender#key-1'
const jwe = await createJWE(cleartext, [recipients[0].encrypter, recipients[1].encrypter], {
more: 'protected',
skid,
})
expect(jwe.aad).toBeUndefined()
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected', skid })
expect(await decryptJWE(jwe, recipients[0].decrypter)).toEqual(cleartext)
expect(await decryptJWE(jwe, recipients[0].decrypter)).toEqual(cleartext)
})
it('Creates with aad', async () => {
expect.assertions(6)
const aad = u8a.fromString('this data is authenticated')
const jwe = await createJWE(
cleartext,
[recipients[0].encrypter, recipients[1].encrypter],
{ more: 'protected' },
aad
)
expect(u8a.fromString(jwe.aad!!, 'base64url')).toEqual(aad)
expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' })
expect(await decryptJWE(jwe, recipients[0].decrypter)).toEqual(cleartext)
expect(await decryptJWE(jwe, recipients[1].decrypter)).toEqual(cleartext)
delete jwe.aad
await expect(decryptJWE(jwe, recipients[0].decrypter)).rejects.toThrowError('Failed to decrypt')
await expect(decryptJWE(jwe, recipients[0].decrypter)).rejects.toThrowError('Failed to decrypt')
})
it('Incompatible encrypters throw', async () => {
expect.assertions(1)
const enc1 = { enc: 'cool enc alg1' } as Encrypter
const enc2 = { enc: 'cool enc alg2' } as Encrypter
await expect(createJWE(cleartext, [enc1, enc2])).rejects.toThrowError('Incompatible encrypters passed')
})
})
})
})