Skip to content

Commit

Permalink
feat: use multiple keyAgreementKeys when creating JWE (#166)
Browse files Browse the repository at this point in the history
  • Loading branch information
dankelleher authored May 11, 2021
1 parent 20825f2 commit e327ef2
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 65 deletions.
134 changes: 91 additions & 43 deletions src/__tests__/xc20pEncryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,69 @@ import { generateKeyPair } from '@stablelib/x25519'

describe('xc20pEncryption', () => {
describe('resolveX25519Encrypters', () => {
let resolver, did1, did2, did3, did4
const did1 = 'did:test:1'
const did2 = 'did:test:2'
const did3 = 'did:test:3'
const did4 = 'did:test:4'

let resolver
let decrypter1, decrypter2

beforeAll(() => {
did1 = 'did:test:1'
did2 = 'did:test:2'
did3 = 'did:test:3'
did4 = 'did:test:4'
let didDocumentResult1, didDocumentResult2, didDocumentResult3, didDocumentResult4

beforeEach(() => {
const kp1 = generateKeyPair()
const kp2 = generateKeyPair()
decrypter1 = x25519Decrypter(kp1.secretKey)
decrypter2 = x25519Decrypter(kp2.secretKey)
resolver = {
resolve: jest.fn((did) => {
if (did === did1) {
return {
didDocument: {
verificationMethod: [
{
id: did1 + '#abc',
type: 'X25519KeyAgreementKey2019',
controller: did1,
publicKeyBase58: u8a.toString(kp1.publicKey, 'base58btc')
}
],
keyAgreement: [
{
id: 'irrelevant key'
},
did1 + '#abc'
]
}

didDocumentResult1 = {
didDocument: {
verificationMethod: [
{
id: did1 + '#abc',
type: 'X25519KeyAgreementKey2019',
controller: did1,
publicKeyBase58: u8a.toString(kp1.publicKey, 'base58btc')
}
} else if (did === did2) {
return {
didDocument: {
verificationMethod: [],
keyAgreement: [
{
id: did2 + '#abc',
type: 'X25519KeyAgreementKey2019',
controller: did2,
publicKeyBase58: u8a.toString(kp2.publicKey, 'base58btc')
}
]
}
],
keyAgreement: [
{
id: 'irrelevant key'
},
did1 + '#abc'
]
}
}

didDocumentResult2 = {
didDocument: {
verificationMethod: [],
keyAgreement: [
{
id: did2 + '#abc',
type: 'X25519KeyAgreementKey2019',
controller: did2,
publicKeyBase58: u8a.toString(kp2.publicKey, 'base58btc')
}
} else if (did === did3) {
return { didResolutionMetadata: { error: 'notFound' }, didDocument: null }
} else if (did === did4) {
return { didDocument: { publicKey: [], keyAgreement: [{ type: 'wrong type' }] } }
]
}
}

didDocumentResult3 = { didResolutionMetadata: { error: 'notFound' }, didDocument: null }
didDocumentResult4 = { didDocument: { publicKey: [], keyAgreement: [{ type: 'wrong type' }] } }

resolver = {
resolve: jest.fn((did) => {
switch (did) {
case did1:
return didDocumentResult1
case did2:
return didDocumentResult2
case did3:
return didDocumentResult3
case did4:
return didDocumentResult4
}
})
}
Expand All @@ -83,5 +95,41 @@ describe('xc20pEncryption', () => {
'Could not find x25519 key for did:test:4'
)
})

it('resolves encrypters for DIDs with multiple valid keys ', async () => {
expect.assertions(6)

const secondKp1 = generateKeyPair()
const secondKp2 = generateKeyPair()

const newDecrypter1 = x25519Decrypter(secondKp1.secretKey)
const newDecrypter2 = x25519Decrypter(secondKp2.secretKey)

didDocumentResult1.didDocument.verificationMethod.push({
id: did1 + '#def',
type: 'X25519KeyAgreementKey2019',
controller: did1,
publicKeyBase58: u8a.toString(secondKp1.publicKey, 'base58btc')
})
didDocumentResult1.didDocument.keyAgreement.push(did1 + '#def')

didDocumentResult2.didDocument.keyAgreement.push({
id: did2 + '#def',
type: 'X25519KeyAgreementKey2019',
controller: did2,
publicKeyBase58: u8a.toString(secondKp2.publicKey, 'base58btc')
})

const encrypters = await resolveX25519Encrypters([did1, did2], resolver)
const cleartext = randomBytes(8)
const jwe = await createJWE(cleartext, encrypters)

expect(jwe.recipients[0].header.kid).toEqual(did1 + '#abc')
expect(jwe.recipients[1].header.kid).toEqual(did1 + '#def')
expect(jwe.recipients[2].header.kid).toEqual(did2 + '#abc')
expect(jwe.recipients[3].header.kid).toEqual(did2 + '#def')
expect(await decryptJWE(jwe, newDecrypter1)).toEqual(cleartext)
expect(await decryptJWE(jwe, newDecrypter2)).toEqual(cleartext)
})
})
})
48 changes: 26 additions & 22 deletions src/xc20pEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { bytesToBase64url, base58ToBytes, encodeBase64url, toSealed, base64ToByt
import { Recipient, EncryptionResult, Encrypter, Decrypter } from './JWE'
import type { VerificationMethod, Resolvable } from 'did-resolver'

// remove when targeting node 11+ or ES2019
const flatten = <T>(arrays: T[]) => [].concat.apply([], arrays)

function xc20pEncrypter(key: Uint8Array): (cleartext: Uint8Array, aad?: Uint8Array) => EncryptionResult {
const cipher = new XChaCha20Poly1305(key)
return (cleartext: Uint8Array, aad?: Uint8Array) => {
Expand Down Expand Up @@ -79,30 +82,31 @@ export function x25519Encrypter(publicKey: Uint8Array, kid?: string): Encrypter
}

export async function resolveX25519Encrypters(dids: string[], resolver: Resolvable): Promise<Encrypter[]> {
return Promise.all(
dids.map(async (did) => {
const { didResolutionMetadata, didDocument } = await resolver.resolve(did)
if (didResolutionMetadata?.error) {
throw new Error(
`Could not find x25519 key for ${did}: ${didResolutionMetadata.error}, ${didResolutionMetadata.message}`
)
const encryptersForDID = async (did): Promise<Encrypter[]> => {
const { didResolutionMetadata, didDocument } = await resolver.resolve(did)
if (didResolutionMetadata?.error) {
throw new Error(
`Could not find x25519 key for ${did}: ${didResolutionMetadata.error}, ${didResolutionMetadata.message}`
)
}
if (!didDocument.keyAgreement) throw new Error(`Could not find x25519 key for ${did}`)
const agreementKeys: VerificationMethod[] = didDocument.keyAgreement?.map((key) => {
if (typeof key === 'string') {
return [...(didDocument.publicKey || []), ...(didDocument.verificationMethod || [])].find((pk) => pk.id === key)
}
if (!didDocument.keyAgreement) throw new Error(`Could not find x25519 key for ${did}`)
const agreementKeys: VerificationMethod[] = didDocument.keyAgreement?.map((key) => {
if (typeof key === 'string') {
return [...(didDocument.publicKey || []), ...(didDocument.verificationMethod || [])].find(
(pk) => pk.id === key
)
}
return key
})
const pk = agreementKeys.find((key) => {
return key.type === 'X25519KeyAgreementKey2019' && Boolean(key.publicKeyBase58)
})
if (!pk) throw new Error(`Could not find x25519 key for ${did}`)
return x25519Encrypter(base58ToBytes(pk.publicKeyBase58), pk.id)
return key
})
const pks = agreementKeys.filter((key) => {
return key.type === 'X25519KeyAgreementKey2019' && Boolean(key.publicKeyBase58)
})
)
if (!pks.length) throw new Error(`Could not find x25519 key for ${did}`)
return pks.map((pk) => x25519Encrypter(base58ToBytes(pk.publicKeyBase58), pk.id))
}

const encrypterPromises = dids.map((did) => encryptersForDID(did))
const encrypterArrays = await Promise.all(encrypterPromises)

return flatten(encrypterArrays)
}

function validateHeader(header: Record<string, any>) {
Expand Down

0 comments on commit e327ef2

Please sign in to comment.