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..e5de3553d8 --- /dev/null +++ b/modules/express-kms-api-example/README.md @@ -0,0 +1,45 @@ +# 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. + +# 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 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" + 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 + +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/package.json b/modules/express-kms-api-example/package.json new file mode 100644 index 0000000000..4d023e75bc --- /dev/null +++ b/modules/express-kms-api-example/package.json @@ -0,0 +1,39 @@ +{ + "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", + "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": "", + "license": "ISC", + "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", + "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/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", + "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/api/handlers/GET.ts b/modules/express-kms-api-example/src/api/handlers/GET.ts new file mode 100644 index 0000000000..77454ed604 --- /dev/null +++ b/modules/express-kms-api-example/src/api/handlers/GET.ts @@ -0,0 +1,102 @@ +import { NextFunction, Request, Response } from 'express'; +import db from '../../db'; + +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 + * enum: + * - user + * - backup + * description: The kind of key to retrieve + * 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 + * 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: "user" + * type: "independent" + * 404: + * description: Private key not found + * 500: + * description: Internal server error + */ +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.body.userKeyProvider; + + 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, + ]); + + if (!data) { + res.status(404); + res.send({ message: `Not Found` }); + return; + } + + const { encryptedPrv, kmsKey, type } = data; + + const kmsRes = await userKeyProvider.getKey(kmsKey, encryptedPrv, {}); + if ('code' in kmsRes) { + res.status(500); + res.send({ message: 'Internal server error' }); + return; + } + const { prv } = kmsRes; + + 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 new file mode 100644 index 0000000000..fc1f39efa6 --- /dev/null +++ b/modules/express-kms-api-example/src/api/handlers/POST.ts @@ -0,0 +1,148 @@ +import { NextFunction, Request, Response } from 'express'; +import db from '../../db'; +import { KmsErrorRes, 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): Promise { + + // 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, userKeyProvider, backupKeyProvider } = req.body; + + // check for duplicates + 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 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) { + 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' }); + return; + } + kmsKey = kmsRes.kmsKey; + } + + // send to kms + 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 private key in KMS' }); + return; + } + + // insert into database + // TODO: better catching + 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 }); + next(); +} diff --git a/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts b/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts new file mode 100644 index 0000000000..49fd6c490a --- /dev/null +++ b/modules/express-kms-api-example/src/api/schemas/postKeySchema.ts @@ -0,0 +1,12 @@ +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), + userKeyProvider: z.any(), // TODO: move this away from schema + backupKeyProvider: z.any(), +}); diff --git a/modules/express-kms-api-example/src/api/schemas/types.ts b/modules/express-kms-api-example/src/api/schemas/types.ts new file mode 100644 index 0000000000..5f790ad08a --- /dev/null +++ b/modules/express-kms-api-example/src/api/schemas/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/app.ts b/modules/express-kms-api-example/src/app.ts new file mode 100644 index 0000000000..6294434d65 --- /dev/null +++ b/modules/express-kms-api-example/src/app.ts @@ -0,0 +1,36 @@ +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'; +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'; +dotenv.config({ path: path.resolve(__dirname, '../.env') }); + +const app = express(); +const PORT = '3000'; + +const swaggerSpec = swaggerJSDoc(swaggerOptions); +// -- MIDDLEWARES -- +app.use(bodyParser.json()); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +app.use(checkApiKeyMiddleware); +app.use(keyProviderMiddleware); + +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, () => { + 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 new file mode 100644 index 0000000000..ae6d09c324 --- /dev/null +++ b/modules/express-kms-api-example/src/db.ts @@ -0,0 +1,71 @@ +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); +} + +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, fetchAll, run, setup, fetchOne +}; 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/src/middlewares/keyProvider.ts b/modules/express-kms-api-example/src/middlewares/keyProvider.ts new file mode 100644 index 0000000000..2c9f65958d --- /dev/null +++ b/modules/express-kms-api-example/src/middlewares/keyProvider.ts @@ -0,0 +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: Request, res: Response, next: NextFunction) { + if (!userKeyProvider) userKeyProvider = await instantiateProviderForKeySource('user'); + if (!backupKeyProvider) backupKeyProvider = await instantiateProviderForKeySource('backup'); + + 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 new file mode 100644 index 0000000000..fe00a96ddd --- /dev/null +++ b/modules/express-kms-api-example/src/providers/aws/aws-kms.ts @@ -0,0 +1,102 @@ +import * as awskms from '@aws-sdk/client-kms'; +import { + CreateKmsKeyKmsRes, + GetKeyKmsRes, + KmsErrorRes, + 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({ + 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 }; + } + 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 }; + } + } + + 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); + 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); + } + } + + async getKey(kmsKey: string, keyId: string, options: any): Promise { + const input: awskms.DecryptRequest = { + CiphertextBlob: Buffer.from(keyId), + KeyId: kmsKey, + }; + const command = new awskms.DecryptCommand(input); + + let res; + try { + res = await this.kms.send(command); + if (res.Plaintext === undefined) throw 1; + } 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/azure/azure-kms.ts b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts new file mode 100644 index 0000000000..700887b283 --- /dev/null +++ b/modules/express-kms-api-example/src/providers/azure/azure-kms.ts @@ -0,0 +1,83 @@ +import { ChainedTokenCredential, DefaultAzureCredential } from '@azure/identity'; +import azureKMS, { EncryptParameters } from '@azure/keyvault-keys'; +import { + CreateKmsKeyKmsRes, + GetKeyKmsRes, + KmsErrorRes, + KmsInterface, + PostKeyKmsRes, +} from '../kms-interface/kmsInterface'; + +type AzureKmsProviderConstructorProps = { + keyVaultName: string; + kmsKey: string; + encryptionAlgorithm: EncryptParameters['algorithm']; // RSA1_5 | A256GCM | etc +}; + +export class AzureKmsProvider implements KmsInterface { + providerName = 'azure'; + vaultUrl = ''; + credentials: ChainedTokenCredential = new DefaultAzureCredential(); + encryptionAlgorithm: EncryptParameters['algorithm'] | undefined; // RSA1_5 | A256GCM | etc + keyClient!: azureKMS.KeyClient; + + constructor({ keyVaultName, encryptionAlgorithm }: AzureKmsProviderConstructorProps) { + this.vaultUrl = `https://${keyVaultName}.vault.azure.net`; + this.keyClient = new azureKMS.KeyClient(this.vaultUrl, this.credentials); + this.encryptionAlgorithm = encryptionAlgorithm; + } + + async postKey(kmsKey: string, prv: string, options: any): Promise { + const keyVaultKey = await this.keyClient.getKey(kmsKey); + const clientSDK = new azureKMS.CryptographyClient(keyVaultKey, this.credentials); + + if (this.encryptionAlgorithm === undefined) { + return this.errorHandler(Error('On postKey-Azure: Encryption algorithm is not defined')); + } + + const input = { + algorithm: this.encryptionAlgorithm, + plaintext: Buffer.from(prv), + }; + + try { + const resp = await clientSDK.encrypt(input); + return { + topLevelKeyId: resp.keyID, + encryptedPrv: resp.result.toString(), + metadata: { provider: this.providerName, algorithm: input.algorithm }, + }; + } catch (err) { + return this.errorHandler(err); + } + } + + async getKey(kmsKey: string, keyId: string, options: any): Promise { + // TODO: Azure implementation + const keyVaultKey = await this.keyClient.getKey(kmsKey); + const clientSDK = new azureKMS.CryptographyClient(keyVaultKey, this.credentials); + + try { + 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.result.toString(), + }; + } catch (err) { + return this.errorHandler(err); + } + } + + async createKmsKey(options: any): Promise { + return this.errorHandler(Error('createKmsKey not implemented')); + } + + 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 new file mode 100644 index 0000000000..6f4107514a --- /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 = { + encryptedPrv: string; // TODO: should this be an any? + topLevelKeyId?: any; + metadata?: any; +}; + +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..d140148cf0 --- /dev/null +++ b/modules/express-kms-api-example/src/providers/mock/mock-kms.ts @@ -0,0 +1,36 @@ +import { + CreateKmsKeyKmsRes, + GetKeyKmsRes, + KmsErrorRes, + KmsInterface, + PostKeyKmsRes, +} from '../kms-interface/kmsInterface'; + +export class MockKmsProvider implements KmsInterface { + providerName = 'mock'; + + async postKey(kmsKey: string, prv: string, options: any): Promise { + const mockOutput = { + encryptedPrv: 'none shall pass', + topLevelKeyId: kmsKey, + }; + + 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; + } +} 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..bf1b64e50e --- /dev/null +++ b/modules/express-kms-api-example/src/providers/provider-importer.ts @@ -0,0 +1,21 @@ +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']; + 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/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'], +}; 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); +} 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"] +}