UNPKG

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
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') }) }) }) })