From 5d8bc236e670389da6cfd860cc3f43306e2d30d5 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Tue, 13 May 2025 15:00:09 -0300 Subject: [PATCH 01/13] feat: new kms api example module branch TICKET: WP-4379 --- .gitignore | 1 + modules/express-kms-api-example/README.md | 5 + modules/express-kms-api-example/package.json | 25 ++++ .../scripts/create-tables.sql | 13 ++ modules/express-kms-api-example/src/app.ts | 23 ++++ .../src/controllers/key/GET.ts | 20 +++ .../src/controllers/key/POST.ts | 48 ++++++++ .../src/controllers/key/schemas.ts | 10 ++ .../src/controllers/key/types.ts | 8 ++ modules/express-kms-api-example/src/db.ts | 15 +++ .../src/middlewares/authApiKeys.ts | 23 ++++ modules/express-kms-api-example/tsconfig.json | 116 ++++++++++++++++++ 12 files changed, 307 insertions(+) create mode 100644 modules/express-kms-api-example/README.md create mode 100644 modules/express-kms-api-example/package.json create mode 100644 modules/express-kms-api-example/scripts/create-tables.sql create mode 100644 modules/express-kms-api-example/src/app.ts create mode 100644 modules/express-kms-api-example/src/controllers/key/GET.ts create mode 100644 modules/express-kms-api-example/src/controllers/key/POST.ts create mode 100644 modules/express-kms-api-example/src/controllers/key/schemas.ts create mode 100644 modules/express-kms-api-example/src/controllers/key/types.ts create mode 100644 modules/express-kms-api-example/src/db.ts create mode 100644 modules/express-kms-api-example/src/middlewares/authApiKeys.ts create mode 100644 modules/express-kms-api-example/tsconfig.json diff --git a/.gitignore b/.gitignore index 12bfcf05ac..113683eadd 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ modules/**/dist/ modules/**/pack-scoped/ coverage /.direnv/ +*.db diff --git a/modules/express-kms-api-example/README.md b/modules/express-kms-api-example/README.md new file mode 100644 index 0000000000..40b5d3bf91 --- /dev/null +++ b/modules/express-kms-api-example/README.md @@ -0,0 +1,5 @@ +# KMS API example (REST API) + +Based on TDD specification [On-Prem Wallets(https://docs.google.com/document/d/1ku2agwirV3tHCJF350VF_uaVx73D6vu7yUBaDp-cxL0/edit?tab=t.0#heading=h.165ukudv7ejt)] + +Made with ExpressJS, Typescript and sqlite3. diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json new file mode 100644 index 0000000000..812662fe0b --- /dev/null +++ b/modules/express-kms-api-example/package.json @@ -0,0 +1,25 @@ +{ + "name": "examplekmsapi", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "npx tsx --watch src/app.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "body-parser": "^2.2.0", + "express": "^5.1.0", + "sqlite3": "^5.1.7" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.1", + "@types/express": "^5.0.1", + "@types/node": "^22.15.17", + "tsx": "^4.19.4", + "typescript": "^5.8.3" + } +} diff --git a/modules/express-kms-api-example/scripts/create-tables.sql b/modules/express-kms-api-example/scripts/create-tables.sql new file mode 100644 index 0000000000..39d2d5d79a --- /dev/null +++ b/modules/express-kms-api-example/scripts/create-tables.sql @@ -0,0 +1,13 @@ +-- TODO: delete this comments! questions for Pranav/Mohammad +-- Some fields with VARCHAR(X) not sure how many characters +-- do we need for a coin. + +-- I took the fields from the TDD doc on page 3 +-- Not sure also about the prv and pub key length, should we limit them? +CREATE TABLE PRIVATE_KEYS( + id TEXT PRIMARY KEY, + coin VARCHAR(30) NOT NULL, + source VARCHAR(15) CHECK(source IN ('user', 'backup')) NOT NULL, + type VARCHAR(15) CHECK(type IN ('independent', 'tss')) NOT NULL, + prv TEXT NOT NULL, + pub TEXT NOT NULL); diff --git a/modules/express-kms-api-example/src/app.ts b/modules/express-kms-api-example/src/app.ts new file mode 100644 index 0000000000..ba8d1d3663 --- /dev/null +++ b/modules/express-kms-api-example/src/app.ts @@ -0,0 +1,23 @@ +import bodyParser from 'body-parser'; +import express from 'express'; +import { GET as keyGET } from './controllers/key/GET'; +import { POST as keyPOST } from './controllers/key/POST'; +import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; + +// TODO: move to proper .env +// Add note about the port to the README +// Or hardcode it +const PORT = '3000'; + +const app = express(); +app.use(bodyParser.json()); +app.use(checkApiKeyMiddleware); + +// TODO: create router for keys controllers, +// I added the controllers calls directly here. +app.post('key', keyPOST); +app.get('/key/:pub', keyGET); + +app.listen(PORT, () => { + console.log(`KMS API example listening on port ${PORT}`); +}); diff --git a/modules/express-kms-api-example/src/controllers/key/GET.ts b/modules/express-kms-api-example/src/controllers/key/GET.ts new file mode 100644 index 0000000000..3d334732b9 --- /dev/null +++ b/modules/express-kms-api-example/src/controllers/key/GET.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import db from '../../db'; + +type GetParamsType = { + pub: string; +}; + +export function GET(req: Request, res: Response) { + const { pub } = req.params; + //TODO: what happens if source comes empty? should we return an error? an empty result? + const source = req.query.source; + const data = db.query('SELECT (prv, type) FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [pub, source]); + + // TODO: not sure how to type this + const { prv, type } = data; + + // TODO: i know that we could chain res.status() with .json but what's the preferred way? + res.status(200); + return res.json({ prv, pub, source, type }); +} diff --git a/modules/express-kms-api-example/src/controllers/key/POST.ts b/modules/express-kms-api-example/src/controllers/key/POST.ts new file mode 100644 index 0000000000..8a6ad4c4c6 --- /dev/null +++ b/modules/express-kms-api-example/src/controllers/key/POST.ts @@ -0,0 +1,48 @@ +import { NextFunction, Request, Response } from 'express'; +import db from '../../db'; +import { ZodPostKeySchema } from './schemas'; + +export function POST(req: Request, res: Response, next: NextFunction) { + try { + ZodPostKeySchema.parse(req.body); + } catch (e) { + res.status(400); + res.send({ message: 'Invalid data provided from client' }); + } + + const { prv, pub, coin, source, type } = req.body; + + // TODO: + // check duplicated using the prv/pub key? + // exploitable if we show an error? + const keyObject = db.query('SELECT * from PRIVATE_KEYS WHERE prv = ? AND pub = ?', [prv, pub]); + + // priv + pub should be unique so we raise an error + if (keyObject) { + res.status(409); // It could be also 403 but 409 is specific for dupplicates. + // TODO: I could return the prv and pub in the error but seems exploitable as hell + res.send({ message: `Error: Duplicated Key` }); + return; + } + + // From what i got on the TDD, as store you mean create a new entry right? + // not sure about the note that says "for MPC pub would be the commonKeyChain + // does "pub" comes empty at some point? + try { + // TODO: check how to type the queries??? + const data = db.query('INSERT INTO PRIVATE_KEYS(prv, pub, coin, source, type) values (?, ?, ?, ?, ?)', [ + prv, + pub, + coin, + source, + type, + ]); + const { id: keyId } = data; + res.status(200); + return res.json({ keyId, coin, source, type, pub }); + } catch (e) { + res.status(500); + res.send({ message: 'Internal server error' }); // some unexpected error on DB, needs better login tho + return; + } +} diff --git a/modules/express-kms-api-example/src/controllers/key/schemas.ts b/modules/express-kms-api-example/src/controllers/key/schemas.ts new file mode 100644 index 0000000000..a5e212d672 --- /dev/null +++ b/modules/express-kms-api-example/src/controllers/key/schemas.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; +import { KeySource, KeyType, MultiSigCoins } from './types'; + +export const ZodPostKeySchema = z.object({ + prv: z.string(), // TODO: min/max length? + pub: z.string(), // TODO: min/max length? + coin: z.enum(MultiSigCoins), + source: z.enum(KeySource), + type: z.enum(KeyType), +}); diff --git a/modules/express-kms-api-example/src/controllers/key/types.ts b/modules/express-kms-api-example/src/controllers/key/types.ts new file mode 100644 index 0000000000..5f790ad08a --- /dev/null +++ b/modules/express-kms-api-example/src/controllers/key/types.ts @@ -0,0 +1,8 @@ +// TODO: add the full list of supported coins +export const MultiSigCoins = ['btc', 'heth'] as const; +export const KeySource = ['user', 'backup'] as const; +export const KeyType = ['independent', 'tss'] as const; + +export type MultiSigCoinsType = (typeof MultiSigCoins)[number]; +export type KeySourceType = (typeof KeySource)[number]; +export type KeyTypeType = (typeof KeyType)[number]; diff --git a/modules/express-kms-api-example/src/db.ts b/modules/express-kms-api-example/src/db.ts new file mode 100644 index 0000000000..eb9de42e67 --- /dev/null +++ b/modules/express-kms-api-example/src/db.ts @@ -0,0 +1,15 @@ +import sqlite3 from 'sqlite3'; + +// TODO: better error handling +const db = new sqlite3.Database('database.db', (err) => { + if (err) console.error(err.message); +}); + +// TODO: return type missing, params untyped +function query(sql: string, params: any[]) { + return db.prepare(sql).all(params); +} + +export default { + query, +}; diff --git a/modules/express-kms-api-example/src/middlewares/authApiKeys.ts b/modules/express-kms-api-example/src/middlewares/authApiKeys.ts new file mode 100644 index 0000000000..7508447927 --- /dev/null +++ b/modules/express-kms-api-example/src/middlewares/authApiKeys.ts @@ -0,0 +1,23 @@ +import { NextFunction, Request, Response } from 'express'; +//TODO: move the list of API keys to a safer place like an env file +const API_KEYS_EXTERNALS = ['abc', 'def']; + +export function checkApiKeyMiddleware(req: Request, res: Response, next: NextFunction): void { + const apiKey = req.headers['x-api-key']; + let invalidKey = false; + if (!apiKey) { + invalidKey = true; + } else if (typeof apiKey === 'string') { + invalidKey = !API_KEYS_EXTERNALS.includes(apiKey); + } else if (Array.isArray(apiKey)) { + // Added the forced cast 'as' because for some reason typescript doesn't infers that + // apiKey is an array at this point despite the check on L14 + invalidKey = !(apiKey as string[]).some((key) => API_KEYS_EXTERNALS.includes(key)); + } + + if (invalidKey) { + res.status(401).send({ message: 'Unauthorized' }); + return; + } + next(); +} diff --git a/modules/express-kms-api-example/tsconfig.json b/modules/express-kms-api-example/tsconfig.json new file mode 100644 index 0000000000..3bd3ee68dd --- /dev/null +++ b/modules/express-kms-api-example/tsconfig.json @@ -0,0 +1,116 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "rootDir": "./", + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["**/*.ts"], + "exclude": ["dist"] +} From 059a4f25daaa0180606323a0c80ba4c376d9640e Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Thu, 15 May 2025 12:11:23 -0400 Subject: [PATCH 02/13] feat(kms): added providers to kms Ticket: WP-4379 TICKET: WP-4379 --- modules/express-kms-api-example/package.json | 4 +- .../{controllers/key => api/handlers}/GET.ts | 0 .../{controllers/key => api/handlers}/POST.ts | 15 ++--- .../schemas/postKeySchema.ts} | 0 .../{controllers/key => api/schemas}/types.ts | 0 modules/express-kms-api-example/src/app.ts | 6 +- .../src/providers/aws/aws-kms.ts | 62 +++++++++++++++++++ .../providers/kms-interface/kmsInterface.ts | 26 ++++++++ 8 files changed, 99 insertions(+), 14 deletions(-) rename modules/express-kms-api-example/src/{controllers/key => api/handlers}/GET.ts (100%) rename modules/express-kms-api-example/src/{controllers/key => api/handlers}/POST.ts (72%) rename modules/express-kms-api-example/src/{controllers/key/schemas.ts => api/schemas/postKeySchema.ts} (100%) rename modules/express-kms-api-example/src/{controllers/key => api/schemas}/types.ts (100%) create mode 100644 modules/express-kms-api-example/src/providers/aws/aws-kms.ts create mode 100644 modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json index 812662fe0b..e0e70aa7fe 100644 --- a/modules/express-kms-api-example/package.json +++ b/modules/express-kms-api-example/package.json @@ -11,9 +11,11 @@ "license": "ISC", "description": "", "dependencies": { + "@aws-sdk/client-kms": "^3.808.0", "body-parser": "^2.2.0", "express": "^5.1.0", - "sqlite3": "^5.1.7" + "sqlite3": "^5.1.7", + "zod": "^3.24.4" }, "devDependencies": { "@tsconfig/node22": "^22.0.1", diff --git a/modules/express-kms-api-example/src/controllers/key/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts similarity index 100% rename from modules/express-kms-api-example/src/controllers/key/GET.ts rename to modules/express-kms-api-example/src/api/handlers/GET.ts diff --git a/modules/express-kms-api-example/src/controllers/key/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts similarity index 72% rename from modules/express-kms-api-example/src/controllers/key/POST.ts rename to modules/express-kms-api-example/src/api/handlers/POST.ts index 8a6ad4c4c6..cf5f16569b 100644 --- a/modules/express-kms-api-example/src/controllers/key/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express'; import db from '../../db'; -import { ZodPostKeySchema } from './schemas'; +import { ZodPostKeySchema } from '../schemas/postKeySchema'; export function POST(req: Request, res: Response, next: NextFunction) { try { @@ -12,16 +12,11 @@ export function POST(req: Request, res: Response, next: NextFunction) { const { prv, pub, coin, source, type } = req.body; - // TODO: - // check duplicated using the prv/pub key? - // exploitable if we show an error? - const keyObject = db.query('SELECT * from PRIVATE_KEYS WHERE prv = ? AND pub = ?', [prv, pub]); - - // priv + pub should be unique so we raise an error + // check for duplicates + const keyObject = db.query('SELECT * from PRIVATE_KEYS WHERE pub = ? AND source = ?', [prv, pub]); if (keyObject) { - res.status(409); // It could be also 403 but 409 is specific for dupplicates. - // TODO: I could return the prv and pub in the error but seems exploitable as hell - res.send({ message: `Error: Duplicated Key` }); + res.status(409); + res.send({ message: `Error: Duplicated Key for source: ${source} and pub: ${pub}` }); return; } diff --git a/modules/express-kms-api-example/src/controllers/key/schemas.ts b/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts similarity index 100% rename from modules/express-kms-api-example/src/controllers/key/schemas.ts rename to modules/express-kms-api-example/src/api/schemas/postKeySchema.ts diff --git a/modules/express-kms-api-example/src/controllers/key/types.ts b/modules/express-kms-api-example/src/api/schemas/types.ts similarity index 100% rename from modules/express-kms-api-example/src/controllers/key/types.ts rename to modules/express-kms-api-example/src/api/schemas/types.ts diff --git a/modules/express-kms-api-example/src/app.ts b/modules/express-kms-api-example/src/app.ts index ba8d1d3663..e09b39351e 100644 --- a/modules/express-kms-api-example/src/app.ts +++ b/modules/express-kms-api-example/src/app.ts @@ -1,15 +1,15 @@ import bodyParser from 'body-parser'; import express from 'express'; -import { GET as keyGET } from './controllers/key/GET'; -import { POST as keyPOST } from './controllers/key/POST'; +import { GET as keyGET } from './api/handlers/GET'; +import { POST as keyPOST } from './api/handlers/POST'; import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; // TODO: move to proper .env // Add note about the port to the README // Or hardcode it +const app = express(); const PORT = '3000'; -const app = express(); app.use(bodyParser.json()); app.use(checkApiKeyMiddleware); diff --git a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts new file mode 100644 index 0000000000..10ba90c745 --- /dev/null +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -0,0 +1,62 @@ +import { getKeyKmsRes, kmsErrorRes, kmsInterface, postKeyKmsRes } from "../kms-interface/kmsInterface"; +import * as awskms from '@aws-sdk/client-kms' + +export class awsKmsProvider implements kmsInterface { + providerName: string = "aws"; + kms: awskms.KMSClient = new awskms.KMSClient(); + kmsKey: string = ""; + + errorHandler(err: any) { switch(err.constructor) { + case awskms.DependencyTimeoutException: { + return { message: "KMS server timesout", code: 500 } + } + case awskms.InvalidKeyUsageException: { + return { message: "KMS key is not configured to encrypt data. Check if KMS key is setup properly on aws", code: 400 } + } + case awskms.KeyUnavailableException: { + return { message: "KMS key not avaliable. Check if KMS key is setup properly on aws", code: 500 } + } + case awskms.KMSInternalException: { + return { message: "KMS Internal error occurs", code: 500 } + } + case awskms.NotFoundException: { + return { message: "Resource not found", code: 400 } + } + default: + return { message: "Unknown error occurs", code: 500 } + }} + + postKey(prv: string): Promise { + const input: awskms.EncryptRequest = { + KeyId: this.kmsKey, + Plaintext: Buffer.from(prv) + } + const command = new awskms.EncryptCommand(input); + + return this.kms.send(command) + .then(res => { + if (res.CiphertextBlob === undefined) throw 1; // TODO: more proper handling + return { + bitgoKeyId: res.CiphertextBlob?.toString(), // TODO: should we store this as a string? + topLevelKeyId: res.KeyId, + metadata: res.$metadata + } + }).catch(this.errorHandler); + } + + getKey(keyId: string): Promise { + const input: awskms.DecryptRequest = { + CiphertextBlob: Buffer.from(keyId), + KeyId: this.kmsKey, + } + const command = new awskms.DecryptCommand(input); + + return this.kms.send(command) + .then(res => { + if (res.Plaintext === undefined) throw 1; + return { + prv: res.Plaintext?.toString() + } + }).catch(this.errorHandler); + } +} diff --git a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts new file mode 100644 index 0000000000..3a11c727fc --- /dev/null +++ b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts @@ -0,0 +1,26 @@ +export type kmsErrorRes = { + message: string, + code: number, + data?: any +} + +export type postKeyKmsRes = { + bitgoKeyId: string, // TODO: should this be an any? + topLevelKeyId?: any, + metadata?: any +}; + +export type getKeyKmsRes = { + prv: string, +} + +export abstract class kmsInterface { + abstract providerName: string; + abstract kms: kmsClient; + abstract kmsKey: string; // TODO: should we store the kms key in database? it seems stupid to store it in memory + + constructor() {} + + abstract postKey(prv: string): Promise; + abstract getKey(keyId: any): Promise; +} From d2f3a7d6c8b3c5e9e1a3e2fb227ebf0c8c5a2aec Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Fri, 16 May 2025 10:11:15 -0300 Subject: [PATCH 03/13] feat: added azure provider and ref TICKET: WP-4379 --- modules/express-kms-api-example/package.json | 4 +- .../src/providers/aws/aws-kms.ts | 116 ++++++++++-------- .../src/providers/azure/azure-kms.ts | 81 ++++++++++++ .../providers/kms-interface/kmsInterface.ts | 49 +++++--- 4 files changed, 177 insertions(+), 73 deletions(-) create mode 100644 modules/express-kms-api-example/src/providers/azure/azure-kms.ts diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json index e0e70aa7fe..b819d3c646 100644 --- a/modules/express-kms-api-example/package.json +++ b/modules/express-kms-api-example/package.json @@ -11,7 +11,9 @@ "license": "ISC", "description": "", "dependencies": { - "@aws-sdk/client-kms": "^3.808.0", + "@aws-sdk/client-kms": "^3.810.0", + "@azure/identity": "^4.10.0", + "@azure/keyvault-keys": "^4.9.0", "body-parser": "^2.2.0", "express": "^5.1.0", "sqlite3": "^5.1.7", diff --git a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts index 10ba90c745..3a8bed817a 100644 --- a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -1,62 +1,72 @@ -import { getKeyKmsRes, kmsErrorRes, kmsInterface, postKeyKmsRes } from "../kms-interface/kmsInterface"; -import * as awskms from '@aws-sdk/client-kms' +import * as awskms from '@aws-sdk/client-kms'; +import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes, ProviderNames } from '../kms-interface/kmsInterface'; -export class awsKmsProvider implements kmsInterface { - providerName: string = "aws"; - kms: awskms.KMSClient = new awskms.KMSClient(); - kmsKey: string = ""; +export class AwsKmsProvider implements KmsInterface { + providerName: ProviderNames = 'aws'; + cryptoClient = undefined; + keyClient: awskms.KMSClient = new awskms.KMSClient(); + kmsKey = ''; - errorHandler(err: any) { switch(err.constructor) { - case awskms.DependencyTimeoutException: { - return { message: "KMS server timesout", code: 500 } - } - case awskms.InvalidKeyUsageException: { - return { message: "KMS key is not configured to encrypt data. Check if KMS key is setup properly on aws", code: 400 } - } - case awskms.KeyUnavailableException: { - return { message: "KMS key not avaliable. Check if KMS key is setup properly on aws", code: 500 } - } - case awskms.KMSInternalException: { - return { message: "KMS Internal error occurs", code: 500 } - } - case awskms.NotFoundException: { - return { message: "Resource not found", code: 400 } - } - default: - return { message: "Unknown error occurs", code: 500 } - }} + errorHandler(err: any): KmsErrorRes { + switch (err.constructor) { + case awskms.DependencyTimeoutException: { + return { message: 'KMS server timesout', code: 500 }; + } + case awskms.InvalidKeyUsageException: { + return { + message: 'KMS key is not configured to encrypt data. Check if KMS key is setup properly on aws', + code: 400, + }; + } + case awskms.KeyUnavailableException: { + return { message: 'KMS key not avaliable. Check if KMS key is setup properly on aws', code: 500 }; + } + case awskms.KMSInternalException: { + return { message: 'KMS Internal error occurs', code: 500 }; + } + case awskms.NotFoundException: { + return { message: 'Resource not found', code: 400 }; + } + default: + return { message: 'Unknown error occurs', code: 500 }; + } + } - postKey(prv: string): Promise { - const input: awskms.EncryptRequest = { - KeyId: this.kmsKey, - Plaintext: Buffer.from(prv) - } - const command = new awskms.EncryptCommand(input); + async postKey(prv: string): Promise { + const input: awskms.EncryptRequest = { + KeyId: this.kmsKey, + Plaintext: Buffer.from(prv), + }; + const command = new awskms.EncryptCommand(input); - return this.kms.send(command) - .then(res => { - if (res.CiphertextBlob === undefined) throw 1; // TODO: more proper handling - return { - bitgoKeyId: res.CiphertextBlob?.toString(), // TODO: should we store this as a string? - topLevelKeyId: res.KeyId, - metadata: res.$metadata - } - }).catch(this.errorHandler); + try { + const res = await this.keyClient.send(command); + if (res.CiphertextBlob === undefined) throw 1; // TODO: more proper handling + return { + encryptedPrv: res.CiphertextBlob?.toString(), // TODO: should we store this as a string? + topLevelKeyId: res.KeyId, + metadata: res.$metadata, + }; + } catch (err) { + return this.errorHandler(err); } + } - getKey(keyId: string): Promise { - const input: awskms.DecryptRequest = { - CiphertextBlob: Buffer.from(keyId), - KeyId: this.kmsKey, - } - const command = new awskms.DecryptCommand(input); + async getKey(keyId: string): Promise { + const input: awskms.DecryptRequest = { + CiphertextBlob: Buffer.from(keyId), + KeyId: this.kmsKey, + }; + const command = new awskms.DecryptCommand(input); - return this.kms.send(command) - .then(res => { - if (res.Plaintext === undefined) throw 1; - return { - prv: res.Plaintext?.toString() - } - }).catch(this.errorHandler); + try { + const res = await this.keyClient.send(command); + if (res.Plaintext === undefined) throw 1; + return { + prv: res.Plaintext?.toString(), + }; + } catch (err) { + return this.errorHandler(err); } + } } diff --git a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts new file mode 100644 index 0000000000..6b88c54d76 --- /dev/null +++ b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts @@ -0,0 +1,81 @@ +import * as awskms from '@aws-sdk/client-kms'; +import { ChainedTokenCredential, DefaultAzureCredential } from '@azure/identity'; +import azureKMS, { EncryptParameters } from '@azure/keyvault-keys'; +import { GetKeyKmsRes, KmsEncryptRequest, KmsErrorRes, ProviderNames } from '../kms-interface/kmsInterface'; + +type AzureKmsProviderConstructorProps = { + keyVaultName: string; + kmsKey: string; + encryptionAlgorithm: EncryptParameters['algorithm']; // RSA1_5 | A256GCM | etc +}; + +export class AzureKmsProvider implements KmsInterface { + providerName: ProviderNames = 'azure'; + kmsKey = ''; + vaultUrl = ''; + credentials: ChainedTokenCredential = new DefaultAzureCredential(); + encryptionAlgorithm: EncryptParameters['algorithm'] | undefined; // RSA1_5 | A256GCM | etc + keyClient!: azureKMS.KeyClient; + + constructor({ keyVaultName, kmsKey, encryptionAlgorithm }: AzureKmsProviderConstructorProps) { + this.vaultUrl = `https://${keyVaultName}.vault.azure.net`; + this.keyClient = new azureKMS.KeyClient(this.vaultUrl, this.credentials); + this.kmsKey = kmsKey; + this.encryptionAlgorithm = encryptionAlgorithm; + } + + async postKey(prv: string): Promise { + if (this.providerName !== 'azure') { + throw new Error('On postKey-Azure: Provider name is not azure'); + } + + const keyVaultKey = await this.keyClient.getKey(this.kmsKey); + const clientSDK = new azureKMS.CryptographyClient(keyVaultKey, this.credentials); + + if (this.encryptionAlgorithm === undefined) { + throw new Error('On postKey-Azure: Encryption algorithm is not defined'); + } + + const input: KmsEncryptRequest = { + provider: 'azure', // I need to pass the provider name directly or add a guard + algorithm: this.encryptionAlgorithm, + plaintext: Buffer.from(prv), + }; + + try { + const resp = await clientSDK.encrypt({ algorithm: input.algorithm, plaintext: input.plaintext }); + return { + topLevelKeyId: resp.keyID, + encryptedPrv: resp.result.toString(), + metadata: { provider: this.providerName, algorithm: input.algorithm }, + }; + } catch (err) { + return this.errorHandler(err); + } + } + + getKey(keyId: string): Promise { + // TODO: Azure implementation + const input: awskms.DecryptRequest = { + CiphertextBlob: Buffer.from(keyId), + KeyId: this.kmsKey, + }; + const command = new awskms.DecryptCommand(input); + + try { + const res = await this.keyClient.send(command); + if (res.Plaintext === undefined) throw 1; + return { + prv: res.Plaintext?.toString(), + }; + } catch (err) { + return this.errorHandler(err); + } + } + + errorHandler(err: any): KmsErrorRes { + //TODO: I was looking for some instance that contains the error codes but couldn't find it + // so for now i'm returning a generic error msg + return { message: err.message, code: 500 }; + } +} diff --git a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts index 3a11c727fc..502bf1c642 100644 --- a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts +++ b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts @@ -1,26 +1,37 @@ -export type kmsErrorRes = { - message: string, - code: number, - data?: any -} +import { EncryptParameters } from '@azure/keyvault-keys'; -export type postKeyKmsRes = { - bitgoKeyId: string, // TODO: should this be an any? - topLevelKeyId?: any, - metadata?: any +export type KmsErrorRes = { + message: string; + code: number; + data?: any; }; -export type getKeyKmsRes = { - prv: string, -} +export type PostKeyKmsRes = { + encryptedPrv: string; // TODO: should this be an any? + topLevelKeyId?: any; + metadata?: any; +}; -export abstract class kmsInterface { - abstract providerName: string; - abstract kms: kmsClient; - abstract kmsKey: string; // TODO: should we store the kms key in database? it seems stupid to store it in memory +export type GetKeyKmsRes = { + prv: string; +}; - constructor() {} +export type ProviderNames = 'aws' | 'azure'; - abstract postKey(prv: string): Promise; - abstract getKey(keyId: any): Promise; +export interface KmsInterface { + providerName: ProviderNames; + keyClient: KmsClient; // Max: replacing kms with keyClient as in aws seems that you could do all ops from one class + cryptoClient: CryptoClient | undefined; // but in Azure you need the keyClient to connect and the cryptoClient to operate + kmsKey: string; // TODO: should we store the kms key in database? it seems stupid to store it in memory + postKey(prv: string): Promise; + getKey(keyId: any): Promise; } + +// TODO: maybe move this type to somewhere else to avoid importing the keyvault-keys lib here +export type KmsEncryptRequest = + | { + provider: 'aws'; + KeyId: string; + Plaintext: Buffer; + } + | { provider: 'azure'; algorithm: EncryptParameters['algorithm']; plaintext: Buffer }; From a9977b7b111e60c6f4e2fb73f1e273f14c3edeca Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Fri, 16 May 2025 10:16:01 -0400 Subject: [PATCH 04/13] feat(kms): remove redundant types in provider interface & cleanup Ticket: WP-4379 TICKET: WP-4379 --- .../src/api/handlers/POST.ts | 18 ++++++++++- .../src/providers/aws/aws-kms.ts | 30 +++++++++---------- .../src/providers/azure/azure-kms.ts | 23 +++++++------- .../providers/kms-interface/kmsInterface.ts | 24 +++------------ 4 files changed, 45 insertions(+), 50 deletions(-) diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index cf5f16569b..a59c40a16a 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -1,8 +1,11 @@ import { NextFunction, Request, Response } from 'express'; import db from '../../db'; import { ZodPostKeySchema } from '../schemas/postKeySchema'; +import { KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../../providers/kms-interface/kmsInterface'; -export function POST(req: Request, res: Response, next: NextFunction) { +export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface) { + + // parse request try { ZodPostKeySchema.parse(req.body); } catch (e) { @@ -20,9 +23,22 @@ export function POST(req: Request, res: Response, next: NextFunction) { return; } + // db script to fetch master key from DB if necessary + const kmsKey = ""; + + // send to kms + const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey(kmsKey, prv, {}); + if ('code' in kmsRes) { // TODO: type guard + res.status(kmsRes.code); + res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' }); + return; + } + // From what i got on the TDD, as store you mean create a new entry right? // not sure about the note that says "for MPC pub would be the commonKeyChain // does "pub" comes empty at some point? + + // store into database try { // TODO: check how to type the queries??? const data = db.query('INSERT INTO PRIVATE_KEYS(prv, pub, coin, source, type) values (?, ?, ?, ?, ?)', [ diff --git a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts index 3a8bed817a..f901ab1805 100644 --- a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -1,11 +1,9 @@ import * as awskms from '@aws-sdk/client-kms'; -import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes, ProviderNames } from '../kms-interface/kmsInterface'; +import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; -export class AwsKmsProvider implements KmsInterface { - providerName: ProviderNames = 'aws'; - cryptoClient = undefined; - keyClient: awskms.KMSClient = new awskms.KMSClient(); - kmsKey = ''; +export class awsKmsProvider implements KmsInterface { + providerName: string = "aws"; + kms: awskms.KMSClient = new awskms.KMSClient(); errorHandler(err: any): KmsErrorRes { switch (err.constructor) { @@ -32,15 +30,15 @@ export class AwsKmsProvider implements KmsInterface } } - async postKey(prv: string): Promise { - const input: awskms.EncryptRequest = { - KeyId: this.kmsKey, - Plaintext: Buffer.from(prv), - }; - const command = new awskms.EncryptCommand(input); + async postKey(kmsKey: string, prv: string, options: any): Promise { + const input: awskms.EncryptRequest = { + KeyId: kmsKey, + Plaintext: Buffer.from(prv) + } + const command = new awskms.EncryptCommand(input); try { - const res = await this.keyClient.send(command); + const res = await this.kms.send(command); if (res.CiphertextBlob === undefined) throw 1; // TODO: more proper handling return { encryptedPrv: res.CiphertextBlob?.toString(), // TODO: should we store this as a string? @@ -52,15 +50,15 @@ export class AwsKmsProvider implements KmsInterface } } - async getKey(keyId: string): Promise { + async getKey(kmsKey: string, keyId: string, options: any): Promise { const input: awskms.DecryptRequest = { CiphertextBlob: Buffer.from(keyId), - KeyId: this.kmsKey, + KeyId: kmsKey, }; const command = new awskms.DecryptCommand(input); try { - const res = await this.keyClient.send(command); + const res = await this.kms.send(command); if (res.Plaintext === undefined) throw 1; return { prv: res.Plaintext?.toString(), diff --git a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts index 6b88c54d76..82ed0327b4 100644 --- a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts +++ b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts @@ -1,7 +1,7 @@ import * as awskms from '@aws-sdk/client-kms'; import { ChainedTokenCredential, DefaultAzureCredential } from '@azure/identity'; import azureKMS, { EncryptParameters } from '@azure/keyvault-keys'; -import { GetKeyKmsRes, KmsEncryptRequest, KmsErrorRes, ProviderNames } from '../kms-interface/kmsInterface'; +import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; type AzureKmsProviderConstructorProps = { keyVaultName: string; @@ -9,41 +9,38 @@ type AzureKmsProviderConstructorProps = { encryptionAlgorithm: EncryptParameters['algorithm']; // RSA1_5 | A256GCM | etc }; -export class AzureKmsProvider implements KmsInterface { - providerName: ProviderNames = 'azure'; - kmsKey = ''; +export class AzureKmsProvider implements KmsInterface { + providerName: string = 'azure'; vaultUrl = ''; credentials: ChainedTokenCredential = new DefaultAzureCredential(); encryptionAlgorithm: EncryptParameters['algorithm'] | undefined; // RSA1_5 | A256GCM | etc keyClient!: azureKMS.KeyClient; - constructor({ keyVaultName, kmsKey, encryptionAlgorithm }: AzureKmsProviderConstructorProps) { + constructor({ keyVaultName, encryptionAlgorithm }: AzureKmsProviderConstructorProps) { this.vaultUrl = `https://${keyVaultName}.vault.azure.net`; this.keyClient = new azureKMS.KeyClient(this.vaultUrl, this.credentials); - this.kmsKey = kmsKey; this.encryptionAlgorithm = encryptionAlgorithm; } - async postKey(prv: string): Promise { + async postKey(kmsKey: string, prv: string, options: any): Promise { if (this.providerName !== 'azure') { throw new Error('On postKey-Azure: Provider name is not azure'); } - const keyVaultKey = await this.keyClient.getKey(this.kmsKey); + const keyVaultKey = await this.keyClient.getKey(kmsKey); const clientSDK = new azureKMS.CryptographyClient(keyVaultKey, this.credentials); if (this.encryptionAlgorithm === undefined) { throw new Error('On postKey-Azure: Encryption algorithm is not defined'); } - const input: KmsEncryptRequest = { - provider: 'azure', // I need to pass the provider name directly or add a guard + const input = { algorithm: this.encryptionAlgorithm, plaintext: Buffer.from(prv), }; try { - const resp = await clientSDK.encrypt({ algorithm: input.algorithm, plaintext: input.plaintext }); + const resp = await clientSDK.encrypt(input); return { topLevelKeyId: resp.keyID, encryptedPrv: resp.result.toString(), @@ -54,11 +51,11 @@ export class AzureKmsProvider implements KmsInterface { + getKey(kmsKey:string, keyId: string, options: any): Promise { // TODO: Azure implementation const input: awskms.DecryptRequest = { CiphertextBlob: Buffer.from(keyId), - KeyId: this.kmsKey, + KeyId: kmsKey, }; const command = new awskms.DecryptCommand(input); diff --git a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts index 502bf1c642..0783fec101 100644 --- a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts +++ b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts @@ -1,5 +1,3 @@ -import { EncryptParameters } from '@azure/keyvault-keys'; - export type KmsErrorRes = { message: string; code: number; @@ -16,22 +14,8 @@ export type GetKeyKmsRes = { prv: string; }; -export type ProviderNames = 'aws' | 'azure'; - -export interface KmsInterface { - providerName: ProviderNames; - keyClient: KmsClient; // Max: replacing kms with keyClient as in aws seems that you could do all ops from one class - cryptoClient: CryptoClient | undefined; // but in Azure you need the keyClient to connect and the cryptoClient to operate - kmsKey: string; // TODO: should we store the kms key in database? it seems stupid to store it in memory - postKey(prv: string): Promise; - getKey(keyId: any): Promise; +export interface KmsInterface { + providerName: string; + postKey(kmsKey: string, prv: string, options: any): Promise; + getKey(kmsKey: string, keyId: any, options: any): Promise; } - -// TODO: maybe move this type to somewhere else to avoid importing the keyvault-keys lib here -export type KmsEncryptRequest = - | { - provider: 'aws'; - KeyId: string; - Plaintext: Buffer; - } - | { provider: 'azure'; algorithm: EncryptParameters['algorithm']; plaintext: Buffer }; From 8de198123b528e4970a4a3a006e8053c06515ce2 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Fri, 16 May 2025 12:18:15 -0300 Subject: [PATCH 05/13] fix: missing azure getkey provider method created TICKET: WP-4379 --- .../src/providers/azure/azure-kms.ts | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts index 82ed0327b4..a4c03d1056 100644 --- a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts +++ b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts @@ -1,4 +1,3 @@ -import * as awskms from '@aws-sdk/client-kms'; import { ChainedTokenCredential, DefaultAzureCredential } from '@azure/identity'; import azureKMS, { EncryptParameters } from '@azure/keyvault-keys'; import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; @@ -10,7 +9,7 @@ type AzureKmsProviderConstructorProps = { }; export class AzureKmsProvider implements KmsInterface { - providerName: string = 'azure'; + providerName = 'azure'; vaultUrl = ''; credentials: ChainedTokenCredential = new DefaultAzureCredential(); encryptionAlgorithm: EncryptParameters['algorithm'] | undefined; // RSA1_5 | A256GCM | etc @@ -23,15 +22,11 @@ export class AzureKmsProvider implements KmsInterface { } async postKey(kmsKey: string, prv: string, options: any): Promise { - if (this.providerName !== 'azure') { - throw new Error('On postKey-Azure: Provider name is not azure'); - } - const keyVaultKey = await this.keyClient.getKey(kmsKey); const clientSDK = new azureKMS.CryptographyClient(keyVaultKey, this.credentials); if (this.encryptionAlgorithm === undefined) { - throw new Error('On postKey-Azure: Encryption algorithm is not defined'); + return this.errorHandler(Error('On postKey-Azure: Encryption algorithm is not defined')); } const input = { @@ -51,19 +46,19 @@ export class AzureKmsProvider implements KmsInterface { } } - getKey(kmsKey:string, keyId: string, options: any): Promise { + async getKey(kmsKey: string, keyId: string, options: any): Promise { // TODO: Azure implementation - const input: awskms.DecryptRequest = { - CiphertextBlob: Buffer.from(keyId), - KeyId: kmsKey, - }; - const command = new awskms.DecryptCommand(input); + const keyVaultKey = await this.keyClient.getKey(kmsKey); + const clientSDK = new azureKMS.CryptographyClient(keyVaultKey, this.credentials); try { - const res = await this.keyClient.send(command); - if (res.Plaintext === undefined) throw 1; + const res = await clientSDK.decrypt({ + ciphertext: Buffer.from(keyId), + algorithm: 'RSA1_5', // TODO: algorithm hardcoded for now as other variants requires an iv parameter that i need to investigate + }); + if (res.result === undefined) throw 1; return { - prv: res.Plaintext?.toString(), + prv: res.result.toString(), }; } catch (err) { return this.errorHandler(err); From 0757c423b7717c6ca0c86258478c05687c1a456d Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Tue, 20 May 2025 09:33:58 -0300 Subject: [PATCH 06/13] feat: wip open api support for kms api example get method missing post TICKET: WP-4379 --- modules/express-kms-api-example/package.json | 7 ++- .../src/api/handlers/GET.ts | 55 +++++++++++++++++++ .../src/api/handlers/POST.ts | 8 +-- modules/express-kms-api-example/src/app.ts | 12 +++- .../express-kms-api-example/src/swagger.ts | 18 ++++++ 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 modules/express-kms-api-example/src/swagger.ts diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json index b819d3c646..48fce90c7e 100644 --- a/modules/express-kms-api-example/package.json +++ b/modules/express-kms-api-example/package.json @@ -4,7 +4,8 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "dev": "npx tsx --watch src/app.ts" + "dev": "npx tsx --watch src/app.ts", + "dev-test": "npx tsx --watch src/index-test.ts" }, "keywords": [], "author": "", @@ -17,12 +18,16 @@ "body-parser": "^2.2.0", "express": "^5.1.0", "sqlite3": "^5.1.7", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "zod": "^3.24.4" }, "devDependencies": { "@tsconfig/node22": "^22.0.1", "@types/express": "^5.0.1", "@types/node": "^22.15.17", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", "tsx": "^4.19.4", "typescript": "^5.8.3" } diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 3d334732b9..90e2afd2f2 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -5,6 +5,61 @@ type GetParamsType = { pub: string; }; +/** + * @openapi + * /key/{pub}: + * get: + * summary: Retrieve a private key stored + * tags: + * - key management service + * parameters: + * - in: path + * name: pub + * required: true + * schema: + * type: string + * description: Public key related to the priv key to retrieve + * - in: query + * name: source + * required: true + * schema: + * type: string + * description: TODO - what is the source? + * responses: + * 200: + * description: Private key retrieved + * content: + * application/json: + * schema: + * type: object + * required: + * - prv + * - pub + * - coin + * - source + * - type + * properties: + * prv: + * type: string + * pub: + * type: string + * source: + * type: string + * coin: + * type: string + * type: + * type: string + * example: + * prv: "MIICXAIBAAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAK5gM0c9klGjiunJ+OSH7fX+HQDwykZm20bdEa2fRU4dqT/sRm4Ta1iwAfAgMBAAEC" + * pub: "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAU4dqT/sRm4Ta1iwAfAgMBAAE=" + * source: "TODO replace SOURCE MOCK" + * coin: "sol" + * type: "EdDSA" + * 404: + * description: Private key not found + * 500: + * description: Internal server error + */ export function GET(req: Request, res: Response) { const { pub } = req.params; //TODO: what happens if source comes empty? should we return an error? an empty result? diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index a59c40a16a..bec7ca4a62 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -1,10 +1,9 @@ import { NextFunction, Request, Response } from 'express'; import db from '../../db'; -import { ZodPostKeySchema } from '../schemas/postKeySchema'; import { KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../../providers/kms-interface/kmsInterface'; +import { ZodPostKeySchema } from '../schemas/postKeySchema'; export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface) { - // parse request try { ZodPostKeySchema.parse(req.body); @@ -24,11 +23,12 @@ export async function POST(req: Request, res: Response, next: NextFunction, kms: } // db script to fetch master key from DB if necessary - const kmsKey = ""; + const kmsKey = ''; // send to kms const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey(kmsKey, prv, {}); - if ('code' in kmsRes) { // TODO: type guard + if ('code' in kmsRes) { + // TODO: type guard res.status(kmsRes.code); res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' }); return; diff --git a/modules/express-kms-api-example/src/app.ts b/modules/express-kms-api-example/src/app.ts index e09b39351e..fc2be3746c 100644 --- a/modules/express-kms-api-example/src/app.ts +++ b/modules/express-kms-api-example/src/app.ts @@ -1,8 +1,11 @@ import bodyParser from 'body-parser'; import express from 'express'; +import swaggerJSDoc from 'swagger-jsdoc'; +import swaggerUi from 'swagger-ui-express'; import { GET as keyGET } from './api/handlers/GET'; import { POST as keyPOST } from './api/handlers/POST'; import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; +import { swaggerOptions } from './swagger'; // TODO: move to proper .env // Add note about the port to the README @@ -10,13 +13,20 @@ import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; const app = express(); const PORT = '3000'; +const swaggerSpec = swaggerJSDoc(swaggerOptions); + app.use(bodyParser.json()); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); app.use(checkApiKeyMiddleware); // TODO: create router for keys controllers, // I added the controllers calls directly here. -app.post('key', keyPOST); +app.post('/key', keyPOST); app.get('/key/:pub', keyGET); +app.get('/openapi.json', (req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.send(swaggerSpec); +}); app.listen(PORT, () => { console.log(`KMS API example listening on port ${PORT}`); diff --git a/modules/express-kms-api-example/src/swagger.ts b/modules/express-kms-api-example/src/swagger.ts new file mode 100644 index 0000000000..52ba5b4919 --- /dev/null +++ b/modules/express-kms-api-example/src/swagger.ts @@ -0,0 +1,18 @@ +import { Options } from 'swagger-jsdoc'; + +export const swaggerOptions: Options = { + definition: { + openapi: '3.0.0', + info: { + title: 'KMS API', + version: '1.0.0', + description: 'API documentation for Key Management Service API example', + }, + servers: [ + { + url: 'http://localhost:3000', + }, + ], + }, + apis: ['./**/*.ts'], +}; From 7f126c047d5bc15118dca2f17e1866a0fcad36e7 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Tue, 20 May 2025 16:53:58 -0300 Subject: [PATCH 07/13] feat: added post method open api spec for key related endpoints TICKET: WP-4379 --- .../src/api/handlers/GET.ts | 18 +++-- .../src/api/handlers/POST.ts | 79 +++++++++++++++++++ 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 90e2afd2f2..4c140aded2 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -24,7 +24,10 @@ type GetParamsType = { * required: true * schema: * type: string - * description: TODO - what is the source? + * enum: + * - user + * - backup + * description: The kind of key to retrieve * responses: * 200: * description: Private key retrieved @@ -45,16 +48,19 @@ type GetParamsType = { * type: string * source: * type: string - * coin: - * type: string + * enum: + * - user + * - backup * type: * type: string + * enum: + * - user + * - backup * example: * prv: "MIICXAIBAAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAK5gM0c9klGjiunJ+OSH7fX+HQDwykZm20bdEa2fRU4dqT/sRm4Ta1iwAfAgMBAAEC" * pub: "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAU4dqT/sRm4Ta1iwAfAgMBAAE=" - * source: "TODO replace SOURCE MOCK" - * coin: "sol" - * type: "EdDSA" + * source: "user" + * type: "independent" * 404: * description: Private key not found * 500: diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index bec7ca4a62..fc9730ab8d 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -3,6 +3,85 @@ import db from '../../db'; import { KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../../providers/kms-interface/kmsInterface'; import { ZodPostKeySchema } from '../schemas/postKeySchema'; +/** + * @openapi + * /key: + * post: + * summary: Store a new private key + * tags: + * - key management service + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - prv + * - pub + * - coin + * - source + * - type + * properties: + * prv: + * type: string + * pub: + * type: string + * coin: + * type: string + * source: + * type: string + * enum: + * - user + * - backup + * type: + * type: string + * enum: + * - independent + * - mpc + * responses: + * 200: + * description: Successfully stored key + * content: + * application/json: + * schema: + * type: object + * required: + * - prv + * - pub + * - coin + * - source + * - type + * properties: + * keyId: + * type: string + * coin: + * type: string + * source: + * type: string + * enum: + * - user + * - backup + * type: + * type: string + * enum: + * - independent + * - mpc + * pub: + * type: string + * example: + * keyId: "MIICXAIBAAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAK5gM0c9klGjiunJ+OSH7fX+HQDwykZm20bdEa2fRU4dqT/sRm4Ta1iwAfAgMBAAEC" + * coin: "sol" + * source: "user" + * type: "tss" + * pub: "MIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgH3D4WKfdvhhj9TSGrI0FxAmdfiyfOphuM/kmLMIMKdahZLE5b8YoPL5oIE5NT+157iyQptb7q7qY9nA1jw86Br79FIsi6hLOuAne+1u4jVyJi4PLFAU4dqT/sRm4Ta1iwAfAgMBAAE=" + * 400: + * description: Invalid data + * 409: + * description: Duplicate key + * 500: + * description: Internal server error + */ export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface) { // parse request try { From 2318ad5bf1c6970931e1f3a870c411bea20e5dea Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Wed, 21 May 2025 11:01:09 -0400 Subject: [PATCH 08/13] feat(kms): DB and scripting changed finished up DB related code added npm script for easier compiling TICKET: WP-4379 --- modules/express-kms-api-example/package.json | 5 +- .../src/api/handlers/GET.ts | 28 ++++++-- .../src/api/handlers/POST.ts | 66 ++++++++++--------- modules/express-kms-api-example/src/app.ts | 8 ++- modules/express-kms-api-example/src/db.ts | 58 +++++++++++++++- .../src/providers/aws/aws-kms.ts | 29 ++++++-- .../providers/kms-interface/kmsInterface.ts | 5 ++ .../src/providers/mock/mock-kms.ts | 30 +++++++++ 8 files changed, 183 insertions(+), 46 deletions(-) create mode 100644 modules/express-kms-api-example/src/providers/mock/mock-kms.ts diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json index 48fce90c7e..6b85a4d334 100644 --- a/modules/express-kms-api-example/package.json +++ b/modules/express-kms-api-example/package.json @@ -5,7 +5,10 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "npx tsx --watch src/app.ts", - "dev-test": "npx tsx --watch src/index-test.ts" + "dev-test": "npx tsx --watch src/index-test.ts", + "build": "tsc -p .", + "run": "node dist/src/app.js", + "build-and-run": "tsc -p .; node dist/src/app.js" }, "keywords": [], "author": "", diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 4c140aded2..2731db96ed 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -1,5 +1,6 @@ -import { Request, Response } from 'express'; +import { NextFunction, Request, Response } from 'express'; import db from '../../db'; +import { KmsInterface } from '../../providers/kms-interface/kmsInterface'; type GetParamsType = { pub: string; @@ -66,16 +67,29 @@ type GetParamsType = { * 500: * description: Internal server error */ -export function GET(req: Request, res: Response) { +export async function GET(req: Request, res: Response, next: NextFunction, kms: KmsInterface): Promise { const { pub } = req.params; - //TODO: what happens if source comes empty? should we return an error? an empty result? + + // fetch from DB const source = req.query.source; - const data = db.query('SELECT (prv, type) FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [pub, source]); + const data = await db.fetchOne('SELECT (encryptedPrv, kmsKey, type) FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [pub, source]); + if (!data) { + res.status(404); + res.send({ message: `Not Found` }) + return; + } + + const { encryptedPrv, kmsKey, type } = data; - // TODO: not sure how to type this - const { prv, type } = data; + // fetch prv from kms + const prv = await kms.getKey(kmsKey, encryptedPrv, {}) + .catch((err) => { + res.status(500); + res.send({ message: 'Internal server error' }); + return; // TODO: test this + }); // TODO: i know that we could chain res.status() with .json but what's the preferred way? res.status(200); - return res.json({ prv, pub, source, type }); + res.json({ prv, pub, source, type }); } diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index fc9730ab8d..36e4136531 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -82,7 +82,7 @@ import { ZodPostKeySchema } from '../schemas/postKeySchema'; * 500: * description: Internal server error */ -export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface) { +export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface): Promise { // parse request try { ZodPostKeySchema.parse(req.body); @@ -94,45 +94,51 @@ export async function POST(req: Request, res: Response, next: NextFunction, kms: const { prv, pub, coin, source, type } = req.body; // check for duplicates - const keyObject = db.query('SELECT * from PRIVATE_KEYS WHERE pub = ? AND source = ?', [prv, pub]); - if (keyObject) { + const keyObject = await db.fetchAll('SELECT * from PRIVATE_KEYS WHERE pub = ? AND source = ?', [pub, source]); + if (keyObject.length != 0) { res.status(409); res.send({ message: `Error: Duplicated Key for source: ${source} and pub: ${pub}` }); return; } - // db script to fetch master key from DB if necessary - const kmsKey = ''; - + // db script to fetch kms key from the database, if any exist + let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock']) + if (!kmsKey) { + kmsKey = await kms.createKmsKey({}).then((kmsRes) => { + if ('code' in kmsRes) { + res.status(kmsRes.code); + res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' }); + return; + } + return kmsRes.kmsKey; + }) + } + // send to kms - const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey(kmsKey, prv, {}); - if ('code' in kmsRes) { - // TODO: type guard + const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey("", prv, {}); + if ('code' in kmsRes) { // TODO: type guard res.status(kmsRes.code); res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' }); return; } - // From what i got on the TDD, as store you mean create a new entry right? - // not sure about the note that says "for MPC pub would be the commonKeyChain - // does "pub" comes empty at some point? - - // store into database - try { - // TODO: check how to type the queries??? - const data = db.query('INSERT INTO PRIVATE_KEYS(prv, pub, coin, source, type) values (?, ?, ?, ?, ?)', [ - prv, - pub, - coin, - source, - type, - ]); - const { id: keyId } = data; - res.status(200); - return res.json({ keyId, coin, source, type, pub }); - } catch (e) { + // insert into database + // TODO: better catching + await db.run('INSERT INTO PRIVATE_KEYS values (?, ?, ?, ?, ?, ?, ?)', [ + pub, + source, + kmsRes.encryptedPrv, + kms.providerName, + kmsRes.topLevelKeyId, + coin, + type, + ]).catch((err) => { res.status(500); - res.send({ message: 'Internal server error' }); // some unexpected error on DB, needs better login tho - return; - } + res.send({ message: 'Internal server error' }); + return; // TODO: test this + }); + + res.status(200); + res.json({ coin, source, type, pub }); + next(); } diff --git a/modules/express-kms-api-example/src/app.ts b/modules/express-kms-api-example/src/app.ts index fc2be3746c..298d6c001d 100644 --- a/modules/express-kms-api-example/src/app.ts +++ b/modules/express-kms-api-example/src/app.ts @@ -6,12 +6,15 @@ import { GET as keyGET } from './api/handlers/GET'; import { POST as keyPOST } from './api/handlers/POST'; import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; import { swaggerOptions } from './swagger'; +import { mockKmsProvider } from './providers/mock/mock-kms'; +import db from './db'; // TODO: move to proper .env // Add note about the port to the README // Or hardcode it const app = express(); const PORT = '3000'; +const kmsInterface = new mockKmsProvider(); // TODO: use a config file to determine the provider, perhaps globally? const swaggerSpec = swaggerJSDoc(swaggerOptions); @@ -21,13 +24,14 @@ app.use(checkApiKeyMiddleware); // TODO: create router for keys controllers, // I added the controllers calls directly here. -app.post('/key', keyPOST); -app.get('/key/:pub', keyGET); +app.post('/key', (req, res, next) => keyPOST(req, res, next, kmsInterface)); +app.get('/key/:pub', (req, res, next) => keyGET(req, res, next, kmsInterface)); app.get('/openapi.json', (req, res) => { res.setHeader('Content-Type', 'application/json'); res.send(swaggerSpec); }); app.listen(PORT, () => { + db.setup(); console.log(`KMS API example listening on port ${PORT}`); }); diff --git a/modules/express-kms-api-example/src/db.ts b/modules/express-kms-api-example/src/db.ts index eb9de42e67..ae6d09c324 100644 --- a/modules/express-kms-api-example/src/db.ts +++ b/modules/express-kms-api-example/src/db.ts @@ -10,6 +10,62 @@ function query(sql: string, params: any[]) { return db.prepare(sql).all(params); } +async function fetchAll(sql: string, params: any[]): Promise { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + resolve(rows); + }) + }) +} + +async function fetchOne(sql: string, params: any[]): Promise { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) reject(err); + resolve(row) + }) + }) +} + +async function run(sql: string, params: any[]): Promise { + return new Promise((resolve, reject) => { + db.prepare(sql).all(params, (err, rows) => { + if (err) reject(err); + resolve(); + }) + }) +} + +async function setup() { + + console.log("setting up database...") + + const createKeysTable = ` + CREATE TABLE IF NOT EXISTS PRIVATE_KEYS( + pub TEXT NOT NULL, + source VARCHAR(15) CHECK(source IN ('user', 'backup')) NOT NULL, + encryptedPrv STRING, + provider STRING NOT NULL, + kmsKey STRING NOT NULL, + coin VARCHAR(30) NOT NULL, + type VARCHAR(15) CHECK(type IN ('independent', 'tss')) NOT NULL, + PRIMARY KEY (pub, source) + ); + `; + + return db.run(createKeysTable, (err) => { + if (err) { + console.log("ERROR: cannot creat database"); + + throw { + message: "Cannot create keys table", + code: 500 + } + } + }) +} + export default { - query, + query, fetchAll, run, setup, fetchOne }; diff --git a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts index f901ab1805..f397123bf0 100644 --- a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -1,5 +1,5 @@ import * as awskms from '@aws-sdk/client-kms'; -import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; +import { CreateKmsKeyKmsRes, GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; export class awsKmsProvider implements KmsInterface { providerName: string = "aws"; @@ -57,14 +57,33 @@ export class awsKmsProvider implements KmsInterface { }; const command = new awskms.DecryptCommand(input); + let res; try { - const res = await this.kms.send(command); + res = await this.kms.send(command); if (res.Plaintext === undefined) throw 1; - return { - prv: res.Plaintext?.toString(), - }; } catch (err) { return this.errorHandler(err); } + + return { + prv: res.Plaintext?.toString(), + }; + } + + async createKmsKey(options: any): Promise { + const input: awskms.CreateKeyRequest = {}; + const command = new awskms.CreateKeyCommand(input); + + let res; + try { + res = await this.kms.send(command); + if (res.KeyMetadata === undefined || res.KeyMetadata.Arn === undefined) throw 1; + } catch (err) { + return this.errorHandler (err); + } + + return { + kmsKey: res.KeyMetadata?.Arn as string, + } } } diff --git a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts index 0783fec101..6f4107514a 100644 --- a/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts +++ b/modules/express-kms-api-example/src/providers/kms-interface/kmsInterface.ts @@ -14,8 +14,13 @@ export type GetKeyKmsRes = { prv: string; }; +export type CreateKmsKeyKmsRes = { + kmsKey: string; +} + export interface KmsInterface { providerName: string; postKey(kmsKey: string, prv: string, options: any): Promise; getKey(kmsKey: string, keyId: any, options: any): Promise; + createKmsKey(options: any): Promise; } diff --git a/modules/express-kms-api-example/src/providers/mock/mock-kms.ts b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts new file mode 100644 index 0000000000..c12ecd8cae --- /dev/null +++ b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts @@ -0,0 +1,30 @@ +import { CreateKmsKeyKmsRes, GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from "../kms-interface/kmsInterface"; + +export class mockKmsProvider implements KmsInterface { + providerName: string = "mock"; + + async postKey(kmsKey: string, prv: string, options: any): Promise { + const mockOutput = { + encryptedPrv: "none shall pass", + topLevelKeyId: 0 + } + + return mockOutput; + } + + async getKey(kmsKey: string, keyId: string, options: any): Promise { + const mockOutput = { + prv: "this is not a correct private key" + } + + return mockOutput; + } + + async createKmsKey(options: any): Promise { + const mockOutput = { + kmsKey: "super secure skeleton" + } + + return mockOutput; + } +} From 099210367cd32d759a98d4f425db5ddfa147ba1f Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Thu, 22 May 2025 10:35:50 -0400 Subject: [PATCH 09/13] feat(kms) bug fixes Ticket: WP-4379 TICKET: WP-4379 --- .../src/api/handlers/GET.ts | 12 ++++++------ .../src/api/handlers/POST.ts | 17 ++++++++--------- .../src/providers/azure/azure-kms.ts | 10 +++++++++- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 2731db96ed..7348e92cf8 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -82,12 +82,12 @@ export async function GET(req: Request, res: Response, next: Next const { encryptedPrv, kmsKey, type } = data; // fetch prv from kms - const prv = await kms.getKey(kmsKey, encryptedPrv, {}) - .catch((err) => { - res.status(500); - res.send({ message: 'Internal server error' }); - return; // TODO: test this - }); + const prv = await kms.getKey(kmsKey, encryptedPrv, {}); + if ('code' in prv) { + res.status(500); + res.send({ message: 'Internal server error' }); + return; + } // TODO: i know that we could chain res.status() with .json but what's the preferred way? res.status(200); diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index 36e4136531..19d71f8c35 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -104,19 +104,18 @@ export async function POST(req: Request, res: Response, next: NextFunction, kms: // db script to fetch kms key from the database, if any exist let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock']) if (!kmsKey) { - kmsKey = await kms.createKmsKey({}).then((kmsRes) => { - if ('code' in kmsRes) { - res.status(kmsRes.code); - res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' }); - return; - } - return kmsRes.kmsKey; - }) + const kmsRes = await kms.createKmsKey({}); + if ('code' in kmsRes) { + res.status(kmsRes.code); + res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' }); + return; + } + kmsKey = kmsRes.kmsKey; } // send to kms const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey("", prv, {}); - if ('code' in kmsRes) { // TODO: type guard + if ('code' in kmsRes) { res.status(kmsRes.code); res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' }); return; diff --git a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts index a4c03d1056..d162ddd62c 100644 --- a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts +++ b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts @@ -1,6 +1,6 @@ import { ChainedTokenCredential, DefaultAzureCredential } from '@azure/identity'; import azureKMS, { EncryptParameters } from '@azure/keyvault-keys'; -import { GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; +import { CreateKmsKeyKmsRes, GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; type AzureKmsProviderConstructorProps = { keyVaultName: string; @@ -65,6 +65,14 @@ export class AzureKmsProvider implements KmsInterface { } } + async createKmsKey(options: any): Promise { + // TODO: Azure implementation + return { + message: "Not yet implemented", + code: 500 + } + } + errorHandler(err: any): KmsErrorRes { //TODO: I was looking for some instance that contains the error codes but couldn't find it // so for now i'm returning a generic error msg From b3d1c1eecd547b8ace682300b579dd1005bf274e Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Thu, 22 May 2025 11:12:06 -0400 Subject: [PATCH 10/13] feat(kms) more bug fixes Ticket: WP-4379 TICKET: WP-4379 --- .../express-kms-api-example/src/api/handlers/GET.ts | 10 ++++++---- .../express-kms-api-example/src/api/handlers/POST.ts | 2 +- .../src/providers/mock/mock-kms.ts | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 7348e92cf8..756a047d57 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -71,8 +71,8 @@ export async function GET(req: Request, res: Response, next: Next const { pub } = req.params; // fetch from DB - const source = req.query.source; - const data = await db.fetchOne('SELECT (encryptedPrv, kmsKey, type) FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [pub, source]); + const source = req.body.source; + const data = await db.fetchOne('SELECT encryptedPrv, kmsKey, type FROM PRIVATE_KEYS WHERE pub = ? AND source = ?', [pub, source]); if (!data) { res.status(404); res.send({ message: `Not Found` }) @@ -82,14 +82,16 @@ export async function GET(req: Request, res: Response, next: Next const { encryptedPrv, kmsKey, type } = data; // fetch prv from kms - const prv = await kms.getKey(kmsKey, encryptedPrv, {}); - if ('code' in prv) { + const kmsRes = await kms.getKey(kmsKey, encryptedPrv, {}); + if ('code' in kmsRes) { res.status(500); res.send({ message: 'Internal server error' }); return; } + const { prv } = kmsRes; // TODO: i know that we could chain res.status() with .json but what's the preferred way? res.status(200); res.json({ prv, pub, source, type }); + next(); } diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index 19d71f8c35..b7993e9cf0 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -114,7 +114,7 @@ export async function POST(req: Request, res: Response, next: NextFunction, kms: } // send to kms - const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey("", prv, {}); + const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey(kmsKey, prv, {}); if ('code' in kmsRes) { res.status(kmsRes.code); res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' }); diff --git a/modules/express-kms-api-example/src/providers/mock/mock-kms.ts b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts index c12ecd8cae..b6d3279e09 100644 --- a/modules/express-kms-api-example/src/providers/mock/mock-kms.ts +++ b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts @@ -6,7 +6,7 @@ export class mockKmsProvider implements KmsInterface { async postKey(kmsKey: string, prv: string, options: any): Promise { const mockOutput = { encryptedPrv: "none shall pass", - topLevelKeyId: 0 + topLevelKeyId: kmsKey, } return mockOutput; From 1adf3312a62c7143f827bbc93b65fbb4f5548516 Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Thu, 22 May 2025 12:29:45 -0300 Subject: [PATCH 11/13] feat: added config layer for loading ext provider implementations on runtime TICKET: WP-4379 --- modules/express-kms-api-example/README.md | 37 +++++++++++++ modules/express-kms-api-example/package.json | 1 + .../src/api/handlers/GET.ts | 21 +++++--- .../src/api/handlers/POST.ts | 48 +++++++++-------- modules/express-kms-api-example/src/app.ts | 20 ++++--- .../src/middlewares/keyProvider.ts | 14 +++++ .../src/providers/aws/aws-kms.ts | 30 ++++++----- .../src/providers/azure/azure-kms.ts | 14 ++--- .../src/providers/mock/mock-kms.ts | 52 +++++++++++-------- .../src/providers/provider-importer.ts | 23 ++++++++ .../src/utils/string-utils.ts | 3 ++ 11 files changed, 181 insertions(+), 82 deletions(-) create mode 100644 modules/express-kms-api-example/src/middlewares/keyProvider.ts create mode 100644 modules/express-kms-api-example/src/providers/provider-importer.ts create mode 100644 modules/express-kms-api-example/src/utils/string-utils.ts diff --git a/modules/express-kms-api-example/README.md b/modules/express-kms-api-example/README.md index 40b5d3bf91..afecaea6d9 100644 --- a/modules/express-kms-api-example/README.md +++ b/modules/express-kms-api-example/README.md @@ -3,3 +3,40 @@ Based on TDD specification [On-Prem Wallets(https://docs.google.com/document/d/1ku2agwirV3tHCJF350VF_uaVx73D6vu7yUBaDp-cxL0/edit?tab=t.0#heading=h.165ukudv7ejt)] Made with ExpressJS, Typescript and sqlite3. + +# Installation steps / setup + +1 - Clone the BitGoJS repo and navigate in the terminal until you reach this folder (express-kms-api-example) +2 - Set the current node version with node version manager: $nvm use +3 - Install all the packages: $npm install +4 - Create a .env file inside src/ (src/.env) +5 - Edit the .env file and set this couple of variables: + + USER_PROVIDER_CLASS="aws" + BACKUP_PROVIDER_CLASS="aws" + +you could use different class names between user provider and backup provider or the same, it depends on your particular setup. +Important notes: in the /providers folder, you may be able to add your providers (aws, azure, mock, custom, etc), for adding a new custom provider just follow the structure of the AWS one, starting from the root folder: + +providers/ +| +|-aws/ + | + |--aws-kms.ts + +the "aws" part on "aws-kms" filename is the same as the USER_PROVIDER_CLASS or BACKUP_PROVIDER_CLASS + +In the case of aws, in aws-kms.ts you may find the AwsKmsProvider class declared inside the file. +For your custom provider, suppose that the provider is called "custom", you may need this folder structure: + +providers/ +| +|-custom/ + | + |--custom-kms.ts + +Inside custom-kms.ts ==> class CustomKmsProvider (implements the common interface) + +6 - Implement your custom providers if necessary or use the aws/azure implementation included +7 - Run the project(test): $npm run dev + diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json index 6b85a4d334..5ace4d0b3b 100644 --- a/modules/express-kms-api-example/package.json +++ b/modules/express-kms-api-example/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@tsconfig/node22": "^22.0.1", "@types/express": "^5.0.1", + "@types/express-serve-static-core": "^5.0.6", "@types/node": "^22.15.17", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.8", diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 756a047d57..5d25e9f31e 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -1,6 +1,5 @@ import { NextFunction, Request, Response } from 'express'; import db from '../../db'; -import { KmsInterface } from '../../providers/kms-interface/kmsInterface'; type GetParamsType = { pub: string; @@ -67,22 +66,29 @@ type GetParamsType = { * 500: * description: Internal server error */ -export async function GET(req: Request, res: Response, next: NextFunction, kms: KmsInterface): Promise { +export async function GET(req: Request, res: Response, next: NextFunction): Promise { + //TODO: fix type, it says that the prop doesn't exists + // but in fact it's a incorect type declaration + const userKeyProvider = req.userKeyProvider; + const { pub } = req.params; // fetch from DB - const source = req.body.source; - const data = await db.fetchOne('SELECT encryptedPrv, kmsKey, type FROM PRIVATE_KEYS WHERE pub = ? AND source = ?', [pub, source]); + const source = req.query.source; + const data = await db.fetchOne('SELECT encryptedPrv, kmsKey, type FROM PRIVATE_KEY WHERE pub = ? AND source = ?', [ + pub, + source, + ]); + if (!data) { res.status(404); - res.send({ message: `Not Found` }) + res.send({ message: `Not Found` }); return; } const { encryptedPrv, kmsKey, type } = data; - // fetch prv from kms - const kmsRes = await kms.getKey(kmsKey, encryptedPrv, {}); + const kmsRes = await userKeyProvider.getKey(kmsKey, encryptedPrv, {}); if ('code' in kmsRes) { res.status(500); res.send({ message: 'Internal server error' }); @@ -90,7 +96,6 @@ export async function GET(req: Request, res: Response, next: Next } const { prv } = kmsRes; - // TODO: i know that we could chain res.status() with .json but what's the preferred way? res.status(200); res.json({ prv, pub, source, type }); next(); diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index b7993e9cf0..bb6bbf61ad 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -1,6 +1,6 @@ import { NextFunction, Request, Response } from 'express'; import db from '../../db'; -import { KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../../providers/kms-interface/kmsInterface'; +import { KmsErrorRes, PostKeyKmsRes } from '../../providers/kms-interface/kmsInterface'; import { ZodPostKeySchema } from '../schemas/postKeySchema'; /** @@ -82,7 +82,9 @@ import { ZodPostKeySchema } from '../schemas/postKeySchema'; * 500: * description: Internal server error */ -export async function POST(req: Request, res: Response, next: NextFunction, kms: KmsInterface): Promise { +export async function POST(req: Request, res: Response, next: NextFunction): Promise { + const userKeyProvider = req.userKeyProvider; + // parse request try { ZodPostKeySchema.parse(req.body); @@ -95,16 +97,16 @@ export async function POST(req: Request, res: Response, next: NextFunction, kms: // check for duplicates const keyObject = await db.fetchAll('SELECT * from PRIVATE_KEYS WHERE pub = ? AND source = ?', [pub, source]); - if (keyObject.length != 0) { + if (keyObject.length !== 0) { res.status(409); res.send({ message: `Error: Duplicated Key for source: ${source} and pub: ${pub}` }); return; } // db script to fetch kms key from the database, if any exist - let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock']) + let kmsKey = await db.fetchOne('SELECT kmsKey from PRIVATE_KEYS WHERE provider = ? LIMIT 1', ['mock']); if (!kmsKey) { - const kmsRes = await kms.createKmsKey({}); + const kmsRes = await userKeyProvider.createKmsKey({}); if ('code' in kmsRes) { res.status(kmsRes.code); res.send({ message: 'Internal server error. Failed to create top-level kms key in KMS' }); @@ -112,30 +114,32 @@ export async function POST(req: Request, res: Response, next: NextFunction, kms: } kmsKey = kmsRes.kmsKey; } - + // send to kms - const kmsRes: PostKeyKmsRes | KmsErrorRes = await kms.postKey(kmsKey, prv, {}); - if ('code' in kmsRes) { + const kmsRes: PostKeyKmsRes | KmsErrorRes = await userKeyProvider.postKey(kmsKey, prv, {}); + if ('code' in kmsRes) { res.status(kmsRes.code); - res.send({ message: 'Internal server error. Failed to encrypt prvaite key in KMS' }); + res.send({ message: 'Internal server error. Failed to encrypt private key in KMS' }); return; } // insert into database // TODO: better catching - await db.run('INSERT INTO PRIVATE_KEYS values (?, ?, ?, ?, ?, ?, ?)', [ - pub, - source, - kmsRes.encryptedPrv, - kms.providerName, - kmsRes.topLevelKeyId, - coin, - type, - ]).catch((err) => { - res.status(500); - res.send({ message: 'Internal server error' }); - return; // TODO: test this - }); + await db + .run('INSERT INTO PRIVATE_KEYS values (?, ?, ?, ?, ?, ?, ?)', [ + pub, + source, + kmsRes.encryptedPrv, + userKeyProvider.providerName, + kmsRes.topLevelKeyId, + coin, + type, + ]) + .catch((err) => { + res.status(500); + res.send({ message: 'Internal server error' }); + return; // TODO: test this + }); res.status(200); res.json({ coin, source, type, pub }); diff --git a/modules/express-kms-api-example/src/app.ts b/modules/express-kms-api-example/src/app.ts index 298d6c001d..79a5fca4cd 100644 --- a/modules/express-kms-api-example/src/app.ts +++ b/modules/express-kms-api-example/src/app.ts @@ -1,31 +1,29 @@ import bodyParser from 'body-parser'; +import dotenv from 'dotenv'; import express from 'express'; import swaggerJSDoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; import { GET as keyGET } from './api/handlers/GET'; import { POST as keyPOST } from './api/handlers/POST'; +import db from './db'; import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; +import keyProviderMiddleware from './middlewares/keyProvider'; import { swaggerOptions } from './swagger'; -import { mockKmsProvider } from './providers/mock/mock-kms'; -import db from './db'; +dotenv.config(); -// TODO: move to proper .env -// Add note about the port to the README -// Or hardcode it const app = express(); const PORT = '3000'; -const kmsInterface = new mockKmsProvider(); // TODO: use a config file to determine the provider, perhaps globally? const swaggerSpec = swaggerJSDoc(swaggerOptions); - +// -- MIDDLEWARES -- app.use(bodyParser.json()); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + app.use(checkApiKeyMiddleware); +app.use(keyProviderMiddleware); -// TODO: create router for keys controllers, -// I added the controllers calls directly here. -app.post('/key', (req, res, next) => keyPOST(req, res, next, kmsInterface)); -app.get('/key/:pub', (req, res, next) => keyGET(req, res, next, kmsInterface)); +app.post('/key', keyPOST); +app.get('/key/:pub', keyGET); app.get('/openapi.json', (req, res) => { res.setHeader('Content-Type', 'application/json'); res.send(swaggerSpec); diff --git a/modules/express-kms-api-example/src/middlewares/keyProvider.ts b/modules/express-kms-api-example/src/middlewares/keyProvider.ts new file mode 100644 index 0000000000..98d96b0b70 --- /dev/null +++ b/modules/express-kms-api-example/src/middlewares/keyProvider.ts @@ -0,0 +1,14 @@ +import { instantiateProviderForKeySource } from '../providers/provider-importer'; + +let userKeyProvider: Awaited>; +let backupKeyProvider: Awaited>; + +export default async function keyProviderMiddleware(req, res, next) { + if (!userKeyProvider) userKeyProvider = await instantiateProviderForKeySource('user'); + if (!backupKeyProvider) backupKeyProvider = await instantiateProviderForKeySource('backup'); + + req.userKeyProvider = userKeyProvider; + req.backupKeyProvider = backupKeyProvider; + + next(); +} diff --git a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts index f397123bf0..cbe08754f6 100644 --- a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -1,9 +1,15 @@ import * as awskms from '@aws-sdk/client-kms'; -import { CreateKmsKeyKmsRes, GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; +import { + CreateKmsKeyKmsRes, + GetKeyKmsRes, + KmsErrorRes, + KmsInterface, + PostKeyKmsRes, +} from '../kms-interface/kmsInterface'; -export class awsKmsProvider implements KmsInterface { - providerName: string = "aws"; - kms: awskms.KMSClient = new awskms.KMSClient(); +export class AwsKmsProvider implements KmsInterface { + providerName = 'aws'; + kms: awskms.KMSClient = new awskms.KMSClient(); errorHandler(err: any): KmsErrorRes { switch (err.constructor) { @@ -30,12 +36,12 @@ export class awsKmsProvider implements KmsInterface { } } - async postKey(kmsKey: string, prv: string, options: any): Promise { - const input: awskms.EncryptRequest = { - KeyId: kmsKey, - Plaintext: Buffer.from(prv) - } - const command = new awskms.EncryptCommand(input); + async postKey(kmsKey: string, prv: string, options: any): Promise { + const input: awskms.EncryptRequest = { + KeyId: kmsKey, + Plaintext: Buffer.from(prv), + }; + const command = new awskms.EncryptCommand(input); try { const res = await this.kms.send(command); @@ -79,11 +85,11 @@ export class awsKmsProvider implements KmsInterface { res = await this.kms.send(command); if (res.KeyMetadata === undefined || res.KeyMetadata.Arn === undefined) throw 1; } catch (err) { - return this.errorHandler (err); + return this.errorHandler(err); } return { kmsKey: res.KeyMetadata?.Arn as string, - } + }; } } diff --git a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts index d162ddd62c..700887b283 100644 --- a/modules/express-kms-api-example/src/providers/azure/azure-kms.ts +++ b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts @@ -1,6 +1,12 @@ import { ChainedTokenCredential, DefaultAzureCredential } from '@azure/identity'; import azureKMS, { EncryptParameters } from '@azure/keyvault-keys'; -import { CreateKmsKeyKmsRes, GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from '../kms-interface/kmsInterface'; +import { + CreateKmsKeyKmsRes, + GetKeyKmsRes, + KmsErrorRes, + KmsInterface, + PostKeyKmsRes, +} from '../kms-interface/kmsInterface'; type AzureKmsProviderConstructorProps = { keyVaultName: string; @@ -66,11 +72,7 @@ export class AzureKmsProvider implements KmsInterface { } async createKmsKey(options: any): Promise { - // TODO: Azure implementation - return { - message: "Not yet implemented", - code: 500 - } + return this.errorHandler(Error('createKmsKey not implemented')); } errorHandler(err: any): KmsErrorRes { diff --git a/modules/express-kms-api-example/src/providers/mock/mock-kms.ts b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts index b6d3279e09..d140148cf0 100644 --- a/modules/express-kms-api-example/src/providers/mock/mock-kms.ts +++ b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts @@ -1,30 +1,36 @@ -import { CreateKmsKeyKmsRes, GetKeyKmsRes, KmsErrorRes, KmsInterface, PostKeyKmsRes } from "../kms-interface/kmsInterface"; +import { + CreateKmsKeyKmsRes, + GetKeyKmsRes, + KmsErrorRes, + KmsInterface, + PostKeyKmsRes, +} from '../kms-interface/kmsInterface'; -export class mockKmsProvider implements KmsInterface { - providerName: string = "mock"; - - async postKey(kmsKey: string, prv: string, options: any): Promise { - const mockOutput = { - encryptedPrv: "none shall pass", - topLevelKeyId: kmsKey, - } +export class MockKmsProvider implements KmsInterface { + providerName = 'mock'; - return mockOutput; - } + async postKey(kmsKey: string, prv: string, options: any): Promise { + const mockOutput = { + encryptedPrv: 'none shall pass', + topLevelKeyId: kmsKey, + }; - async getKey(kmsKey: string, keyId: string, options: any): Promise { - const mockOutput = { - prv: "this is not a correct private key" - } + return mockOutput; + } - return mockOutput; - } + async getKey(kmsKey: string, keyId: string, options: any): Promise { + const mockOutput = { + prv: 'this is not a correct private key', + }; - async createKmsKey(options: any): Promise { - const mockOutput = { - kmsKey: "super secure skeleton" - } + return mockOutput; + } - return mockOutput; - } + async createKmsKey(options: any): Promise { + const mockOutput = { + kmsKey: 'super secure skeleton', + }; + + return mockOutput; + } } diff --git a/modules/express-kms-api-example/src/providers/provider-importer.ts b/modules/express-kms-api-example/src/providers/provider-importer.ts new file mode 100644 index 0000000000..d98369d3df --- /dev/null +++ b/modules/express-kms-api-example/src/providers/provider-importer.ts @@ -0,0 +1,23 @@ +import * as dotenv from 'dotenv'; +import { capitalize } from '../utils/string-utils'; + +dotenv.config(); + +export async function instantiateProviderForKeySource( + source: 'user' | 'backup' +): Promise { + // const provider = process.env[source === 'user' ? 'USER_PROVIDER_CLASS' : 'BACKUP_PROVIDER_CLASS']; + //TODO: module resolution error to solve, hardcoded for now + const provider = 'mock'; + if (!provider) throw new Error(`Provider for ${source} is not defined. Only 'user' or 'backup' are allowed.`); + + const modulePath = `./${provider}/${provider}-kms`; + const providerModule = await import(modulePath); + + const className = `${capitalize(provider)}KmsProvider`; + + const ProviderClass = providerModule[className]; + if (!ProviderClass) throw new Error(`Provider class "${className}" not found in ${modulePath}`); + + return new ProviderClass(); +} diff --git a/modules/express-kms-api-example/src/utils/string-utils.ts b/modules/express-kms-api-example/src/utils/string-utils.ts new file mode 100644 index 0000000000..7519095d07 --- /dev/null +++ b/modules/express-kms-api-example/src/utils/string-utils.ts @@ -0,0 +1,3 @@ +export function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} From c9957264d42bf31050cb86ba8bdfbb27a3e67b5e Mon Sep 17 00:00:00 2001 From: mtexeira-simtlix Date: Thu, 22 May 2025 16:05:23 -0300 Subject: [PATCH 12/13] fix: fixed the issue with the env file TICKET: WP-4379 --- modules/express-kms-api-example/README.md | 5 ++++- modules/express-kms-api-example/src/app.ts | 3 ++- .../src/providers/provider-importer.ts | 4 +--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/express-kms-api-example/README.md b/modules/express-kms-api-example/README.md index afecaea6d9..e5de3553d8 100644 --- a/modules/express-kms-api-example/README.md +++ b/modules/express-kms-api-example/README.md @@ -9,7 +9,7 @@ Made with ExpressJS, Typescript and sqlite3. 1 - Clone the BitGoJS repo and navigate in the terminal until you reach this folder (express-kms-api-example) 2 - Set the current node version with node version manager: $nvm use 3 - Install all the packages: $npm install -4 - Create a .env file inside src/ (src/.env) +4 - Create a .env file in the root of the project (at the side of package.json) 5 - Edit the .env file and set this couple of variables: USER_PROVIDER_CLASS="aws" @@ -40,3 +40,6 @@ Inside custom-kms.ts ==> class CustomKmsProvider (implements the common interfac 6 - Implement your custom providers if necessary or use the aws/azure implementation included 7 - Run the project(test): $npm run dev +Extras: + +- If you want to use the mock server, instead of "aws" replace with "mock" on step 5 diff --git a/modules/express-kms-api-example/src/app.ts b/modules/express-kms-api-example/src/app.ts index 79a5fca4cd..6294434d65 100644 --- a/modules/express-kms-api-example/src/app.ts +++ b/modules/express-kms-api-example/src/app.ts @@ -1,6 +1,7 @@ import bodyParser from 'body-parser'; import dotenv from 'dotenv'; import express from 'express'; +import path from 'path'; import swaggerJSDoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; import { GET as keyGET } from './api/handlers/GET'; @@ -9,7 +10,7 @@ import db from './db'; import { checkApiKeyMiddleware } from './middlewares/authApiKeys'; import keyProviderMiddleware from './middlewares/keyProvider'; import { swaggerOptions } from './swagger'; -dotenv.config(); +dotenv.config({ path: path.resolve(__dirname, '../.env') }); const app = express(); const PORT = '3000'; diff --git a/modules/express-kms-api-example/src/providers/provider-importer.ts b/modules/express-kms-api-example/src/providers/provider-importer.ts index d98369d3df..bf1b64e50e 100644 --- a/modules/express-kms-api-example/src/providers/provider-importer.ts +++ b/modules/express-kms-api-example/src/providers/provider-importer.ts @@ -6,9 +6,7 @@ dotenv.config(); export async function instantiateProviderForKeySource( source: 'user' | 'backup' ): Promise { - // const provider = process.env[source === 'user' ? 'USER_PROVIDER_CLASS' : 'BACKUP_PROVIDER_CLASS']; - //TODO: module resolution error to solve, hardcoded for now - const provider = 'mock'; + const provider = process.env[source === 'user' ? 'USER_PROVIDER_CLASS' : 'BACKUP_PROVIDER_CLASS']; if (!provider) throw new Error(`Provider for ${source} is not defined. Only 'user' or 'backup' are allowed.`); const modulePath = `./${provider}/${provider}-kms`; From 74118b87418a9aaa38430f835909fb5e75f282af Mon Sep 17 00:00:00 2001 From: Alex Tse Date: Mon, 26 May 2025 11:03:44 -0400 Subject: [PATCH 13/13] feat(kms) added credentials to AWS access TICKET: WP-4379 --- modules/express-kms-api-example/package.json | 1 + .../express-kms-api-example/src/api/handlers/GET.ts | 2 +- .../express-kms-api-example/src/api/handlers/POST.ts | 5 +++-- .../src/api/schemas/postKeySchema.ts | 2 ++ .../src/middlewares/keyProvider.ts | 7 ++++--- .../src/providers/aws/aws-kms.ts | 11 +++++++++-- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/modules/express-kms-api-example/package.json b/modules/express-kms-api-example/package.json index 5ace4d0b3b..4d023e75bc 100644 --- a/modules/express-kms-api-example/package.json +++ b/modules/express-kms-api-example/package.json @@ -16,6 +16,7 @@ "description": "", "dependencies": { "@aws-sdk/client-kms": "^3.810.0", + "@aws-sdk/credential-providers": "^3.817.0", "@azure/identity": "^4.10.0", "@azure/keyvault-keys": "^4.9.0", "body-parser": "^2.2.0", diff --git a/modules/express-kms-api-example/src/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts index 5d25e9f31e..77454ed604 100644 --- a/modules/express-kms-api-example/src/api/handlers/GET.ts +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -69,7 +69,7 @@ type GetParamsType = { export async function GET(req: Request, res: Response, next: NextFunction): Promise { //TODO: fix type, it says that the prop doesn't exists // but in fact it's a incorect type declaration - const userKeyProvider = req.userKeyProvider; + const userKeyProvider = req.body.userKeyProvider; const { pub } = req.params; diff --git a/modules/express-kms-api-example/src/api/handlers/POST.ts b/modules/express-kms-api-example/src/api/handlers/POST.ts index bb6bbf61ad..fc1f39efa6 100644 --- a/modules/express-kms-api-example/src/api/handlers/POST.ts +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -83,17 +83,18 @@ import { ZodPostKeySchema } from '../schemas/postKeySchema'; * description: Internal server error */ export async function POST(req: Request, res: Response, next: NextFunction): Promise { - const userKeyProvider = req.userKeyProvider; // parse request try { + console.log('POST /key', req.body); ZodPostKeySchema.parse(req.body); } catch (e) { res.status(400); res.send({ message: 'Invalid data provided from client' }); + return; } - const { prv, pub, coin, source, type } = req.body; + const { prv, pub, coin, source, type, userKeyProvider, backupKeyProvider } = req.body; // check for duplicates const keyObject = await db.fetchAll('SELECT * from PRIVATE_KEYS WHERE pub = ? AND source = ?', [pub, source]); diff --git a/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts b/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts index a5e212d672..49fd6c490a 100644 --- a/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts +++ b/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts @@ -7,4 +7,6 @@ export const ZodPostKeySchema = z.object({ coin: z.enum(MultiSigCoins), source: z.enum(KeySource), type: z.enum(KeyType), + userKeyProvider: z.any(), // TODO: move this away from schema + backupKeyProvider: z.any(), }); diff --git a/modules/express-kms-api-example/src/middlewares/keyProvider.ts b/modules/express-kms-api-example/src/middlewares/keyProvider.ts index 98d96b0b70..2c9f65958d 100644 --- a/modules/express-kms-api-example/src/middlewares/keyProvider.ts +++ b/modules/express-kms-api-example/src/middlewares/keyProvider.ts @@ -1,14 +1,15 @@ +import { NextFunction, Request, Response } from 'express'; import { instantiateProviderForKeySource } from '../providers/provider-importer'; let userKeyProvider: Awaited>; let backupKeyProvider: Awaited>; -export default async function keyProviderMiddleware(req, res, next) { +export default async function keyProviderMiddleware(req: Request, res: Response, next: NextFunction) { if (!userKeyProvider) userKeyProvider = await instantiateProviderForKeySource('user'); if (!backupKeyProvider) backupKeyProvider = await instantiateProviderForKeySource('backup'); - req.userKeyProvider = userKeyProvider; - req.backupKeyProvider = backupKeyProvider; + req.body.userKeyProvider = userKeyProvider; + req.body.backupKeyProvider = backupKeyProvider; next(); } diff --git a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts index cbe08754f6..fe00a96ddd 100644 --- a/modules/express-kms-api-example/src/providers/aws/aws-kms.ts +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -6,12 +6,19 @@ import { KmsInterface, PostKeyKmsRes, } from '../kms-interface/kmsInterface'; +import { fromIni } from '@aws-sdk/credential-providers'; export class AwsKmsProvider implements KmsInterface { providerName = 'aws'; - kms: awskms.KMSClient = new awskms.KMSClient(); - + kms: awskms.KMSClient = new awskms.KMSClient({ + region: 'us-east-2', + credentials: fromIni({ + profile: 'saml', + }) + }); + errorHandler(err: any): KmsErrorRes { + console.error('AWS KMS encountered an error\n', err); switch (err.constructor) { case awskms.DependencyTimeoutException: { return { message: 'KMS server timesout', code: 500 };