From d07391c6055bdf9185339d9aea1046a844d8f640 Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Tue, 25 May 2021 12:43:03 +0200 Subject: [PATCH 1/9] fix: added ECDH-1PU+XC20PKW; added AnonEncrypter/Decrypter; added AuthEncrypter/Decrypter --- src/Digest.ts | 9 +- src/JWE.ts | 4 +- src/__tests__/JWE.test.ts | 183 ++++++++++++++++++++++++++++++++++- src/__tests__/jwe-vectors.js | 101 ++++++++++++++++++- src/xc20pEncryption.ts | 161 ++++++++++++++++++++++++++++++ 5 files changed, 450 insertions(+), 8 deletions(-) diff --git a/src/Digest.ts b/src/Digest.ts index 7bf05b34..a1cbe1f7 100644 --- a/src/Digest.ts +++ b/src/Digest.ts @@ -27,14 +27,15 @@ const lengthAndInput = (input: Uint8Array): Uint8Array => u8a.concat([writeUint3 // This implementation of concatKDF was inspired by these two implementations: // https://github.com/digitalbazaar/minimal-cipher/blob/master/algorithms/ecdhkdf.js // https://github.com/panva/jose/blob/master/lib/jwa/ecdh/derive.js -export function concatKDF(secret: Uint8Array, keyLen: number, alg: string): Uint8Array { +export function concatKDF(secret: Uint8Array, keyLen: number, alg: string, producerInfo?:Uint8Array, consumerInfo?:Uint8Array): Uint8Array { if (keyLen !== 256) throw new Error(`Unsupported key length: ${keyLen}`) const value = u8a.concat([ - lengthAndInput(u8a.fromString(alg)), - lengthAndInput(new Uint8Array(0)), // apu - lengthAndInput(new Uint8Array(0)), // apv + lengthAndInput(u8a.fromString(alg)), + lengthAndInput(typeof producerInfo === 'undefined' ? new Uint8Array(0) : producerInfo), // apu + lengthAndInput(typeof consumerInfo === 'undefined' ? new Uint8Array(0) : consumerInfo), // apv writeUint32BE(keyLen) ]) + // since our key lenght is 256 we only have to do one round const roundNumber = 1 return hash(u8a.concat([writeUint32BE(roundNumber), secret, value])) diff --git a/src/JWE.ts b/src/JWE.ts index 41d0fc3a..145d03a6 100644 --- a/src/JWE.ts +++ b/src/JWE.ts @@ -5,7 +5,9 @@ interface RecipientHeader { iv: string tag: string epk?: Record // Ephemeral Public Key - kid?: string + kid?: string, + apv?: string + apu?: string } export interface Recipient { diff --git a/src/__tests__/JWE.test.ts b/src/__tests__/JWE.test.ts index e8d858d7..1bc67c77 100644 --- a/src/__tests__/JWE.test.ts +++ b/src/__tests__/JWE.test.ts @@ -1,7 +1,8 @@ import { decryptJWE, createJWE, Encrypter } from '../JWE' import vectors from './jwe-vectors.js' -import { xc20pDirEncrypter, xc20pDirDecrypter, x25519Encrypter, x25519Decrypter } from '../xc20pEncryption' -import { decodeBase64url } from '../util' +import { xc20pDirEncrypter, xc20pDirDecrypter, x25519Encrypter, x25519Decrypter, + xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2, xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2 } from '../xc20pEncryption' +import { decodeBase64url, encodeBase64url } from '../util' import * as u8a from 'uint8arrays' import { randomBytes } from '@stablelib/random' import { generateKeyPairFromSeed } from '@stablelib/x25519' @@ -49,6 +50,36 @@ describe('JWE', () => { await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('Invalid JWE') }) }) + + describe('ECDH-1PU(v3)+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => { + test.each(vectors.ecdh1PuV3Xc20Pkw.pass)('decrypts valid jwe', async ({ + senderkey, recipientkeys, cleartext, jwe }) => { + expect.assertions(recipientkeys.length) + for(let recipientkey of recipientkeys) { + const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( + u8a.fromString(recipientkey, 'base64pad'), u8a.fromString(senderkey, 'base64pad')) + var cleartextU8a = await decryptJWE(jwe, decrypter) + expect(u8a.toString(cleartextU8a)).toEqual(cleartext) + } + }) + + test.each(vectors.ecdh1PuV3Xc20Pkw.fail)('fails to decrypt bad jwe', async ({ + senderkey, recipientkeys, jwe }) => { + expect.assertions(recipientkeys.length) + for(let 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.ecdh1PuV3Xc20Pkw.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('Invalid JWE') + }) + }) }) describe('createJWE', () => { @@ -186,4 +217,152 @@ describe('JWE', () => { }) }) }) + + describe('ECDH-1PU(v3)+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => { + describe('One recipient', () => { + let cleartext, recipientKey, senderKey, 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 skid, kid, no apu and no apv', async () => { + const kid = 'did:example:receiver#key-1' + const skid = 'did:example:sender#key-1' + const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { kid, skid } ) + expect.assertions(6) + const jwe = await createJWE(cleartext, [encrypter]) + expect(jwe.aad).toBeUndefined() + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', skid: skid }) + 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 skid, 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 skid, kid, apu and apv', async () => { + const kid = 'did:example:receiver#key-1' + const skid = 'did:example:sender#key-1' + const apu = encodeBase64url('Alice') + const apv = encodeBase64url('Bob') + const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { kid, skid, apu, apv } ) + expect.assertions(6) + const jwe = await createJWE(cleartext, [encrypter]) + expect(jwe.aad).toBeUndefined() + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', skid: skid }) + 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 ) + 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 () => { + 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('Multiple recipients', () => { + let cleartext, senderkey + let recipients = [] + let skid = 'did:example:sender#key-1' + + 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, skid } ), + 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, skid } ), + 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', skid: skid }) + 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 jwe = await createJWE(cleartext, [recipients[0].encrypter, recipients[1].encrypter], { more: 'protected' }) + expect(jwe.aad).toBeUndefined() + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', skid: skid, more: 'protected' }) + 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', skid: skid, 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') + }) + }) + }) }) diff --git a/src/__tests__/jwe-vectors.js b/src/__tests__/jwe-vectors.js index b1b6e436..e57d9ac6 100644 --- a/src/__tests__/jwe-vectors.js +++ b/src/__tests__/jwe-vectors.js @@ -139,5 +139,104 @@ module.exports = { jwe: {"protected":"eyJlbmMiOiJYQzIwUCJ9","recipients":[{"header":{"alg":"ECDH-ES+XC20PKW","tag":"fH6nMnuRhiwQU2GJ4WjIPA","epk":{"kty":"EC","crv":"P-256","x":"2mH373XQ_4IolX_FHzz1sztPs3UwwrP9Bm0D22gy4-U","y":"l8Yg3yTOOqhI9C5qNJhBqfJD9b0eacJZE0-pLCqImag"}},"encrypted_key":"sRAp3GM1vcOs-xQdCEb1OAl6WJxn0hJThRVUfkkW7es"},{"header":{"alg":"ECDH-ES+XC20PKW","tag":"tMK8ojOBHlzvATpzPwVqtQ","iv":"eMtFNTA1nKVgdYiEjWte3aZ-yto3Pp0g","epk":{"kty":"OKP","crv":"X25519","x":"6B5sqfpzjPedAPYpzMGeq6jc3w__GL_EI4dnl9u0ES0"}},"encrypted_key":"EL331vcSsYSDCt4rhLo009bxhCq9vmy07UFf31Ez9mk"}],"iv":"fgrzpDg-3TCKuNC5DMa1pwssyweKJ4Jo","ciphertext":"Kc3J_Z6l8wakQphIa7aO-9y-yvU276aukH-7V18vnT5_H3Y_XNjZlLen_Lxcy7NCq7zuiHjsGl0I3r6ihpdis6aFFQTFYfuTuNJOKO6k8uXU2AQ-KnTazg","tag":"9CF_koFccgK6w9WZho_9eQ"}, } ] - } + }, + ecdh1PuV3Xc20Pkw: { + pass: [ + { + senderkey: 'Ga6k9NGzLLbyz4uDF/25rmxL6kcMpIUfAB6q4jyErEI=', + recipientkeys: [ 'eGftJuIHIOQ4pIhpdHGgqJAYGvNRQyL1UgbuHCJKrlw=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJlbmMiOiJYQzIwUCJ9","iv":"tqp15TShA-eDERy2qEgCLmDl1QJSDZ4j","ciphertext":"5jPbpy_tj3FVszRzrEHwc6J0o-KluNSa56zyN3D7EHiJ_hgQDwUN8B-U1AJ_1uaBuPBmV0e-zAE4iX9ils_POcvwdpEB0LVnJ6QPYoOdbMx94uLb6pd6xw","tag":"QAdzJ4M8bSqvvuYY9-H_tw","recipients":[{"encrypted_key":"R8CAGP5rj3IZsKHWnSKrb_Z5iFwtLvDIn_WqO3pIko0","header":{"alg":"ECDH-1PU+XC20PKW","iv":"uedotKy0c6EhJMrWZC8r4_60n-vqUdAK","tag":"BImFz89iFXrhX_OmwqZRPA","epk":{"kty":"OKP","crv":"X25519","x":"ZHdwr-bpjEIYvvmcVyTT-UvjJS1DxUOLMNo5CxjcQns"},"enc":"XC20P"}}]} + }, + { + senderkey: '4pJFgMDsu0JqjFT9l2NnFv+/Q/1qUP9dzt0lFdu1+00=', + recipientkeys: [ 'G7MtaOo6BMsi8VoEgu4DEJmfgl088DIHLm6BbMFNnMk=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJza2lkIjoiZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0xIiwiZW5jIjoiWEMyMFAifQ","iv":"4wcrBHUEBhhi1jYQWeGXzFSmf013CWjE","ciphertext":"nCCKJTjHI8IzNNC7OoCrKtXhkqzYUp2EeBkcSDy6rn4Z0oDc1-GAfJumQw83MO3aNKxEkF_iFEZiE6dlZKmvX5o9VDMk-pG8dd9gTlBS8Jx5V7GIotATkg","tag":"1U7BeQvvkGrK5idhUrwxOw","recipients":[{"encrypted_key":"2o2Ponu58YToFT1fi4jh6XADnLZK_2HV629zPB39FmY","header":{"alg":"ECDH-1PU+XC20PKW","iv":"DFTIc_GxomeBBNW0Ne5pYarqCFpCNEAQ","tag":"eGtKwjevonz39if11DIe3g","epk":{"kty":"OKP","crv":"X25519","x":"an9B9-jgsR53lrLIRVdgd2_AOglxnFv6JFmHhiBXniw"},"kid":"did:example:receiver#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + }, + { + senderkey: 'o9+nnB/a7L7OaHpDKV3ZNqO8kMxN87bTfc3PPHwdmAY=', + recipientkeys: [ 'aHCSf53GyAsi2NEPN7jSJCiBNPI6caFZSnTsARA2/JU=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJlbmMiOiJYQzIwUCJ9","iv":"Mv2AaWtIV9xKPkR0Z4YWwHbPWNYfkQUm","ciphertext":"bO4O_N4LDn0LXovMFr-YUIguYAOgRwEilWikeehEigMlHuRMhk9gXAxzgEXOVR3EeAY0rOiJBs3kM0lXbkibbq5jD7dkoTO8d4f9VwJTvjh1n5T7dIS_4g","tag":"Fp4irT84Ry261664HeDixQ","recipients":[{"encrypted_key":"xm_rMaWJbyi5d1Hy3DvGc-ShjBMmtBLBaBrgYbjbqqs","header":{"alg":"ECDH-1PU+XC20PKW","iv":"CfJBZwkmufgbkhH5RMmAGmnAO7_TeiEy","tag":"U1ffVZr8hhnAgJKmr9tgzA","epk":{"kty":"OKP","crv":"X25519","x":"ZtKE_n4apf8xJxPfrk_22fHeYz1oMVV-9Ilsjkt9GWQ"},"apu":"QWxpY2U","apv":"Qm9i","enc":"XC20P"}}]} + }, + { + senderkey: 'gEBhMCE0zlLPTPY6TW/X1nFC+6Gn22KSuqdj8xuMDC8=', + recipientkeys: [ 'TnDUuo7hbVWYw/49HjZfWGDnDGZ/6tRdvwina0kYGwM=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJza2lkIjoiZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0xIiwiZW5jIjoiWEMyMFAifQ","iv":"MDK1vppdO1fBhnWYBw5Vcj7OzXFoWLI6","ciphertext":"6orRa7wBlgRKsyaTxbHSEzphYRH_1HwC5FDJDsuBQ7Fv7XwAJ12gvkxSKx4HvFNRgcgsODmdjYGyQQFnkqswFyZwyNylYpJyh5bAaqV61Z7R79kYHuMRVg","tag":"NHflobCWt6lplerL8dj36w","recipients":[{"encrypted_key":"7swx_oZVz5Zwv1nfHx1ls8ZFaK2w-U-SbKN425GLrKQ","header":{"alg":"ECDH-1PU+XC20PKW","iv":"CgpZBwuh4UQMiE_ESRBdH7V9X4ZEo7cf","tag":"jm6l1jIaI8mOEn_wzTXHTQ","epk":{"kty":"OKP","crv":"X25519","x":"SGslzCO9UZ7p4jU3_jqgu-bHh7ojq0RxR3rswAhcvGo"},"kid":"did:example:receiver#key-1","apu":"QWxpY2U","apv":"Qm9i","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + }, + { + senderkey: 'winSRtxUQasfBLcd8HPmF85kS6HMa0RLRtA8PblTsFc=', + recipientkeys: [ '2EITYEbrM3CtbggjtIWb+XR1nXn33ak2f8x5U0+tUs8=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwiZW5jIjoiWEMyMFAifQ","iv":"0AgTZOUg3yw0wayLySRVij8I7bDxQ0oZ","ciphertext":"3fYjaPgzawEdGbRir1dPzhTKNTtGlUvSkEFsW7wA3fpBrwN5qx3K_jyeixKkotOvn7kCG-NTgGAJ77ValW1Cl2X3fbb84YkYd1-UYr_qdBO_7-UELu145Q","tag":"H9h1pnOyWBpHUf76vnNobA","recipients":[{"encrypted_key":"mGqF1GmWGTzTQ1dtEHYuib1PEJs9bnezJBC0Qdw4Ih0","header":{"alg":"ECDH-1PU+XC20PKW","iv":"kQ9GZb3X3BGbf5KajtR7GhpW2Jneo1yp","tag":"cKZ6ilsGPmmA2X9rO3wOBQ","epk":{"kty":"OKP","crv":"X25519","x":"JhZV5gNSZ9LxoqKZ1tfkFUoisdqTUPpZXPThe-7pVnI"},"more":"protected","enc":"XC20P"}}]} + }, + { + senderkey: 'Jec1EkYpuvVj2vKXIyMjSo2JS7KXwMA1rvVGj7umYlw=', + recipientkeys: [ 'L16P46IUqXyxbdG3vxq0HqwzBbMwkVU9/SKjRy7Nubo=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwiZW5jIjoiWEMyMFAifQ","iv":"HtAxWrZXeFYQqhTX3VvTaPoo_iW78xhZ","ciphertext":"CPlGjk7prypqISuF0bMNgemNTG2JCLBrZbmsIAFBpqyUsJR9ZR6QA1osOb-ENZGqzem-TQvd8hn1EWtQiDBN_Sg8vt41GAfBvP3jYTxSvOMO4co2SZ864Q","tag":"6lvid-vUJHIIrTTdqtZWjQ","aad":"dGhpcyBkYXRhIGlzIGF1dGhlbnRpY2F0ZWQ","recipients":[{"encrypted_key":"-gFoPiUt1Ooqm6OfBxCS7zPntO_H13-a4fRah7OXNU4","header":{"alg":"ECDH-1PU+XC20PKW","iv":"WqVUpQgsjdkLeJ_9h3_cGq5F9bA49r84","tag":"OjXw26fPv6YYx0BAGK4r0w","epk":{"kty":"OKP","crv":"X25519","x":"yO4REF7yeuojtAgO7Zv4aBlopDhoId6RdKm4ByPYVG4"},"more":"protected","enc":"XC20P"}}]} + }, + { + senderkey: 's1mmgl42lUYs/m9NFcZXsrejKxpu0wpmExmskyXWsUQ=', + recipientkeys: [ 'OG/mkqO2noX0/7E0I+HTHGMTpYxbPLG8X9ak7ADGOtY=', '0I452d/J7+xl5OB/4ZGXoRPKBwpJdvd7E20SGLy9IAQ=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJza2lkIjoiZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0xIiwiZW5jIjoiWEMyMFAifQ","iv":"Hzl9pqbvncH10U6MFRpOZ7xyYqZTJkP_","ciphertext":"wvHImSFeFG6NpCEfpoAVe-DT8YgFPHt0dyPIS3nP3t6wY8A2GWf3z9-uzgX0ZVNr73_d0M_rhnPnBzlBiocsXrq7HLuBBucHoM2bC3NX2W_PoOoUHcf3zw","tag":"vDIWnftTdrkrHjiireD4aw","recipients":[{"encrypted_key":"DJYCzKQcf5heWMeOIcgVvCY99GVRMAcsrXsuElKK54s","header":{"alg":"ECDH-1PU+XC20PKW","iv":"XdcU-TJ2ZflgIDmQBJUDyDvHSCKdZpur","tag":"lGHm5Iofs-RZaGp3N4z0dQ","epk":{"kty":"OKP","crv":"X25519","x":"ZKI_CUgkKm2BSGZl61wCU8C94eiJMBYLZqZzFDTTJy4"},"kid":"did:example:receiver1#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}},{"encrypted_key":"6hTnZ6Lw1PUtWXISFMTqI8BmQ9TQo3svwiC5CI8dhcg","header":{"alg":"ECDH-1PU+XC20PKW","iv":"ZgGZNiv_Zcm-dnoNl3keXAXMPO-ZSuAb","tag":"-zU7jiF-tNWdI7oDVzk52Q","epk":{"kty":"OKP","crv":"X25519","x":"oP9HpmTjYJpDvK1TJN0u9bZH70E7RLRVsx47-5zosUk"},"kid":"did:example:receiver2#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + }, + { + senderkey: 'L9q9/9Ja+sRXsgtaoJu4BKsU4tPShkD43q2q/J6QhS8=', + recipientkeys: [ 'NDj9lf1KGYV62+suEaV7eM9Jyf52IcNOgfk/gq2ZM88=', 'FjYBTXCaNXqOafuznKOiDsdza6seF6O1THL/aaOCoQ0=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwic2tpZCI6ImRpZDpleGFtcGxlOnNlbmRlciNrZXktMSIsImVuYyI6IlhDMjBQIn0","iv":"BN5rEL2D8n6O4X5qFVD1NgspGtKABgXR","ciphertext":"lNbDB1MsqC654o5vuV2NlYjXp26WgPcYMWxU2xx1lTIuK1V3loF3vrRG7gItxWQp3KHJL4TZVYcGd29hFkB_Aw4JIp2t1-sLtjPsvs7P9hf0I-60Em3pFA","tag":"U2gFdvyJgTbhnt3WxIZv6A","recipients":[{"encrypted_key":"GoUONl8e-5lkG9nl4xgCmyGCJG6cR3l-PsTpWFAJ2-4","header":{"alg":"ECDH-1PU+XC20PKW","iv":"QHMSdRjuHQamlyiDG11xdI6ZRbMXxrd1","tag":"F7N6Gr_3kbS5uscGgrNEkg","epk":{"kty":"OKP","crv":"X25519","x":"4__NuoaZRG124GpJYReph2VsYSRYELNYiLIf6hXtVXQ"},"kid":"did:example:receiver1#key-1","more":"protected","skid":"did:example:sender#key-1","enc":"XC20P"}},{"encrypted_key":"H2GfRVKHOwdEsmvSoZWu7jP1Y--kwh3nKYMUtBiQzM8","header":{"alg":"ECDH-1PU+XC20PKW","iv":"gTF_tpp8FIOKqGItFwilpOhEhOErqa9j","tag":"mKP5Ey4c0CEt6R0inWhuRA","epk":{"kty":"OKP","crv":"X25519","x":"BGB0V4X_XJLVV5fXM-CbGyF6x7EQh4fWE6NxdAueXSE"},"kid":"did:example:receiver2#key-1"}}]} + }, + { + senderkey: 'DMk0fWkt2Y8Y717xbUps8o+g9vXgqhIvUzG22u3YoVQ=', + recipientkeys: [ 'aK2tDSxuQB3wE0+pW2xhez+jd2Nlnlsn40TfmG/290A=', 'MBJmtMzmfH86xjiuFZ7yObzhUlWZyTSkXgNvClB7Nz8=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwic2tpZCI6ImRpZDpleGFtcGxlOnNlbmRlciNrZXktMSIsImVuYyI6IlhDMjBQIn0","iv":"WEAjww6hpVW-q0qOHDtEEN-AwWVEkHgf","ciphertext":"OuSj8p9DJ2O4cOXRWHi3bLQbnsTRNuSKjgAr5ig1AcsXRj0olOOEK-gb5Qs7sNREUUBqUyK9SC2_cW2JD5BC-MKal08eriN7N2e-m5SS9OOIsZiyGtnI-A","tag":"MHAFgLIHcNS-m42OiVvNwQ","aad":"dGhpcyBkYXRhIGlzIGF1dGhlbnRpY2F0ZWQ","recipients":[{"encrypted_key":"puuKXBUXSBlRZCICaQnG-OLX_F_-GVE4lESLqZ4QDTk","header":{"alg":"ECDH-1PU+XC20PKW","iv":"zZqI4m9XO-aA6u2EVEZUGJexnMuSnxC4","tag":"HDZdndyMdRf77I2IO-zUow","epk":{"kty":"OKP","crv":"X25519","x":"KZfzLUZMwvlc7mItQyx0F9b1caC0SxiGuNemmYQ8nF8"},"kid":"did:example:receiver1#key-1","more":"protected","skid":"did:example:sender#key-1","enc":"XC20P"}},{"encrypted_key":"X85SlJ6WEJv4TFTxn2SYH1w0fEKH_HkI_oZZaK_VglE","header":{"alg":"ECDH-1PU+XC20PKW","iv":"HtBD10hnvYwqFB7BcHe9PPrOAV4qybHZ","tag":"p0ySr_KDlUWRAwSwE2ifNQ","epk":{"kty":"OKP","crv":"X25519","x":"zAkbzcZYPveVuE5nEiNGq4bN8Ja3ImJG_BI0UkTs_ys"},"kid":"did:example:receiver2#key-1","more":"protected","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + } + ], + fail: [ + { + // wrong sender key + senderkey: 'DMk0fWkt2Y8Y717xbUps8o+g9vXgqhIvUzG22u3YoVQ=', + recipientkeys: [ 'eGftJuIHIOQ4pIhpdHGgqJAYGvNRQyL1UgbuHCJKrlw=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJlbmMiOiJYQzIwUCJ9","iv":"tqp15TShA-eDERy2qEgCLmDl1QJSDZ4j","ciphertext":"5jPbpy_tj3FVszRzrEHwc6J0o-KluNSa56zyN3D7EHiJ_hgQDwUN8B-U1AJ_1uaBuPBmV0e-zAE4iX9ils_POcvwdpEB0LVnJ6QPYoOdbMx94uLb6pd6xw","tag":"QAdzJ4M8bSqvvuYY9-H_tw","recipients":[{"encrypted_key":"R8CAGP5rj3IZsKHWnSKrb_Z5iFwtLvDIn_WqO3pIko0","header":{"alg":"ECDH-1PU+XC20PKW","iv":"uedotKy0c6EhJMrWZC8r4_60n-vqUdAK","tag":"BImFz89iFXrhX_OmwqZRPA","epk":{"kty":"OKP","crv":"X25519","x":"ZHdwr-bpjEIYvvmcVyTT-UvjJS1DxUOLMNo5CxjcQns"},"enc":"XC20P"}}]} + }, + { + // wrong recipient keys + senderkey: 'Ga6k9NGzLLbyz4uDF/25rmxL6kcMpIUfAB6q4jyErEI=', + recipientkeys: [ 'aK2tDSxuQB3wE0+pW2xhez+jd2Nlnlsn40TfmG/290A=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJlbmMiOiJYQzIwUCJ9","iv":"tqp15TShA-eDERy2qEgCLmDl1QJSDZ4j","ciphertext":"5jPbpy_tj3FVszRzrEHwc6J0o-KluNSa56zyN3D7EHiJ_hgQDwUN8B-U1AJ_1uaBuPBmV0e-zAE4iX9ils_POcvwdpEB0LVnJ6QPYoOdbMx94uLb6pd6xw","tag":"QAdzJ4M8bSqvvuYY9-H_tw","recipients":[{"encrypted_key":"R8CAGP5rj3IZsKHWnSKrb_Z5iFwtLvDIn_WqO3pIko0","header":{"alg":"ECDH-1PU+XC20PKW","iv":"uedotKy0c6EhJMrWZC8r4_60n-vqUdAK","tag":"BImFz89iFXrhX_OmwqZRPA","epk":{"kty":"OKP","crv":"X25519","x":"ZHdwr-bpjEIYvvmcVyTT-UvjJS1DxUOLMNo5CxjcQns"},"enc":"XC20P"}}]} + }, + { + // wrong sender key + senderkey: 'DMk0fWkt2Y8Y717xbUps8o+g9vXgqhIvUzG22u3YoVQ=', + recipientkeys: [ 'OG/mkqO2noX0/7E0I+HTHGMTpYxbPLG8X9ak7ADGOtY=', '0I452d/J7+xl5OB/4ZGXoRPKBwpJdvd7E20SGLy9IAQ=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJza2lkIjoiZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0xIiwiZW5jIjoiWEMyMFAifQ","iv":"Hzl9pqbvncH10U6MFRpOZ7xyYqZTJkP_","ciphertext":"wvHImSFeFG6NpCEfpoAVe-DT8YgFPHt0dyPIS3nP3t6wY8A2GWf3z9-uzgX0ZVNr73_d0M_rhnPnBzlBiocsXrq7HLuBBucHoM2bC3NX2W_PoOoUHcf3zw","tag":"vDIWnftTdrkrHjiireD4aw","recipients":[{"encrypted_key":"DJYCzKQcf5heWMeOIcgVvCY99GVRMAcsrXsuElKK54s","header":{"alg":"ECDH-1PU+XC20PKW","iv":"XdcU-TJ2ZflgIDmQBJUDyDvHSCKdZpur","tag":"lGHm5Iofs-RZaGp3N4z0dQ","epk":{"kty":"OKP","crv":"X25519","x":"ZKI_CUgkKm2BSGZl61wCU8C94eiJMBYLZqZzFDTTJy4"},"kid":"did:example:receiver1#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}},{"encrypted_key":"6hTnZ6Lw1PUtWXISFMTqI8BmQ9TQo3svwiC5CI8dhcg","header":{"alg":"ECDH-1PU+XC20PKW","iv":"ZgGZNiv_Zcm-dnoNl3keXAXMPO-ZSuAb","tag":"-zU7jiF-tNWdI7oDVzk52Q","epk":{"kty":"OKP","crv":"X25519","x":"oP9HpmTjYJpDvK1TJN0u9bZH70E7RLRVsx47-5zosUk"},"kid":"did:example:receiver2#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + }, + { + // wrong recipient keys + senderkey: 's1mmgl42lUYs/m9NFcZXsrejKxpu0wpmExmskyXWsUQ=', + recipientkeys: [ 'aK2tDSxuQB3wE0+pW2xhez+jd2Nlnlsn40TfmG/290A=', 'aK2tDSxuQB3wE0+pW2xhez+jd2Nlnlsn40TfmG/290A=' ], + cleartext: '/GOQlvtSg2V6m9L1IfjPpoyunkmjtvzZX5/gh+lo847Ys3oP+1wd0NmAsCGHiSTB58aAx6PG1+Vi4sXUtRP4kw==', + jwe: {"protected":"eyJza2lkIjoiZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0xIiwiZW5jIjoiWEMyMFAifQ","iv":"Hzl9pqbvncH10U6MFRpOZ7xyYqZTJkP_","ciphertext":"wvHImSFeFG6NpCEfpoAVe-DT8YgFPHt0dyPIS3nP3t6wY8A2GWf3z9-uzgX0ZVNr73_d0M_rhnPnBzlBiocsXrq7HLuBBucHoM2bC3NX2W_PoOoUHcf3zw","tag":"vDIWnftTdrkrHjiireD4aw","recipients":[{"encrypted_key":"DJYCzKQcf5heWMeOIcgVvCY99GVRMAcsrXsuElKK54s","header":{"alg":"ECDH-1PU+XC20PKW","iv":"XdcU-TJ2ZflgIDmQBJUDyDvHSCKdZpur","tag":"lGHm5Iofs-RZaGp3N4z0dQ","epk":{"kty":"OKP","crv":"X25519","x":"ZKI_CUgkKm2BSGZl61wCU8C94eiJMBYLZqZzFDTTJy4"},"kid":"did:example:receiver1#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}},{"encrypted_key":"6hTnZ6Lw1PUtWXISFMTqI8BmQ9TQo3svwiC5CI8dhcg","header":{"alg":"ECDH-1PU+XC20PKW","iv":"ZgGZNiv_Zcm-dnoNl3keXAXMPO-ZSuAb","tag":"-zU7jiF-tNWdI7oDVzk52Q","epk":{"kty":"OKP","crv":"X25519","x":"oP9HpmTjYJpDvK1TJN0u9bZH70E7RLRVsx47-5zosUk"},"kid":"did:example:receiver2#key-1","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + } + ], + invalid: [ + { + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwic2tpZCI6ImRpZDpleGFtcGxlOnNlbmRlciNrZXktMSIsImVuYyI6IlhDMjBQIn0","ciphertext":"6DehIR6ps5yh5Mepwj6XluBSk5AS0d18Y27XTWvV5T0uCRtcxBGO1finKBqzgblJA7dPQ55TZuVd41UERiq9FhsPgp7ehR4bBoyHnm8ftnjSHVpyORxLBw","tag":"T2fKAQQgJGFpI0kfpGXFkg","aad":"dGhpcyBkYXRhIGlzIGF1dGhlbnRpY2F0ZWQ","recipients":[{"encrypted_key":"OKUxwt7G1VbLhl0K5yHGkEQe2Ii8CHblLREK304ub6M","header":{"alg":"ECDH-1PU+XC20PKW","iv":"Gnt5p0e8eG012SfLxh-uo9lKs8cYsYGy","tag":"XWZYufnclg_Ei4JsBMpYNA","epk":{"kty":"OKP","crv":"X25519","x":"u7j3sQuuUbDVFoujne22_1b9HcwHkbAUxRsyAmhGz14"},"kid":"did:example:receiver#key-1","apu":"ZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0x","apv":"ZGlkOmV4YW1wbGU6cmVjZWl2ZXIja2V5LTE","more":"protected","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + }, + { + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwic2tpZCI6ImRpZDpleGFtcGxlOnNlbmRlciNrZXktMSIsImVuYyI6IlhDMjBQIn0","iv":"yZakU656sGJS9UKV5zyC1HV7cIhu0MPs","ciphertext":"6DehIR6ps5yh5Mepwj6XluBSk5AS0d18Y27XTWvV5T0uCRtcxBGO1finKBqzgblJA7dPQ55TZuVd41UERiq9FhsPgp7ehR4bBoyHnm8ftnjSHVpyORxLBw","tag":"T2fKAQQgJGFpI0kfpGXFkg","aad":"dGhpcyBkYXRhIGlzIGF1dGhlbnRpY2F0ZWQ","recipients":[]} + }, + { + jwe: {"protected":"eyJtb3JlIjoicHJvdGVjdGVkIiwic2tpZCI6ImRpZDpleGFtcGxlOnNlbmRlciNrZXktMSIsImVuYyI6IlhDMjBQIn0","iv":"yZakU656sGJS9UKV5zyC1HV7cIhu0MPs","ciphertext":"6DehIR6ps5yh5Mepwj6XluBSk5AS0d18Y27XTWvV5T0uCRtcxBGO1finKBqzgblJA7dPQ55TZuVd41UERiq9FhsPgp7ehR4bBoyHnm8ftnjSHVpyORxLBw","aad":"dGhpcyBkYXRhIGlzIGF1dGhlbnRpY2F0ZWQ","recipients":[{"encrypted_key":"OKUxwt7G1VbLhl0K5yHGkEQe2Ii8CHblLREK304ub6M","header":{"alg":"ECDH-1PU+XC20PKW","iv":"Gnt5p0e8eG012SfLxh-uo9lKs8cYsYGy","tag":"XWZYufnclg_Ei4JsBMpYNA","epk":{"kty":"OKP","crv":"X25519","x":"u7j3sQuuUbDVFoujne22_1b9HcwHkbAUxRsyAmhGz14"},"kid":"did:example:receiver#key-1","apu":"ZGlkOmV4YW1wbGU6c2VuZGVyI2tleS0x","apv":"ZGlkOmV4YW1wbGU6cmVjZWl2ZXIja2V5LTE","more":"protected","skid":"did:example:sender#key-1","enc":"XC20P"}}]} + } + ] + } } diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index c1521f1b..d332617c 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -9,6 +9,70 @@ import type { VerificationMethod, Resolvable } from 'did-resolver' // remove when targeting node 11+ or ES2019 const flatten = (arrays: T[]) => [].concat.apply([], arrays) +export type AuthEncryptParams = { + kid?: string, + skid?: string, + // base64url encoded + apu?: string, + // base64url encoded + apv?: string +} + +export type AnonEncryptParams = { + kid?: string +} + +/** + * Recommended encrypter for authenticated encryption (i.e. sender authentication and requires + * sender private key to encrypt the data). + * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and + * XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). + * + * 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 offical CFRG specification are released. + */ +export function AuthEncrypter(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, + options: Partial = {}): Encrypter { + return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecretKey, options); +} + +/** + * Recommended encrypter for anonymous encryption (i.e. no sender authentication). + * Uses ECDH-ES+XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). + * + * 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 offical CFRG specification is released. + */ +export function AnonEncrypter(publicKey: Uint8Array, + options: Partial = {}): Encrypter { + return x25519Encrypter(publicKey, options.kid) +} + +/** + * Recommended decrypter for authenticated encryption (i.e. sender authentication and requires + * sender public key to decrypt the data). + * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and + * XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). + * + * + * 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 offical CFRG specification are released. + */ +export function AuthDecrypter(recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { + return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecretKey, senderPublicKey); +} + +/** + * Recommended decrypter for anonymous encryption (i.e. no sender authentication). + * Uses ECDH-ES+XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). + * + * 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 offical CFRG specification is released. + */ +export function AnonDecrypter(secretKey: Uint8Array): Decrypter { + return x25519Decrypter(secretKey) +} + function xc20pEncrypter(key: Uint8Array): (cleartext: Uint8Array, aad?: Uint8Array) => EncryptionResult { const cipher = new XChaCha20Poly1305(key) return (cleartext: Uint8Array, aad?: Uint8Array) => { @@ -80,6 +144,65 @@ export function x25519Encrypter(publicKey: Uint8Array, kid?: string): Encrypter } return { alg, enc: 'XC20P', encrypt, encryptCek } } + +// Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: +// - XC20PKW from https://tools.ietf.org/html/draft-amringer-jose-chacha-02 +// - ECDH-1PU from https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 +export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, + options: Partial = {}): Encrypter { + + const alg = 'ECDH-1PU+XC20PKW' + const keyLen = 256 + const crv = 'X25519' + + let partyUInfo, partyVInfo + if (options.apu !== undefined) partyUInfo = base64ToBytes(options.apu) + if (options.apv !== undefined) partyVInfo = base64ToBytes(options.apv) + + async function encryptCek(cek): Promise { + const epk = generateKeyPair() + const zE = sharedKey(epk.secretKey, recipientPublicKey) + + // ECDH-1PU requires additional shared secret between + // static key of sender and static key of recipient + const zS = sharedKey(senderSecretKey, recipientPublicKey) + + let sharedSecret = new Uint8Array(zE.length + zS.length); + sharedSecret.set(zE); + sharedSecret.set(zS, zE.length); + + // Key Encryption Key + const kek = concatKDF(sharedSecret, keyLen, alg, partyUInfo, partyVInfo) + + const res = xc20pEncrypter(kek)(cek) + const recipient: Recipient = { + encrypted_key: bytesToBase64url(res.ciphertext), + header: { + alg, + iv: bytesToBase64url(res.iv), + tag: bytesToBase64url(res.tag), + epk: { kty: 'OKP', crv, x: bytesToBase64url(epk.publicKey) } + } + } + if (options.kid) recipient.header.kid = options.kid + if (options.apu) recipient.header.apu = options.apu + if (options.apv) recipient.header.apv = options.apv + + return recipient + } + async function encrypt(cleartext, protectedHeader = {}, aad?): Promise { + // we won't want alg to be set to dir from xc20pDirEncrypter + Object.assign(protectedHeader, { alg: undefined, skid: options.skid }) + // Content Encryption Key + const cek = randomBytes(32) + return { + ...(await xc20pDirEncrypter(cek).encrypt(cleartext, protectedHeader, aad)), + recipient: await encryptCek(cek), + cek + } + } + return { alg, enc: 'XC20P', encrypt, encryptCek } +} export async function resolveX25519Encrypters(dids: string[], resolver: Resolvable): Promise { const encryptersForDID = async (did): Promise => { @@ -136,3 +259,41 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter { } return { alg, enc: 'XC20P', decrypt } } + +// Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: +// - XC20PKW from https://tools.ietf.org/html/draft-amringer-jose-chacha-02 +// - ECDH-1PU from https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 +export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( + recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { + + const alg = 'ECDH-1PU+XC20PKW' + const keyLen = 256 + const crv = 'X25519' + async function decrypt(sealed, iv, aad, recipient): Promise { + validateHeader(recipient.header) + if (recipient.header.epk.crv !== crv) return null + // 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 sharedSecret = new Uint8Array(zE.length + zS.length); + sharedSecret.set(zE); + sharedSecret.set(zS, zE.length); + + // Key Encryption Key + let producerInfo, consumerInfo + if (recipient.header.apu) producerInfo = base64ToBytes(recipient.header.apu) + if (recipient.header.apv) consumerInfo = base64ToBytes(recipient.header.apv) + + const kek = concatKDF(sharedSecret, keyLen, alg, producerInfo, consumerInfo) + // Content Encryption Key + const sealedCek = toSealed(recipient.encrypted_key, recipient.header.tag) + const cek = await xc20pDirDecrypter(kek).decrypt(sealedCek, base64ToBytes(recipient.header.iv)) + if (cek === null) return null + + return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad) + } + return { alg, enc: 'XC20P', decrypt } +} \ No newline at end of file From 0f56e687ef6c192ee1326260ba9ea9cea6de6686 Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Tue, 25 May 2021 12:48:50 +0200 Subject: [PATCH 2/9] fix: removed redundant ',' --- src/JWE.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/JWE.ts b/src/JWE.ts index 145d03a6..ba32418b 100644 --- a/src/JWE.ts +++ b/src/JWE.ts @@ -5,7 +5,7 @@ interface RecipientHeader { iv: string tag: string epk?: Record // Ephemeral Public Key - kid?: string, + kid?: string apv?: string apu?: string } From e7001f22ae9b4eb16dbf5928c748168fded42a5d Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Tue, 25 May 2021 12:52:27 +0200 Subject: [PATCH 3/9] fix: fixed debug output; added V2 for XC20PKW to test vectors --- src/__tests__/JWE.test.ts | 10 +++++----- src/__tests__/jwe-vectors.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/__tests__/JWE.test.ts b/src/__tests__/JWE.test.ts index 1bc67c77..a99f1575 100644 --- a/src/__tests__/JWE.test.ts +++ b/src/__tests__/JWE.test.ts @@ -51,8 +51,8 @@ describe('JWE', () => { }) }) - describe('ECDH-1PU(v3)+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => { - test.each(vectors.ecdh1PuV3Xc20Pkw.pass)('decrypts valid jwe', async ({ + 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(let recipientkey of recipientkeys) { @@ -63,7 +63,7 @@ describe('JWE', () => { } }) - test.each(vectors.ecdh1PuV3Xc20Pkw.fail)('fails to decrypt bad jwe', async ({ + test.each(vectors.ecdh1PuV3Xc20PkwV2.fail)('fails to decrypt bad jwe', async ({ senderkey, recipientkeys, jwe }) => { expect.assertions(recipientkeys.length) for(let recipientkey of recipientkeys) { @@ -73,7 +73,7 @@ describe('JWE', () => { } }) - test.each(vectors.ecdh1PuV3Xc20Pkw.invalid)('throws on invalid jwe', async ({ + test.each(vectors.ecdh1PuV3Xc20PkwV2.invalid)('throws on invalid jwe', async ({ jwe }) => { expect.assertions(1) const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(randomBytes(32), randomBytes(32)) @@ -218,7 +218,7 @@ describe('JWE', () => { }) }) - describe('ECDH-1PU(v3)+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => { + describe('ECDH-1PU+XC20PKW (X25519), Key Wrapping Mode with XC20P content encryption', () => { describe('One recipient', () => { let cleartext, recipientKey, senderKey, decrypter diff --git a/src/__tests__/jwe-vectors.js b/src/__tests__/jwe-vectors.js index e57d9ac6..e8114bb3 100644 --- a/src/__tests__/jwe-vectors.js +++ b/src/__tests__/jwe-vectors.js @@ -140,7 +140,7 @@ module.exports = { } ] }, - ecdh1PuV3Xc20Pkw: { + ecdh1PuV3Xc20PkwV2: { pass: [ { senderkey: 'Ga6k9NGzLLbyz4uDF/25rmxL6kcMpIUfAB6q4jyErEI=', From 844883d074bbe50fc862e1ebc7ce351c78de35b2 Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Tue, 25 May 2021 12:55:50 +0200 Subject: [PATCH 4/9] fix: added more TS comments --- src/xc20pEncryption.ts | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index d332617c..5e8ad3bb 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -54,7 +54,6 @@ export function AnonEncrypter(publicKey: Uint8Array, * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and * XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). * - * * 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 offical CFRG specification are released. */ @@ -145,9 +144,11 @@ export function x25519Encrypter(publicKey: Uint8Array, kid?: string): Encrypter return { alg, enc: 'XC20P', encrypt, encryptCek } } -// Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: -// - XC20PKW from https://tools.ietf.org/html/draft-amringer-jose-chacha-02 -// - ECDH-1PU from https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 +/** + * Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: + * - [XC20PKW](https://tools.ietf.org/html/draft-amringer-jose-chacha-02) + * - [ECDH-1PU](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) + */ export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, options: Partial = {}): Encrypter { @@ -260,10 +261,12 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter { return { alg, enc: 'XC20P', decrypt } } -// Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: -// - XC20PKW from https://tools.ietf.org/html/draft-amringer-jose-chacha-02 -// - ECDH-1PU from https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03 -export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( +/** + * Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: + * - [XC20PKW](https://tools.ietf.org/html/draft-amringer-jose-chacha-02) + * - [ECDH-1PU](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) + */ + export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { const alg = 'ECDH-1PU+XC20PKW' From ee461716245dfc3ba2de2ed99cedf6c81470da45 Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Tue, 25 May 2021 12:58:26 +0200 Subject: [PATCH 5/9] fix: removed white spaces --- src/Digest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Digest.ts b/src/Digest.ts index a1cbe1f7..970cc5d8 100644 --- a/src/Digest.ts +++ b/src/Digest.ts @@ -30,7 +30,7 @@ const lengthAndInput = (input: Uint8Array): Uint8Array => u8a.concat([writeUint3 export function concatKDF(secret: Uint8Array, keyLen: number, alg: string, producerInfo?:Uint8Array, consumerInfo?:Uint8Array): Uint8Array { if (keyLen !== 256) throw new Error(`Unsupported key length: ${keyLen}`) const value = u8a.concat([ - lengthAndInput(u8a.fromString(alg)), + lengthAndInput(u8a.fromString(alg)), lengthAndInput(typeof producerInfo === 'undefined' ? new Uint8Array(0) : producerInfo), // apu lengthAndInput(typeof consumerInfo === 'undefined' ? new Uint8Array(0) : consumerInfo), // apv writeUint32BE(keyLen) From 82042f9eefccf05cee789d08897974aef5da1cd4 Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Tue, 25 May 2021 13:10:13 +0200 Subject: [PATCH 6/9] fix: handling undefined options for anoncrypt --- src/xc20pEncryption.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index 5e8ad3bb..f038b51f 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -45,7 +45,7 @@ export function AuthEncrypter(recipientPublicKey: Uint8Array, senderSecretKey: U */ export function AnonEncrypter(publicKey: Uint8Array, options: Partial = {}): Encrypter { - return x25519Encrypter(publicKey, options.kid) + return (options !== undefined) ? x25519Encrypter(publicKey, options.kid) : x25519Encrypter(publicKey) } /** From d64861aad582ee5782e8c97417fa6d20b6834f7c Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Fri, 28 May 2021 16:29:46 +0200 Subject: [PATCH 7/9] fix: renamed generic anon/auth encrypter/decrypter names --- src/__tests__/JWE.test.ts | 2 +- src/xc20pEncryption.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/__tests__/JWE.test.ts b/src/__tests__/JWE.test.ts index a99f1575..468e3855 100644 --- a/src/__tests__/JWE.test.ts +++ b/src/__tests__/JWE.test.ts @@ -1,7 +1,7 @@ import { decryptJWE, createJWE, Encrypter } from '../JWE' import vectors from './jwe-vectors.js' import { xc20pDirEncrypter, xc20pDirDecrypter, x25519Encrypter, x25519Decrypter, - xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2, xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2 } from '../xc20pEncryption' + xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2, xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2, AnonEncrypter, AuthEncrypter } from '../xc20pEncryption' import { decodeBase64url, encodeBase64url } from '../util' import * as u8a from 'uint8arrays' import { randomBytes } from '@stablelib/random' diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index f038b51f..68d0e374 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -31,7 +31,7 @@ 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 offical CFRG specification are released. */ -export function AuthEncrypter(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, +export function createAuthEncrypter(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, options: Partial = {}): Encrypter { return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecretKey, options); } @@ -43,7 +43,7 @@ export function AuthEncrypter(recipientPublicKey: Uint8Array, senderSecretKey: U * 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 offical CFRG specification is released. */ -export function AnonEncrypter(publicKey: Uint8Array, +export function createAnonEncrypter(publicKey: Uint8Array, options: Partial = {}): Encrypter { return (options !== undefined) ? x25519Encrypter(publicKey, options.kid) : x25519Encrypter(publicKey) } @@ -57,7 +57,7 @@ export function AnonEncrypter(publicKey: Uint8Array, * 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 offical CFRG specification are released. */ -export function AuthDecrypter(recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { +export function createAuthDecrypter(recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecretKey, senderPublicKey); } @@ -68,7 +68,7 @@ export function AuthDecrypter(recipientSecretKey: Uint8Array, senderPublicKey: U * 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 offical CFRG specification is released. */ -export function AnonDecrypter(secretKey: Uint8Array): Decrypter { +export function createAnonDecrypter(secretKey: Uint8Array): Decrypter { return x25519Decrypter(secretKey) } From 13266aaac4e1a989a15c1b30bc935a8f035f4046 Mon Sep 17 00:00:00 2001 From: Oliver Terbu <> Date: Fri, 28 May 2021 16:33:12 +0200 Subject: [PATCH 8/9] fix: fixed exports --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 3e6750cd..bb3f1c52 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,13 @@ export { xc20pDirDecrypter, x25519Encrypter, x25519Decrypter, - resolveX25519Encrypters + resolveX25519Encrypters, + createAuthEncrypter, + createAnonEncrypter, + createAuthDecrypter, + createAnonDecrypter, + xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2, + xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2 } from './xc20pEncryption' export { From b7220aa67d92310487540f5a3f56d70082ee9dcd Mon Sep 17 00:00:00 2001 From: Mircea Nistor Date: Fri, 28 May 2021 18:56:18 +0200 Subject: [PATCH 9/9] style: fix import in test and reformat code --- src/Digest.ts | 8 ++- src/__tests__/JWE.test.ts | 127 ++++++++++++++++++++++++----------- src/__tests__/didkey.test.ts | 3 +- src/xc20pEncryption.ts | 101 +++++++++++++++------------- 4 files changed, 151 insertions(+), 88 deletions(-) diff --git a/src/Digest.ts b/src/Digest.ts index 970cc5d8..9af3cefe 100644 --- a/src/Digest.ts +++ b/src/Digest.ts @@ -27,7 +27,13 @@ const lengthAndInput = (input: Uint8Array): Uint8Array => u8a.concat([writeUint3 // This implementation of concatKDF was inspired by these two implementations: // https://github.com/digitalbazaar/minimal-cipher/blob/master/algorithms/ecdhkdf.js // https://github.com/panva/jose/blob/master/lib/jwa/ecdh/derive.js -export function concatKDF(secret: Uint8Array, keyLen: number, alg: string, producerInfo?:Uint8Array, consumerInfo?:Uint8Array): Uint8Array { +export function concatKDF( + secret: Uint8Array, + keyLen: number, + alg: string, + producerInfo?: Uint8Array, + consumerInfo?: Uint8Array +): Uint8Array { if (keyLen !== 256) throw new Error(`Unsupported key length: ${keyLen}`) const value = u8a.concat([ lengthAndInput(u8a.fromString(alg)), diff --git a/src/__tests__/JWE.test.ts b/src/__tests__/JWE.test.ts index 468e3855..0cc1bc7f 100644 --- a/src/__tests__/JWE.test.ts +++ b/src/__tests__/JWE.test.ts @@ -1,7 +1,13 @@ import { decryptJWE, createJWE, Encrypter } from '../JWE' import vectors from './jwe-vectors.js' -import { xc20pDirEncrypter, xc20pDirDecrypter, x25519Encrypter, x25519Decrypter, - xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2, xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2, AnonEncrypter, AuthEncrypter } from '../xc20pEncryption' +import { + xc20pDirEncrypter, + xc20pDirDecrypter, + x25519Encrypter, + x25519Decrypter, + xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2, + xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2 +} from '../xc20pEncryption' import { decodeBase64url, encodeBase64url } from '../util' import * as u8a from 'uint8arrays' import { randomBytes } from '@stablelib/random' @@ -52,29 +58,36 @@ describe('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(let recipientkey of recipientkeys) { - const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( - u8a.fromString(recipientkey, 'base64pad'), u8a.fromString(senderkey, 'base64pad')) - var cleartextU8a = await decryptJWE(jwe, decrypter) - expect(u8a.toString(cleartextU8a)).toEqual(cleartext) + test.each(vectors.ecdh1PuV3Xc20PkwV2.pass)( + 'decrypts valid jwe', + async ({ senderkey, recipientkeys, cleartext, jwe }) => { + expect.assertions(recipientkeys.length) + for (let recipientkey of recipientkeys) { + const decrypter = xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( + u8a.fromString(recipientkey, 'base64pad'), + u8a.fromString(senderkey, 'base64pad') + ) + var 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(let 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.fail)( + 'fails to decrypt bad jwe', + async ({ senderkey, recipientkeys, jwe }) => { + expect.assertions(recipientkeys.length) + for (let 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 }) => { + 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('Invalid JWE') @@ -241,7 +254,10 @@ describe('JWE', () => { it('Creates with skid, kid, no apu and no apv', async () => { const kid = 'did:example:receiver#key-1' const skid = 'did:example:sender#key-1' - const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { kid, skid } ) + const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { + kid, + skid + }) expect.assertions(6) const jwe = await createJWE(cleartext, [encrypter]) expect(jwe.aad).toBeUndefined() @@ -255,7 +271,10 @@ describe('JWE', () => { it('Creates with no skid, no kid, apu and apv', async () => { const apu = encodeBase64url('Alice') const apv = encodeBase64url('Bob') - const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { apu, apv } ) + const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { + apu, + apv + }) expect.assertions(6) const jwe = await createJWE(cleartext, [encrypter]) expect(jwe.aad).toBeUndefined() @@ -271,7 +290,12 @@ describe('JWE', () => { const skid = 'did:example:sender#key-1' const apu = encodeBase64url('Alice') const apv = encodeBase64url('Bob') - const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { kid, skid, apu, apv } ) + const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey, { + kid, + skid, + apu, + apv + }) expect.assertions(6) const jwe = await createJWE(cleartext, [encrypter]) expect(jwe.aad).toBeUndefined() @@ -283,7 +307,7 @@ describe('JWE', () => { }) it('Creates with data in protected header', async () => { - const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey ) + const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey) expect.assertions(3) const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }) expect(jwe.aad).toBeUndefined() @@ -292,7 +316,7 @@ describe('JWE', () => { }) it('Creates with aad', async () => { - const encrypter = xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientKey.publicKey, senderKey.secretKey ) + 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) @@ -314,16 +338,36 @@ describe('JWE', () => { 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, skid } ), - decrypter: xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipients[0].recipientkey.secretKey, senderkey.publicKey) } } + recipients[0] = { + ...recipients[0], + ...{ + encrypter: xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2( + recipients[0].recipientkey.publicKey, + senderkey.secretKey, + { kid: recipients[0].kid, skid } + ), + 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, skid } ), - decrypter: xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipients[1].recipientkey.secretKey, senderkey.publicKey) } } + recipients[1] = { + ...recipients[1], + ...{ + encrypter: xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2( + recipients[1].recipientkey.publicKey, + senderkey.secretKey, + { kid: recipients[1].kid, skid } + ), + decrypter: xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( + recipients[1].recipientkey.secretKey, + senderkey.publicKey + ) + } + } }) it('Creates with only ciphertext', async () => { @@ -337,7 +381,9 @@ describe('JWE', () => { it('Creates with data in protected header', async () => { expect.assertions(4) - const jwe = await createJWE(cleartext, [recipients[0].encrypter, recipients[1].encrypter], { more: 'protected' }) + const jwe = await createJWE(cleartext, [recipients[0].encrypter, recipients[1].encrypter], { + more: 'protected' + }) expect(jwe.aad).toBeUndefined() expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', skid: skid, more: 'protected' }) expect(await decryptJWE(jwe, recipients[0].decrypter)).toEqual(cleartext) @@ -347,7 +393,12 @@ describe('JWE', () => { 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) + 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', skid: skid, more: 'protected' }) expect(await decryptJWE(jwe, recipients[0].decrypter)).toEqual(cleartext) @@ -364,5 +415,5 @@ describe('JWE', () => { await expect(createJWE(cleartext, [enc1, enc2])).rejects.toThrowError('Incompatible encrypters passed') }) }) - }) + }) }) diff --git a/src/__tests__/didkey.test.ts b/src/__tests__/didkey.test.ts index b868be00..fb9126ec 100644 --- a/src/__tests__/didkey.test.ts +++ b/src/__tests__/didkey.test.ts @@ -31,8 +31,7 @@ describe('Ed25519', () => { id: 'did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM', publicKey: [ { - id: - 'did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM#z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM', + id: 'did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM#z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM', type: 'Ed25519VerificationKey2018', controller: 'did:key:z6MkoTHsgNNrby8JzCNQ1iRLyW5QQ6R8Xuu6AA8igGrMVPUM', publicKeyBase58: 'A12q688RGRdqshXhL9TW8QXQaX9H82ejU9DnqztLaAgy' diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index 68d0e374..f6319164 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -10,10 +10,10 @@ import type { VerificationMethod, Resolvable } from 'did-resolver' const flatten = (arrays: T[]) => [].concat.apply([], arrays) export type AuthEncryptParams = { - kid?: string, - skid?: string, + kid?: string + skid?: string // base64url encoded - apu?: string, + apu?: string // base64url encoded apv?: string } @@ -22,51 +22,53 @@ export type AnonEncryptParams = { kid?: string } -/** +/** * Recommended encrypter for authenticated encryption (i.e. sender authentication and requires - * sender private key to encrypt the data). - * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and + * sender private key to encrypt the data). + * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and * XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). - * + * * 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 offical CFRG specification are released. + * are subject to change as new revisions or until the offical CFRG specification are released. */ -export function createAuthEncrypter(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, - options: Partial = {}): Encrypter { - return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecretKey, options); +export function createAuthEncrypter( + recipientPublicKey: Uint8Array, + senderSecretKey: Uint8Array, + options: Partial = {} +): Encrypter { + return xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey, senderSecretKey, options) } -/** +/** * Recommended encrypter for anonymous encryption (i.e. no sender authentication). * Uses ECDH-ES+XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). - * + * * 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 offical CFRG specification is released. + * is subject to change as new revisions or until the offical CFRG specification is released. */ -export function createAnonEncrypter(publicKey: Uint8Array, - options: Partial = {}): Encrypter { - return (options !== undefined) ? x25519Encrypter(publicKey, options.kid) : x25519Encrypter(publicKey) +export function createAnonEncrypter(publicKey: Uint8Array, options: Partial = {}): Encrypter { + return options !== undefined ? x25519Encrypter(publicKey, options.kid) : x25519Encrypter(publicKey) } -/** +/** * Recommended decrypter for authenticated encryption (i.e. sender authentication and requires * sender public key to decrypt the data). - * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and + * Uses ECDH-1PU [v3](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) and * XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). - * + * * 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 offical CFRG specification are released. + * are subject to change as new revisions or until the offical CFRG specification are released. */ export function createAuthDecrypter(recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { - return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecretKey, senderPublicKey); + return xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2(recipientSecretKey, senderPublicKey) } -/** +/** * Recommended decrypter for anonymous encryption (i.e. no sender authentication). * Uses ECDH-ES+XC20PKW [v2](https://tools.ietf.org/html/draft-amringer-jose-chacha-02). - * + * * 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 offical CFRG specification is released. + * is subject to change as new revisions or until the offical CFRG specification is released. */ export function createAnonDecrypter(secretKey: Uint8Array): Decrypter { return x25519Decrypter(secretKey) @@ -143,34 +145,37 @@ export function x25519Encrypter(publicKey: Uint8Array, kid?: string): Encrypter } return { alg, enc: 'XC20P', encrypt, encryptCek } } - + /** * Implements ECDH-1PU+XC20PKW with XChaCha20Poly1305 based on the following specs: * - [XC20PKW](https://tools.ietf.org/html/draft-amringer-jose-chacha-02) * - [ECDH-1PU](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) */ -export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2(recipientPublicKey: Uint8Array, senderSecretKey: Uint8Array, - options: Partial = {}): Encrypter { - +export function xc20pAuthEncrypterEcdh1PuV3x25519WithXc20PkwV2( + recipientPublicKey: Uint8Array, + senderSecretKey: Uint8Array, + options: Partial = {} +): Encrypter { const alg = 'ECDH-1PU+XC20PKW' const keyLen = 256 const crv = 'X25519' - - let partyUInfo, partyVInfo + + let partyUInfo + let partyVInfo if (options.apu !== undefined) partyUInfo = base64ToBytes(options.apu) if (options.apv !== undefined) partyVInfo = base64ToBytes(options.apv) - + async function encryptCek(cek): Promise { const epk = generateKeyPair() const zE = sharedKey(epk.secretKey, recipientPublicKey) - // ECDH-1PU requires additional shared secret between + // ECDH-1PU requires additional shared secret between // static key of sender and static key of recipient const zS = sharedKey(senderSecretKey, recipientPublicKey) - - let sharedSecret = new Uint8Array(zE.length + zS.length); - sharedSecret.set(zE); - sharedSecret.set(zS, zE.length); + + const sharedSecret = new Uint8Array(zE.length + zS.length) + sharedSecret.set(zE) + sharedSecret.set(zS, zE.length) // Key Encryption Key const kek = concatKDF(sharedSecret, keyLen, alg, partyUInfo, partyVInfo) @@ -266,27 +271,29 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter { * - [XC20PKW](https://tools.ietf.org/html/draft-amringer-jose-chacha-02) * - [ECDH-1PU](https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-03) */ - export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( - recipientSecretKey: Uint8Array, senderPublicKey: Uint8Array): Decrypter { - +export function xc20pAuthDecrypterEcdh1PuV3x25519WithXc20PkwV2( + recipientSecretKey: Uint8Array, + senderPublicKey: Uint8Array +): Decrypter { const alg = 'ECDH-1PU+XC20PKW' const keyLen = 256 const crv = 'X25519' async function decrypt(sealed, iv, aad, recipient): Promise { validateHeader(recipient.header) if (recipient.header.epk.crv !== crv) return null - // ECDH-1PU requires additional shared secret between + // 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 sharedSecret = new Uint8Array(zE.length + zS.length); - sharedSecret.set(zE); - sharedSecret.set(zS, zE.length); + + const sharedSecret = new Uint8Array(zE.length + zS.length) + sharedSecret.set(zE) + sharedSecret.set(zS, zE.length) // Key Encryption Key - let producerInfo, consumerInfo + let producerInfo + let consumerInfo if (recipient.header.apu) producerInfo = base64ToBytes(recipient.header.apu) if (recipient.header.apv) consumerInfo = base64ToBytes(recipient.header.apv) @@ -299,4 +306,4 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter { return xc20pDirDecrypter(cek).decrypt(sealed, iv, aad) } return { alg, enc: 'XC20P', decrypt } -} \ No newline at end of file +}