Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ services:
##################################################################################

veritable-cloudagent-alice:
image: digicatapult/veritable-cloudagent:v0.19.5
image: digicatapult/veritable-cloudagent:v0.20.1
container_name: veritable-cloudagent-alice
restart: always
depends_on:
Expand Down Expand Up @@ -67,7 +67,7 @@ services:

veritable-cloudagent-bob:
container_name: veritable-cloudagent-bob
image: digicatapult/veritable-cloudagent:v0.19.5
image: digicatapult/veritable-cloudagent:v0.20.1
restart: always
depends_on:
ipfs:
Expand Down
7,940 changes: 3,694 additions & 4,246 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@digicatapult/veritable-encryption-service",
"version": "0.2.11",
"version": "0.2.12",
"description": "An OpenAPI service for encryption in Veritable",
"main": "src/index.ts",
"type": "module",
Expand Down Expand Up @@ -52,8 +52,8 @@
"@types/sinon": "^21.0.1",
"@types/supertest": "^7.2.0",
"@types/swagger-ui-express": "^4.1.8",
"@typescript-eslint/eslint-plugin": "^8.59.4",
"@typescript-eslint/parser": "^8.59.4",
"@typescript-eslint/eslint-plugin": "^8.60.0",
"@typescript-eslint/parser": "^8.60.0",
"chai": "^6.2.2",
"chai-as-promised": "^8.0.2",
"concurrently": "^9.2.1",
Expand All @@ -73,7 +73,7 @@
"typescript": "^6.0.3"
},
"dependencies": {
"@credo-ts/core": "^0.6.3",
"@credo-ts/core": "^0.7.0",
"@openwallet-foundation/askar-nodejs": "^0.6.0",
"@tsoa/runtime": "^6.6.0",
"@tweedegolf/sab-adapter-minio": "^3.0.2",
Expand All @@ -85,7 +85,7 @@
"cors": "^2.8.6",
"dotenv": "^17.4.2",
"envalid": "^8.1.1",
"expo": "56.0.3",
"expo": "56.0.5",
"express": "^5.2.1",
"knex": "^3.2.10",
"multer": "^2.1.1",
Expand All @@ -101,6 +101,6 @@
},
"overrides": {
"ref-napi": "npm:@2060.io/ref-napi",
"node-addon-api": "8.7.0"
"node-addon-api": "8.8.0"
}
}
4 changes: 2 additions & 2 deletions src/controllers/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import express from 'express'
import { container } from 'tsyringe'
import { BadRequest } from '../../error.js'
import Database from '../../lib/db/index.js'
import { findPublicKeyBase64 } from '../../services/cloudagent/did.js'
import { findPublicKeyBase64Url } from '../../services/cloudagent/did.js'
import Cloudagent from '../../services/cloudagent/index.js'
import { ENCRYPTION_CONFIGS } from '../../services/encryption/config.js'
import Encryption from '../../services/encryption/index.js'
Expand Down Expand Up @@ -55,7 +55,7 @@ export class FilesController extends Controller {

const recipientDidDoc = await this.cloudagent.resolveDid(recipientDid)

const recipientPublicKey64 = findPublicKeyBase64(recipientDidDoc)
const recipientPublicKey64 = findPublicKeyBase64Url(recipientDidDoc)
if (!recipientPublicKey64) {
throw new BadRequest(`No valid public key found for DID '${recipientDid}'`)
}
Expand Down
6 changes: 3 additions & 3 deletions src/services/cloudagent/did.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from '@credo-ts/core'
import { DidDocument as CloudagentDidDocument } from './types.js'

export const findPublicKeyBase64 = (didDocument: CloudagentDidDocument): string | undefined => {
export const findPublicKeyBase64Url = (didDocument: CloudagentDidDocument): string | undefined => {
let credoDidDocument: CredoDidDocument
try {
credoDidDocument = JsonTransformer.fromJSON(didDocument, CredoDidDocument)
Expand All @@ -25,8 +25,8 @@ export const findPublicKeyBase64 = (didDocument: CloudagentDidDocument): string
const publicKey = publicJwk?.publicKey
if (!publicKey || publicKey.kty !== 'OKP' || publicKey.crv !== 'X25519') continue

// Return base64 encoding of the raw X25519 public key bytes
return TypedArrayEncoder.toBase64(publicKey.publicKey)
// Return base64Url encoding of the raw X25519 public key bytes
return TypedArrayEncoder.toBase64Url(publicKey.publicKey)
}

return undefined
Expand Down
20 changes: 10 additions & 10 deletions src/services/encryption/ecdh.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Buffer as CredoBuffer, JsonEncoder, TypedArrayEncoder } from '@credo-ts/core'
import { JsonEncoder, TypedArrayEncoder } from '@credo-ts/core'
import { Key as AskarKey, EcdhEs, KeyAlgorithm } from '@openwallet-foundation/askar-nodejs'

export const ENC = 'A256GCM'
export const ALG = 'ECDH-ES'
const X25519 = KeyAlgorithm.X25519

export function encryptEcdh(plaintext: Buffer, publicKey64: string): string {
const recipientPublicKey = TypedArrayEncoder.fromBase64(publicKey64)
export function encryptEcdh(plaintext: Buffer, publicKey: string): string {
const recipientPublicKey = TypedArrayEncoder.fromBase64Url(publicKey)

let ephemeralKey: AskarKey | undefined

Expand All @@ -19,10 +19,10 @@ export function encryptEcdh(plaintext: Buffer, publicKey64: string): string {
epk: ephemeralKey.jwkPublic,
}

const encodedHeader = JsonEncoder.toBase64URL(header)
const encodedHeader = JsonEncoder.toBase64Url(header)

const ecdh = new EcdhEs({
algId: Uint8Array.from(CredoBuffer.from(ENC)),
algId: TypedArrayEncoder.fromUtf8String(ENC),
apu: Uint8Array.from([]),
apv: Uint8Array.from([]),
})
Expand All @@ -35,18 +35,18 @@ export function encryptEcdh(plaintext: Buffer, publicKey64: string): string {
const { ciphertext, tag, nonce } = ecdh.encryptDirect({
encryptionAlgorithm: KeyAlgorithm.AesA256Gcm,
ephemeralKey,
message: Uint8Array.from(CredoBuffer.from(plaintext)),
message: plaintext,
recipientKey: recipientAskarKey,
aad: Uint8Array.from(CredoBuffer.from(encodedHeader)), // AAD as bytes of encoded header
aad: TypedArrayEncoder.fromUtf8String(encodedHeader), // AAD as bytes of encoded header
})

// Return compact JWE
return [
encodedHeader,
'', // No encrypted key for ECDH-ES
TypedArrayEncoder.toBase64URL(nonce),
TypedArrayEncoder.toBase64URL(ciphertext),
TypedArrayEncoder.toBase64URL(tag),
TypedArrayEncoder.toBase64Url(nonce),
TypedArrayEncoder.toBase64Url(ciphertext),
TypedArrayEncoder.toBase64Url(tag),
].join('.')
} finally {
ephemeralKey?.handle.free()
Expand Down
4 changes: 2 additions & 2 deletions test/helpers/twoPartyContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import env from '../../src/env.js'
import { resetContainer } from '../../src/ioc.js'
import Database from '../../src/lib/db/index.js'
import { UUID } from '../../src/models/stringTypes.js'
import { findPublicKeyBase64 } from '../../src/services/cloudagent/did.js'
import { findPublicKeyBase64Url } from '../../src/services/cloudagent/did.js'
import VeritableCloudagent from '../../src/services/cloudagent/index.js'
import { Connection, Credential } from '../../src/services/cloudagent/types.js'
import { ENCRYPTION_CONFIGS } from '../../src/services/encryption/config.js'
Expand Down Expand Up @@ -124,7 +124,7 @@ export async function testCleanup(context: TwoPartyContext) {

export const resolveLocalPublicKey = async (context: TwoPartyContext) => {
const did = await context.localCloudagent.resolveDid(localDidWeb)
const publicKey64 = findPublicKeyBase64(did)
const publicKey64 = findPublicKeyBase64Url(did)
if (!publicKey64) throw new Error(`Failed to find public key for resolved local DID:web (${localDidWeb})`)
return publicKey64
}
4 changes: 2 additions & 2 deletions test/integration/encryption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { decode, encode } from 'cbor2'
import * as chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { InternalError } from '../../src/error.js'
import { findPublicKeyBase64 } from '../../src/services/cloudagent/did.js'
import { findPublicKeyBase64Url } from '../../src/services/cloudagent/did.js'
import { EncryptedPayload } from '../../src/services/encryption/aesGcm.js'
import {
resolveLocalPublicKey,
Expand Down Expand Up @@ -66,7 +66,7 @@ describe('encryption', async () => {

it('error - cek decryption fails with wrong recipient public key', async () => {
const bobDid = await context.remoteCloudagent.resolveDid('did:web:veritable-cloudagent-bob%3A8443')
const wrongPublicKey = findPublicKeyBase64(bobDid)
const wrongPublicKey = findPublicKeyBase64Url(bobDid)
if (!wrongPublicKey) throw new Error('Failed to find wrong recipient public key')
const { encryptedCek } = context.localEncryption.encryptPlaintext(plaintext, recipientPublicKey)

Expand Down
4 changes: 2 additions & 2 deletions test/integration/fileUpload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as sinon from 'sinon'
import request from 'supertest'
import env from '../../src/env.js'
import createHttpServer from '../../src/server.js'
import { findPublicKeyBase64 } from '../../src/services/cloudagent/did.js'
import { findPublicKeyBase64Url } from '../../src/services/cloudagent/did.js'
import { DidDocument } from '../../src/services/cloudagent/types.js'
import { setupTwoPartyContext, testCleanup, TwoPartyContext } from '../helpers/twoPartyContext.js'

Expand All @@ -23,7 +23,7 @@ describe('File Upload controller tests', function () {
app = await createHttpServer()
recipientDid = 'did:web:veritable-cloudagent-alice%3A8443'
const did = await context.localCloudagent.resolveDid(recipientDid)
recipientPublicKey = findPublicKeyBase64(did)!
recipientPublicKey = findPublicKeyBase64Url(did)!
})

afterEach(() => {
Expand Down
36 changes: 18 additions & 18 deletions test/unit/did.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { expect } from 'chai'

import { Kms, TypedArrayEncoder } from '@credo-ts/core'

import { findPublicKeyBase64 } from '../../src/services/cloudagent/did.js'
import { findPublicKeyBase64Url } from '../../src/services/cloudagent/did.js'
import type { DidDocument } from '../../src/services/cloudagent/types.js'

const createOkpPublicKeyBytes = (fill: number) => new Uint8Array(32).fill(fill)
Expand All @@ -19,7 +19,7 @@ describe('cloudagent did key extraction', () => {
// DID:web documents commonly use keyAgreement as a string reference that points
// at a verificationMethod entry
const x25519PublicKey = createOkpPublicKeyBytes(1)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:web:example.com',
Expand All @@ -34,13 +34,13 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})

it('returns key when keyAgreement embeds the verification method', () => {
// DIDDocs can embed the key agreement verification method directly in keyAgreement
const x25519PublicKey = createOkpPublicKeyBytes(2)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:peer:123',
Expand All @@ -54,14 +54,14 @@ describe('cloudagent did key extraction', () => {
],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})

it('returns key when keyAgreement references JsonWebKey2020 publicKeyJwk for X25519', () => {
// DIDDocs can use JWK (eg. OKP/X25519), rather than base58 or multibase
const x25519PublicKey = createOkpPublicKeyBytes(3)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const jwkX = TypedArrayEncoder.toBase64URL(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)
const jwkX = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:web:example.com',
Expand All @@ -80,14 +80,14 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})

it('returns key when keyAgreement uses a fragment-only reference', () => {
// DIDDoc serializations can use fragment-only references (eg. '#encryption')
// for keyAgreement entries
const x25519PublicKey = createOkpPublicKeyBytes(4)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:web:example.com',
Expand All @@ -102,15 +102,15 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})

it('returns the first usable X25519 key when multiple keyAgreement entries are present', () => {
// DIDDocs may publish multiple keyAgreement references
// We should skip unusable entries and return the first X25519
const ed25519PublicKey = createOkpPublicKeyBytes(5)
const x25519PublicKey = createOkpPublicKeyBytes(6)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:web:example.com',
Expand All @@ -131,7 +131,7 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#signing', 'did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})

it('returns undefined when keyAgreement is a reference but verificationMethod is missing', () => {
Expand All @@ -141,7 +141,7 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(undefined)
expect(findPublicKeyBase64Url(didDocument)).to.equal(undefined)
})

it('ignores non-X25519 verification methods', () => {
Expand All @@ -160,13 +160,13 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(undefined)
expect(findPublicKeyBase64Url(didDocument)).to.equal(undefined)
})

it('supports canonical Multikey/publicKeyMultibase for X25519', () => {
// DIDDocs can represent key material via Multikey + publicKeyMultibase
const x25519PublicKey = createOkpPublicKeyBytes(7)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:web:example.com',
Expand All @@ -181,14 +181,14 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})

it('supports a mixed DIDDoc with signing and key-agreement Multikey verification methods', () => {
// DID:web documents typically publish both a signing key (Ed25519) and an agreement key (X25519)
const ed25519PublicKey = createOkpPublicKeyBytes(8)
const x25519PublicKey = createOkpPublicKeyBytes(9)
const expectedBase64 = TypedArrayEncoder.toBase64(x25519PublicKey)
const expectedBase64Url = TypedArrayEncoder.toBase64Url(x25519PublicKey)

const didDocument: DidDocument = {
id: 'did:web:example.com',
Expand All @@ -209,6 +209,6 @@ describe('cloudagent did key extraction', () => {
keyAgreement: ['did:web:example.com#encryption'],
}

expect(findPublicKeyBase64(didDocument)).to.equal(expectedBase64)
expect(findPublicKeyBase64Url(didDocument)).to.equal(expectedBase64Url)
})
})
Loading