Skip to content

Commit

Permalink
feat: Add support for authenticated encryption (#177)
Browse files Browse the repository at this point in the history
* fix: added ECDH-1PU+XC20PKW; added AnonEncrypter/Decrypter; added AuthEncrypter/Decrypter
* fix: handling undefined options for anoncrypt
* fix: renamed generic anon/auth encrypter/decrypter names
  • Loading branch information
awoie authored May 31, 2021
1 parent 5731992 commit 9a71b07
Show file tree
Hide file tree
Showing 7 changed files with 523 additions and 9 deletions.
13 changes: 10 additions & 3 deletions src/Digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]))
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
234 changes: 232 additions & 2 deletions src/__tests__/JWE.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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')
})
})
})
})
3 changes: 1 addition & 2 deletions src/__tests__/didkey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading

0 comments on commit 9a71b07

Please sign in to comment.