Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for authenticated encryption #177

Merged
merged 9 commits into from
May 31, 2021
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/Digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(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]))
Expand Down
2 changes: 2 additions & 0 deletions src/JWE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ interface RecipientHeader {
tag: string
epk?: Record<string, any> // Ephemeral Public Key
kid?: string
apv?: string
apu?: string
}

export interface Recipient {
Expand Down
183 changes: 181 additions & 2 deletions src/__tests__/JWE.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -49,6 +50,36 @@ 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', () => {
Expand Down Expand Up @@ -186,4 +217,152 @@ 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')
})
})
})
})
Loading