From 73868594ad02bd5ff6388d1fa7bcd395100f635b Mon Sep 17 00:00:00 2001 From: Nam Hoang Date: Wed, 19 Jul 2023 10:26:55 +0700 Subject: [PATCH] feat: implement the encrypted storage plugin Signed-off-by: Nam Hoang --- packages/cli/package.json | 1 + packages/core-types/package.json | 3 +- packages/core-types/src/index.ts | 49 +++++----- packages/core-types/src/plugin.schema.json | 97 +++++++++++++++++++ .../core-types/src/types/IEncryptedStorage.ts | 54 +++++++++++ packages/encrypted-storage/README.md | 87 +++++++++++++++++ packages/encrypted-storage/api-extractor.json | 18 ++++ packages/encrypted-storage/package.json | 46 +++++++++ .../src/encrypted-storage.ts | 82 ++++++++++++++++ .../src/encrypted-store-middleware.ts | 71 ++++++++++++++ .../src/encrypted-store-router.ts | 33 +++++++ .../src/entities/credential-encrypted-data.ts | 13 +++ .../src/entities/encrypted-data.ts | 36 +++++++ .../src/identifier/encrypted-data-store.ts | 69 +++++++++++++ packages/encrypted-storage/src/index.ts | 10 ++ .../src/migrations/1.createDatabase.ts | 80 +++++++++++++++ .../encrypted-storage/src/migrations/index.ts | 18 ++++ .../src/migrations/migration-functions.ts | 50 ++++++++++ .../express-interceptor/index.d.ts | 1 + packages/encrypted-storage/src/utils.ts | 19 ++++ packages/encrypted-storage/tsconfig.json | 11 +++ 21 files changed, 823 insertions(+), 25 deletions(-) create mode 100644 packages/core-types/src/types/IEncryptedStorage.ts create mode 100644 packages/encrypted-storage/README.md create mode 100644 packages/encrypted-storage/api-extractor.json create mode 100644 packages/encrypted-storage/package.json create mode 100644 packages/encrypted-storage/src/encrypted-storage.ts create mode 100644 packages/encrypted-storage/src/encrypted-store-middleware.ts create mode 100644 packages/encrypted-storage/src/encrypted-store-router.ts create mode 100644 packages/encrypted-storage/src/entities/credential-encrypted-data.ts create mode 100644 packages/encrypted-storage/src/entities/encrypted-data.ts create mode 100644 packages/encrypted-storage/src/identifier/encrypted-data-store.ts create mode 100644 packages/encrypted-storage/src/index.ts create mode 100644 packages/encrypted-storage/src/migrations/1.createDatabase.ts create mode 100644 packages/encrypted-storage/src/migrations/index.ts create mode 100644 packages/encrypted-storage/src/migrations/migration-functions.ts create mode 100644 packages/encrypted-storage/src/module-types/express-interceptor/index.d.ts create mode 100644 packages/encrypted-storage/src/utils.ts create mode 100644 packages/encrypted-storage/tsconfig.json diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ca8ae12..cd2cd6c7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -44,6 +44,7 @@ "@vckit/remote-server": "^1.0.0-beta.5", "@vckit/renderer": "^1.0.0-beta.5", "@vckit/vc-api": "workspace:1.0.0-beta.5", + "@vckit/encrypted-storage": "workspace:*", "@veramo/core": "5.2.0", "@veramo/credential-eip712": "5.2.0", "@veramo/credential-ld": "5.2.0", diff --git a/packages/core-types/package.json b/packages/core-types/package.json index 39a154f1..49f622d4 100644 --- a/packages/core-types/package.json +++ b/packages/core-types/package.json @@ -28,7 +28,8 @@ "ICredentialStatus": "./src/types/ICredentialStatus.ts", "ICredentialStatusVerifier": "./src/types/ICredentialStatusVerifier.ts", "ICredentialStatusManager": "./src/types/ICredentialStatusManager.ts", - "IRenderer": "./src/types/IRender.ts" + "IRenderer": "./src/types/IRender.ts", + "IEncryptedStorage": "./src/types/IEncryptedStorage.ts" } }, "dependencies": { diff --git a/packages/core-types/src/index.ts b/packages/core-types/src/index.ts index 0e1568ad..3c653432 100644 --- a/packages/core-types/src/index.ts +++ b/packages/core-types/src/index.ts @@ -5,27 +5,28 @@ * * @packageDocumentation */ -export { CoreEvents } from './coreEvents.js' -export * from './agent.js' -export * from './types/IAgent.js' -export * from './types/IOACredentialPlugin.js' -export * from './types/ICredentialPlugin.js' -export * from './types/ICredentialIssuer.js' -export * from './types/ICredentialVerifier.js' -export * from './types/ICredentialStatus.js' -export * from './types/ICredentialStatusManager.js' -export * from './types/ICredentialStatusVerifier.js' -export * from './types/IDataStore.js' -export * from './types/IDataStoreORM.js' -export * from './types/IIdentifier.js' -export * from './types/IDIDManager.js' -export * from './types/IKeyManager.js' -export * from './types/IMessage.js' -export * from './types/IMessageHandler.js' -export * from './types/IResolver.js' -export * from './types/IError.js' -export * from './types/IVerifyResult.js' -export * from './types/vc-data-model.js' -export * from './types/IQRCodeEndpoint.js' -export * from './types/IRender.js' -export * from './types/IRendererProvider.js' +export { CoreEvents } from './coreEvents.js'; +export * from './agent.js'; +export * from './types/IAgent.js'; +export * from './types/IOACredentialPlugin.js'; +export * from './types/ICredentialPlugin.js'; +export * from './types/ICredentialIssuer.js'; +export * from './types/ICredentialVerifier.js'; +export * from './types/ICredentialStatus.js'; +export * from './types/ICredentialStatusManager.js'; +export * from './types/ICredentialStatusVerifier.js'; +export * from './types/IDataStore.js'; +export * from './types/IDataStoreORM.js'; +export * from './types/IIdentifier.js'; +export * from './types/IDIDManager.js'; +export * from './types/IKeyManager.js'; +export * from './types/IMessage.js'; +export * from './types/IMessageHandler.js'; +export * from './types/IResolver.js'; +export * from './types/IError.js'; +export * from './types/IVerifyResult.js'; +export * from './types/vc-data-model.js'; +export * from './types/IQRCodeEndpoint.js'; +export * from './types/IRender.js'; +export * from './types/IRendererProvider.js'; +export * from './types/IEncryptedStorage.js'; diff --git a/packages/core-types/src/plugin.schema.json b/packages/core-types/src/plugin.schema.json index 86d5ad60..717777c8 100644 --- a/packages/core-types/src/plugin.schema.json +++ b/packages/core-types/src/plugin.schema.json @@ -6203,5 +6203,102 @@ } } } + }, + "IEncryptedStorage": { + "components": { + "schemas": { + "IEncryptAndStoreDataArgs": { + "type": "object", + "properties": { + "data": {} + }, + "required": [ + "data" + ] + }, + "IEncrypteAndStoreDataResult": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "key": { + "type": "string" + } + }, + "required": [ + "id", + "key" + ] + }, + "IFetchEncryptedDataArgs": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "IFetchEncryptedDataByCredentialHashArgs": { + "type": "object", + "properties": { + "credentialHash": { + "type": "string" + } + }, + "required": [ + "credentialHash" + ] + }, + "IFetchEncryptedDataByCredentialHashResult": { + "type": "object", + "properties": { + "encryptedData": { + "type": "string" + }, + "encryptedDataId": { + "type": "string" + }, + "decryptedKey": { + "type": "string" + } + }, + "required": [ + "encryptedData", + "encryptedDataId", + "decryptedKey" + ] + } + }, + "methods": { + "encryptAndStoreData": { + "description": "", + "arguments": { + "$ref": "#/components/schemas/IEncryptAndStoreDataArgs" + }, + "returnType": { + "$ref": "#/components/schemas/IEncrypteAndStoreDataResult" + } + }, + "fetchEncryptedData": { + "description": "", + "arguments": { + "$ref": "#/components/schemas/IFetchEncryptedDataArgs" + }, + "returnType": { + "type": "string" + } + }, + "fetchEncryptedDataByCredentialHash": { + "description": "", + "arguments": { + "$ref": "#/components/schemas/IFetchEncryptedDataByCredentialHashArgs" + }, + "returnType": { + "$ref": "#/components/schemas/IFetchEncryptedDataByCredentialHashResult" + } + } + } + } } } \ No newline at end of file diff --git a/packages/core-types/src/types/IEncryptedStorage.ts b/packages/core-types/src/types/IEncryptedStorage.ts new file mode 100644 index 00000000..a9baf4bf --- /dev/null +++ b/packages/core-types/src/types/IEncryptedStorage.ts @@ -0,0 +1,54 @@ +import { IPluginMethodMap } from './IAgent'; + +/** + * @public + */ +export interface IEncryptAndStoreDataArgs { + data: any; +} + +/** + * @public + */ +export interface IEncrypteAndStoreDataResult { + id: string; + key: string; +} + +/** + * @public + */ +export interface IFetchEncryptedDataArgs { + id?: string; +} + +/** + * @public + */ +export interface IFetchEncryptedDataByCredentialHashArgs { + credentialHash: string; +} + +/** + * @public + */ +export interface IFetchEncryptedDataByCredentialHashResult { + encryptedData: string; + encryptedDataId: string; + decryptedKey: string; +} + +/** + * @public + */ +export interface IEncryptedStorage extends IPluginMethodMap { + encryptAndStoreData( + args: IEncryptAndStoreDataArgs + ): Promise; + + fetchEncryptedData(args: IFetchEncryptedDataArgs): Promise; + + fetchEncryptedDataByCredentialHash( + args: IFetchEncryptedDataByCredentialHashArgs + ): Promise; +} diff --git a/packages/encrypted-storage/README.md b/packages/encrypted-storage/README.md new file mode 100644 index 00000000..66255ba0 --- /dev/null +++ b/packages/encrypted-storage/README.md @@ -0,0 +1,87 @@ +# Encrypted Storage + +The encrypted storage plugin provides a secure storage for the agent. It is used to store the verifiable credentials that issued when call the `createVerifiableCredential` method. + +## Usage + +### Configuration + +To use the encrypted storage plugin, you need to add the following configuration to the agent.yml. + +Fist, add the `dbConnectionEncrypted` to define the database connection for the encrypted storage. + +```yaml +dbConnectionEncrypted: + $require: typeorm#DataSource + $args: + - type: sqlite + database: + $ref: /constants/databaseFile + synchronize: true + migrationsRun: true + migrations: + $require: '@vckit/encrypted-storage?t=object#migrations' + logging: false + entities: + $require: '@vckit/encrypted-storage?t=object#Entities' +``` + +Second, add the `encryptedStorage` to define the encrypted storage plugin. + +```yaml +# Encrypted Storage Plugin +encryptedStorage: + $require: '@vckit/encrypted-storage#EncryptedStorage' + $args: + - dbConnection: + $ref: /dbConnectionEncrypted +``` + +then require the encrypted storage plugin to the agent. + +```yaml +# Agent +agent: + $require: '@vckit/core#Agent' + $args: + - schemaValidation: false + plugins: + # Plugins + - $ref: /encryptedStorage +``` + +After that, you need to configure the middleware to use the encrypted storage plugin to store the verifiable credentials when issue the verifiable credentials. You can configure the middleware in the `apiRoutes` section of the agent.yml. + +```yaml +# API base path +- - /agent + - $require: '@vckit/remote-server?t=function#apiKeyAuth' + $args: + - apiKey: test123 + # Configure the middleware before the AgentRouter function. The middleware only allow the apis in `apiRoutes` to use the encrypted storage plugin. + - $require: '@vckit/encrypted-storage?t=function#encryptedStoreMiddleware' + $args: + - apiRoutes: + - /createVerifiableCredential + + - $require: '@vckit/remote-server?t=function#AgentRouter' + $args: + - exposedMethods: + $ref: /constants/methods +``` + +Finally, you need to expose the endpoint that can be used to fetch the encrypted verifiable credential. You can configure the endpoint in the `apiRoutes` section of the agent.yml. + +```yaml +# Encrypted storage API +- - /encrypted-storage + - $require: '@vckit/encrypted-storage?t=function#encryptedStoreRouter' +``` + +### To use the encrypted storage plugin + +- To use the encrypted storage plugin, you need to call the `createVerifiableCredential` method with the parameter `save` to store the verifiable credential, then it will trigger the middleware to store the verifiable credential to the encrypted storage. + +- After that, it will response the decrypted key, id of encrypted verifiable credential, and the verifiable credential. + +- Use the decrypted key to decrypt the encrypted verifiable credential that fetched from the endpoint `/encrypted-storage/encrypted-data/:id`. diff --git a/packages/encrypted-storage/api-extractor.json b/packages/encrypted-storage/api-extractor.json new file mode 100644 index 00000000..409d7f16 --- /dev/null +++ b/packages/encrypted-storage/api-extractor.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "apiReport": { + "enabled": true, + "reportFolder": "./api", + "reportTempFolder": "./api" + }, + + "docModel": { + "enabled": true, + "apiJsonFilePath": "./api/.api.json" + }, + + "dtsRollup": { + "enabled": false + }, + "mainEntryPointFilePath": "/build/index.d.ts" +} diff --git a/packages/encrypted-storage/package.json b/packages/encrypted-storage/package.json new file mode 100644 index 00000000..c9ca58a4 --- /dev/null +++ b/packages/encrypted-storage/package.json @@ -0,0 +1,46 @@ +{ + "name": "@vckit/encrypted-storage", + "version": "1.0.0-beta.5", + "description": "To encrypt the data and store to the database.", + "author": "Nam Hoang ", + "homepage": "https://github.com/uncefact/project-vckit#readme", + "main": "build/index.js", + "types": "build/index.d.ts", + "exports": { + ".": "./build/index.js", + "./build/plugin.schema.json": "./build/plugin.schema.json" + }, + "scripts": { + "build": "tsc", + "extract-api": "node ../cli/bin/vckit.js dev extract-api" + }, + "license": "Apache-2.0", + "keywords": [], + "type": "module", + "moduleDirectories": [ + "node_modules", + "src" + ], + "files": [ + "build/**/*", + "src/**/*", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/uncefact/project-vckit.git" + }, + "bugs": { + "url": "https://github.com/uncefact/project-vckit/issues" + }, + "dependencies": { + "@govtechsg/oa-encryption": "^1.3.5", + "@vckit/core-types": "workspace:*", + "@veramo/data-store": "^5.2.0", + "@veramo/utils": "^5.2.0", + "express-interceptor": "^1.2.0", + "typeorm": "^0.3.10", + "uuid": "^9.0.0" + } +} diff --git a/packages/encrypted-storage/src/encrypted-storage.ts b/packages/encrypted-storage/src/encrypted-storage.ts new file mode 100644 index 00000000..a77b67ae --- /dev/null +++ b/packages/encrypted-storage/src/encrypted-storage.ts @@ -0,0 +1,82 @@ +import { EncryptedDataStore } from './identifier/encrypted-data-store.js'; +import { + IAgentPlugin, + IEncryptAndStoreDataArgs, + IEncrypteAndStoreDataResult, + IEncryptedStorage, + IFetchEncryptedDataArgs, + IFetchEncryptedDataByCredentialHashArgs, + IFetchEncryptedDataByCredentialHashResult, +} from '@vckit/core-types'; +import schema from '@vckit/core-types/build/plugin.schema.json' assert { type: 'json' }; +import { + IEncryptionResults, + decryptString, + encryptString, + generateEncryptionKey, +} from '@govtechsg/oa-encryption'; +import { OrPromise, computeEntryHash } from '@veramo/utils'; +import { DataSource } from 'typeorm'; + +/** + * @public + */ +export class EncryptedStorage implements IAgentPlugin { + readonly methods: IEncryptedStorage; + readonly schema = schema.IEncryptedStorage; + + private store: EncryptedDataStore; + constructor(options: { dbConnection: OrPromise }) { + this.store = new EncryptedDataStore(options.dbConnection); + this.methods = { + encryptAndStoreData: this.encryptAndStoreData.bind(this), + fetchEncryptedData: this.fetchEncryptedData.bind(this), + fetchEncryptedDataByCredentialHash: + this.fetchEncryptedDataByCredentialHash.bind(this), + }; + } + + async encryptAndStoreData( + args: IEncryptAndStoreDataArgs + ): Promise { + const { data } = args; + console.log('encryptAndStoreData', JSON.stringify(data, null, 2)); + const credentialHash = computeEntryHash(data); + const key = generateEncryptionKey(); + + const encryptedDocument: Omit = encryptString( + JSON.stringify(data), + key + ); + + const { id } = await this.store.saveEncryptedData( + credentialHash, + key, + JSON.stringify(encryptedDocument) + ); + + return { id, key }; + } + + async fetchEncryptedData(args: IFetchEncryptedDataArgs): Promise { + const encryptedData = await this.store.getEncryptedData(args); + if (!encryptedData) { + throw new Error('Data not found'); + } + + return encryptedData; + } + + async fetchEncryptedDataByCredentialHash( + args: IFetchEncryptedDataByCredentialHashArgs + ): Promise { + const result = await this.store.getEncryptedDataByCredentialHash( + args.credentialHash + ); + if (!result) { + throw new Error('Data not found'); + } + + return result; + } +} diff --git a/packages/encrypted-storage/src/encrypted-store-middleware.ts b/packages/encrypted-storage/src/encrypted-store-middleware.ts new file mode 100644 index 00000000..70a7685e --- /dev/null +++ b/packages/encrypted-storage/src/encrypted-store-middleware.ts @@ -0,0 +1,71 @@ +import { IAgent, IEncrypteAndStoreDataResult } from '@vckit/core-types'; +import { NextFunction, Request, Response, Router } from 'express'; +import interceptor from 'express-interceptor'; +import { RequestWithAgent } from './encrypted-store-router.js'; + +/** + * + * @public + */ +export function encryptedStoreMiddleware(args: { + apiRoutes: string[]; +}): Router { + const router = Router(); + + const intercept = interceptor(function ( + req: RequestWithAgent, + res: Response + ) { + return { + isInterceptable: function () { + return true; + }, + intercept: async function (body: string, send: (body: string) => void) { + if (!req.agent) throw Error('Agent not available'); + let updatedBody: string = body; + + if ( + res.statusCode === 200 && + body && + args.apiRoutes.includes(req.path) + ) { + try { + switch (req.path) { + case '/createVerifiableCredential': + updatedBody = await processCreateVerifiableCredentialRequest( + req.agent, + JSON.parse(body) + ); + + break; + default: + break; + } + } catch (e: any) { + throw Error(e.message); + } + } + + send(updatedBody); + }, + }; + }); + + router.use(intercept); + + return router; +} + +async function processCreateVerifiableCredentialRequest( + agent: IAgent, + payload: object +) { + const { id, key }: IEncrypteAndStoreDataResult = await agent.execute( + 'encryptAndStoreData', + { + data: payload, + } + ); + + return JSON.stringify({ id, key, credential: payload }); +} diff --git a/packages/encrypted-storage/src/encrypted-store-router.ts b/packages/encrypted-storage/src/encrypted-store-router.ts new file mode 100644 index 00000000..3e6d040d --- /dev/null +++ b/packages/encrypted-storage/src/encrypted-store-router.ts @@ -0,0 +1,33 @@ +import { IAgent } from '@vckit/core-types'; +import { Request, Response, Router } from 'express'; + +export interface RequestWithAgent extends Request { + agent?: IAgent; +} + +/** + * @public + * + */ +export function encryptedStoreRouter(): Router { + const router = Router(); + + router.get( + '/encrypted-data/:id', + async (req: RequestWithAgent, res: Response) => { + const { id } = req.params; + const { key } = req.query; + const agent = req.agent; + if (!agent) throw Error('Agent not available'); + + try { + const result = await agent.execute('fetchEncryptedData', { id, key }); + res.status(200).json({ document: JSON.parse(result) }); + } catch (e: any) { + res.status(500).json({ error: e.message }); + } + } + ); + + return router; +} diff --git a/packages/encrypted-storage/src/entities/credential-encrypted-data.ts b/packages/encrypted-storage/src/entities/credential-encrypted-data.ts new file mode 100644 index 00000000..b25e146d --- /dev/null +++ b/packages/encrypted-storage/src/entities/credential-encrypted-data.ts @@ -0,0 +1,13 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('credential-encrypted-data') +export class CredentialEncryptedData extends BaseEntity { + @PrimaryColumn() + credentialHash!: string; + + @Column() + encryptedDataId!: string; + + @Column() + decryptedKey!: string; +} diff --git a/packages/encrypted-storage/src/entities/encrypted-data.ts b/packages/encrypted-storage/src/entities/encrypted-data.ts new file mode 100644 index 00000000..d0d7d2dc --- /dev/null +++ b/packages/encrypted-storage/src/entities/encrypted-data.ts @@ -0,0 +1,36 @@ +import { + BaseEntity, + BeforeInsert, + BeforeUpdate, + Column, + Entity, + PrimaryGeneratedColumn, +} from 'typeorm'; + +@Entity('encrypted-data') +export class EncryptedData extends BaseEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column() + data!: string; + + @BeforeInsert() + setSaveDate() { + this.saveDate = new Date(); + this.updateDate = new Date(); + } + + @BeforeUpdate() + setUpdateDate() { + this.updateDate = new Date(); + } + + @Column({ select: false }) + // @ts-ignore + saveDate: Date; + + @Column({ select: false }) + // @ts-ignore + updateDate: Date; +} diff --git a/packages/encrypted-storage/src/identifier/encrypted-data-store.ts b/packages/encrypted-storage/src/identifier/encrypted-data-store.ts new file mode 100644 index 00000000..ab6e61f3 --- /dev/null +++ b/packages/encrypted-storage/src/identifier/encrypted-data-store.ts @@ -0,0 +1,69 @@ +import { OrPromise } from '@veramo/utils'; +import { DataSource } from 'typeorm'; +import { EncryptedData } from '../entities/encrypted-data.js'; +import { getConnectedDb } from '../utils.js'; +import { CredentialEncryptedData } from '../entities/credential-encrypted-data.js'; +import { IFetchEncryptedDataByCredentialHashResult } from '@vckit/core-types'; + +/** + * @public + */ +export class EncryptedDataStore { + constructor(private dbConnection: OrPromise) {} + + async saveEncryptedData( + credentialHash: string, + decryptedKey: string, + data: string + ): Promise { + const encryptedData = new EncryptedData(); + encryptedData.data = data; + + const db = await getConnectedDb(this.dbConnection); + const result = await db.getRepository(EncryptedData).save(encryptedData); + + const credentialEncryptedData = new CredentialEncryptedData(); + credentialEncryptedData.credentialHash = credentialHash; + credentialEncryptedData.encryptedDataId = result.id; + credentialEncryptedData.decryptedKey = decryptedKey; + console.log('credentialEncryptedData', credentialEncryptedData); + + await db + .getRepository(CredentialEncryptedData) + .save(credentialEncryptedData); + + return result; + } + + async getEncryptedData(args: { id?: string }): Promise { + const db = await getConnectedDb(this.dbConnection); + const encryptedData = await db.getRepository(EncryptedData).findOneBy(args); + return encryptedData?.data; + } + + async getEncryptedDataByCredentialHash( + credentialHash: string + ): Promise { + const db = await getConnectedDb(this.dbConnection); + const credentialEncryptedData = await db + .getRepository(CredentialEncryptedData) + .findOneBy({ credentialHash }); + if (!credentialEncryptedData) { + return undefined; + } + + const encryptedData = await this.getEncryptedData({ + id: credentialEncryptedData.encryptedDataId, + }); + + if (!encryptedData) { + return undefined; + } + + return { + encryptedData, + encryptedDataId: credentialEncryptedData.encryptedDataId, + decryptedKey: credentialEncryptedData.decryptedKey, + }; + } +} diff --git a/packages/encrypted-storage/src/index.ts b/packages/encrypted-storage/src/index.ts new file mode 100644 index 00000000..e8d222e3 --- /dev/null +++ b/packages/encrypted-storage/src/index.ts @@ -0,0 +1,10 @@ +import { CredentialEncryptedData } from './entities/credential-encrypted-data.js'; +import { EncryptedData } from './entities/encrypted-data.js'; + +export { EncryptedDataStore } from './identifier/encrypted-data-store.js'; +export { EncryptedStorage } from './encrypted-storage.js'; +export { migrations } from './migrations/index.js'; +export { encryptedStoreMiddleware } from './encrypted-store-middleware.js'; +export { encryptedStoreRouter } from './encrypted-store-router.js'; + +export const Entities = [EncryptedData, CredentialEncryptedData]; diff --git a/packages/encrypted-storage/src/migrations/1.createDatabase.ts b/packages/encrypted-storage/src/migrations/1.createDatabase.ts new file mode 100644 index 00000000..67a7a994 --- /dev/null +++ b/packages/encrypted-storage/src/migrations/1.createDatabase.ts @@ -0,0 +1,80 @@ +import { + MigrationInterface, + QueryRunner, + Table, + TableForeignKey, +} from 'typeorm'; +import { migrationGetTableName } from './migration-functions.js'; + +/** + * Create the database layout for Veramo 3.0 + * + * @public + */ +export class CreateDatabase1688974564000 implements MigrationInterface { + name = 'CreateDatabase1688974564000'; // Used in case this class gets minified, which would change the classname + + async up(queryRunner: QueryRunner): Promise { + const dateTimeType: string = queryRunner.connection.driver.mappedDataTypes + .createDate as string; + + await queryRunner.createTable( + new Table({ + name: migrationGetTableName(queryRunner, 'encrypted-data'), + columns: [ + { name: 'id', type: 'varchar', isPrimary: true }, + { name: 'data', type: 'varchar', isNullable: false }, + { name: 'saveDate', type: dateTimeType }, + { name: 'updateDate', type: dateTimeType }, + ], + }), + true + ); + + await queryRunner.createTable( + new Table({ + name: migrationGetTableName(queryRunner, 'credential-encrypted-data'), + columns: [ + { + name: 'credentialHash', + type: 'varchar', + isPrimary: true, + }, + { + name: 'encryptedDataId', + type: 'varchar', + isNullable: false, + isUnique: true, + }, + { + name: 'decryptedKey', + type: 'varchar', + isNullable: false, + }, + ], + foreignKeys: [ + { + columnNames: ['encryptedDataId'], + referencedColumnNames: ['id'], + referencedTableName: migrationGetTableName( + queryRunner, + 'encrypted-data' + ), + }, + { + columnNames: ['credentialHash'], + referencedColumnNames: ['hash'], + referencedTableName: migrationGetTableName( + queryRunner, + 'credential' + ), + }, + ], + }) + ); + } + + async down(queryRunner: QueryRunner): Promise { + throw new Error('illegal_operation: cannot roll back initial migration'); + } +} diff --git a/packages/encrypted-storage/src/migrations/index.ts b/packages/encrypted-storage/src/migrations/index.ts new file mode 100644 index 00000000..6abcdfa6 --- /dev/null +++ b/packages/encrypted-storage/src/migrations/index.ts @@ -0,0 +1,18 @@ +import { CreateDatabase1688974564000 } from './1.createDatabase.js'; + +/** + * Allow others to use shared migration functions if they extend Veramo + * + * @public + */ +export * from './migration-functions.js'; + +/** + * The migrations array that SHOULD be used when initializing a TypeORM database connection. + * + * These ensure the correct creation of tables and the proper migrations of data when tables change between versions. + * + * @public + */ + +export const migrations = [CreateDatabase1688974564000]; diff --git a/packages/encrypted-storage/src/migrations/migration-functions.ts b/packages/encrypted-storage/src/migrations/migration-functions.ts new file mode 100644 index 00000000..22501660 --- /dev/null +++ b/packages/encrypted-storage/src/migrations/migration-functions.ts @@ -0,0 +1,50 @@ +import { QueryRunner, Table } from 'typeorm' + +/** + * Get an existing table by name. Checks against givenTableName first, and tableName next. Throws an error if not found + * + * @param queryRunner The query runner object to use for querying + * @param givenName The given name of the table to search for + * @param strictClassName Whether the table name should strictly match the given name + * + * @public + */ +export function migrationGetExistingTableByName(queryRunner: QueryRunner, givenName: string, strictClassName?: boolean): Table { + const table = migrationGetTableByNameImpl(queryRunner, givenName, strictClassName) + if (!table) { + throw Error(`Could not find table with name ${givenName}`) + } + return table +} + +/** + * Get an existing table by name. Checks against givenTableName first, and tableName next. Returns undefined if not found + * + * @param queryRunner The query runner object to use for querying + * @param givenName The given name of the table to search for + * @param strictClassName Whether the table name should strictly match the given name + * + * @private + */ +function migrationGetTableByNameImpl(queryRunner: QueryRunner, givenName: string, strictClassName?: boolean): Table | undefined { + let entityMetadata = queryRunner.connection.entityMetadatas.find((meta) => !!strictClassName ? meta.name === givenName : meta.givenTableName === givenName) + if (!entityMetadata && !strictClassName) { + // We are doing this separately as we don't want the above filter to use an or expression potentially matching first on tableName instead of givenTableName + entityMetadata = queryRunner.connection.entityMetadatas.find((meta) => meta.tableName === givenName) + } + + return entityMetadata ? Table.create(entityMetadata, queryRunner.connection.driver) : undefined +} + +/** + * Get a table name. Checks against givenTableName first, and tableName next from existing tables. Then returns the name. If not found returns the givenName argument + * + * @param queryRunner The query runner object to use for querying + * @param givenName The given name of the table to search for + * @param strictClassName Whether the table name should strictly match the given name + * @public + */ +export function migrationGetTableName(queryRunner: QueryRunner, givenName: string, strictClassName?: boolean): string { + const table = migrationGetTableByNameImpl(queryRunner, givenName, strictClassName) + return !!table ? table.name : givenName +} diff --git a/packages/encrypted-storage/src/module-types/express-interceptor/index.d.ts b/packages/encrypted-storage/src/module-types/express-interceptor/index.d.ts new file mode 100644 index 00000000..2b551564 --- /dev/null +++ b/packages/encrypted-storage/src/module-types/express-interceptor/index.d.ts @@ -0,0 +1 @@ +declare module 'express-interceptor'; diff --git a/packages/encrypted-storage/src/utils.ts b/packages/encrypted-storage/src/utils.ts new file mode 100644 index 00000000..21b92620 --- /dev/null +++ b/packages/encrypted-storage/src/utils.ts @@ -0,0 +1,19 @@ +import { DataSource } from 'typeorm'; +import { OrPromise } from '@veramo/utils'; + +/** + * Ensures that the provided DataSource is connected. + * + * @param dbConnection - a TypeORM DataSource or a Promise that resolves to a DataSource + */ +export async function getConnectedDb( + dbConnection: OrPromise +): Promise { + if (dbConnection instanceof Promise) { + return await dbConnection; + } else if (!dbConnection.isInitialized) { + return await (dbConnection).initialize(); + } else { + return dbConnection; + } +} diff --git a/packages/encrypted-storage/tsconfig.json b/packages/encrypted-storage/tsconfig.json new file mode 100644 index 00000000..dde6c65b --- /dev/null +++ b/packages/encrypted-storage/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "declarationDir": "build" + }, + "references": [ + { "path": "../core-types" } + ] +}