diff --git a/src/Digest.ts b/src/Digest.ts index 7bf05b34..9af3cefe 100644 --- a/src/Digest.ts +++ b/src/Digest.ts @@ -27,14 +27,21 @@ 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(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..ba32418b 100644 --- a/src/JWE.ts +++ b/src/JWE.ts @@ -6,6 +6,8 @@ interface RecipientHeader { tag: string epk?: Record // Ephemeral Public Key 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..0cc1bc7f 100644 --- a/src/__tests__/JWE.test.ts +++ b/src/__tests__/JWE.test.ts @@ -1,7 +1,14 @@ 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 +56,43 @@ describe('JWE', () => { await expect(decryptJWE(jwe as any, decrypter)).rejects.toThrowError('Invalid 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.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 }) => { + 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 +230,190 @@ describe('JWE', () => { }) }) }) + + describe('ECDH-1PU+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__/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/__tests__/jwe-vectors.js b/src/__tests__/jwe-vectors.js index b1b6e436..e8114bb3 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"}, } ] - } + }, + ecdh1PuV3Xc20PkwV2: { + 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/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 { diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index c1521f1b..f6319164 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -9,6 +9,71 @@ 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 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. + */ +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 + * 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 createAuthDecrypter(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 createAnonDecrypter(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) => { @@ -81,6 +146,70 @@ 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 { + const alg = 'ECDH-1PU+XC20PKW' + const keyLen = 256 + const crv = 'X25519' + + 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 + // static key of sender and static key of recipient + const zS = sharedKey(senderSecretKey, recipientPublicKey) + + 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) + + 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 => { const { didResolutionMetadata, didDocument } = await resolver.resolve(did) @@ -136,3 +265,45 @@ export function x25519Decrypter(secretKey: Uint8Array): Decrypter { } return { alg, enc: 'XC20P', decrypt } } + +/** + * 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' + 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) + + const sharedSecret = new Uint8Array(zE.length + zS.length) + sharedSecret.set(zE) + sharedSecret.set(zS, zE.length) + + // Key Encryption Key + let producerInfo + let 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 } +}