From da0519352344544a7cdaf22866f74622ff32fcec Mon Sep 17 00:00:00 2001 From: finn Date: Tue, 7 Nov 2023 13:03:50 -0800 Subject: [PATCH] track pow-authorized DIDs in a database table --- package-lock.json | 1 + package.json | 1 + src/config.ts | 7 +++ src/dwn-server.ts | 2 +- src/http-api.ts | 21 ++++++-- src/pow.ts | 114 +++++++++++++++++++++++++++-------------- src/storage.ts | 4 +- tests/http-api.spec.ts | 6 ++- 8 files changed, 110 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2a43ffd..31b86a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "bytes": "3.1.2", "cors": "2.8.5", "express": "4.18.2", + "kysely": "^0.26.3", "loglevel": "^1.8.1", "loglevel-plugin-prefix": "^0.8.4", "multiformats": "11.0.2", diff --git a/package.json b/package.json index 44a02ee..5f9583b 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "bytes": "3.1.2", "cors": "2.8.5", "express": "4.18.2", + "kysely": "^0.26.3", "loglevel": "^1.8.1", "loglevel-plugin-prefix": "^0.8.4", "multiformats": "11.0.2", diff --git a/src/config.ts b/src/config.ts index 4e52380..c5190c6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -20,6 +20,13 @@ export const config = { eventLog: process.env.DWN_STORAGE_EVENTS || process.env.DWN_STORAGE || 'level://data', + // require POW-based registration for new tenants + powRegistration: process.env.DWN_REGISTRATION_POW == 'true', + tenantRegistrationStore: + process.env.DWN_REGISTRATION_STORE || + process.env.DWN_STORAGE || + 'sqlite://data/dwn.db', + // log level - trace/debug/info/warn/error logLevel: process.env.DWN_SERVER_LOG_LEVEL || 'INFO', }; diff --git a/src/dwn-server.ts b/src/dwn-server.ts index c54c2a3..3ab2a82 100644 --- a/src/dwn-server.ts +++ b/src/dwn-server.ts @@ -51,7 +51,7 @@ export class DwnServer { } this.#httpApi = new HttpApi(this.dwn); - this.#httpApi.start(this.config.port, () => { + await this.#httpApi.start(this.config.port, () => { log.info(`HttpServer listening on port ${this.config.port}`); }); diff --git a/src/http-api.ts b/src/http-api.ts index 57ddd1b..ed18ee2 100644 --- a/src/http-api.ts +++ b/src/http-api.ts @@ -20,13 +20,16 @@ import { JsonRpcErrorCodes, } from './lib/json-rpc.js'; +import { config } from './config.js'; import { jsonRpcApi } from './json-rpc-api.js'; import { requestCounter, responseHistogram } from './metrics.js'; -import { getChallenge, verifyChallenge } from './pow.js'; +import { ProofOfWork } from './pow.js'; +import { getDialectFromURI } from './storage.js'; export class HttpApi { #api: Express; #server: http.Server; + #pow: ProofOfWork | undefined; dwn: Dwn; constructor(dwn: Dwn) { @@ -34,6 +37,12 @@ export class HttpApi { this.#server = http.createServer(this.#api); this.dwn = dwn; + if (config.powRegistration) { + this.#pow = new ProofOfWork( + getDialectFromURI(new URL(config.tenantRegistrationStore)), + ); + } + this.#setupMiddleware(); this.#setupRoutes(); } @@ -184,15 +193,19 @@ export class HttpApi { } }); - this.#api.get('/register', getChallenge); - this.#api.post('/register', verifyChallenge); + if (this.#pow) { + this.#pow.setupRoutes(this.#api); + } } #listen(port: number, callback?: () => void): void { this.#server.listen(port, callback); } - start(port: number, callback?: () => void): http.Server { + async start(port: number, callback?: () => void): Promise { + if (this.#pow) { + await this.#pow.initialize(); + } this.#listen(port, callback); return this.#server; } diff --git a/src/pow.ts b/src/pow.ts index a9bf529..d02b4e5 100644 --- a/src/pow.ts +++ b/src/pow.ts @@ -1,54 +1,85 @@ import { createHash } from 'crypto'; import type { Request, Response } from 'express'; +import type { Express } from 'express'; +import type { Dialect } from 'kysely'; +import { Kysely } from 'kysely'; const recentChallenges: { [challenge: string]: number } = {}; const CHALLENGE_TIMEOUT = 60 * 1000; -setInterval(() => { - for (const challenge of Object.keys(recentChallenges)) { - if ( - recentChallenges[challenge] && - Date.now() - recentChallenges[challenge] > CHALLENGE_TIMEOUT - ) { - console.log('challenge expired:', challenge); - delete recentChallenges[challenge]; - } +export class ProofOfWork { + #db: Kysely; + + constructor(dialect: Dialect) { + this.#db = new Kysely({ dialect: dialect }); } -}, 30000); -export async function getChallenge( - _req: Request, - res: Response, -): Promise { - const challenge = generateChallenge(); - recentChallenges[challenge] = Date.now(); - res.json({ - challenge: challenge, - complexity: getComplexity(), - }); -} + async initialize(): Promise { + setInterval(() => { + for (const challenge of Object.keys(recentChallenges)) { + if ( + recentChallenges[challenge] && + Date.now() - recentChallenges[challenge] > CHALLENGE_TIMEOUT + ) { + delete recentChallenges[challenge]; + } + } + }, CHALLENGE_TIMEOUT / 4); -export async function verifyChallenge( - req: Request, - res: Response, -): Promise { - console.log('verifying challenge:', req.body); - const body: { - challenge: string; - response: string; - } = req.body; + await this.#db.schema + .createTable('authorizedTenants') + .ifNotExists() + .addColumn('did', 'text', (column) => column.primaryKey()) + .execute(); + } - const hash = createHash('sha256'); - hash.update(body.challenge); - hash.update(body.response); + setupRoutes(server: Express): void { + server.get('/register', (req: Request, res: Response) => + this.getChallenge(req, res), + ); + server.post('/register', (req: Request, res: Response) => + this.verifyChallenge(req, res), + ); + } - const complexity = getComplexity(); - if (!hash.digest('hex').startsWith('0'.repeat(complexity))) { - res.status(401).json({ success: false }); - return; + private async getChallenge(_req: Request, res: Response): Promise { + const challenge = generateChallenge(); + recentChallenges[challenge] = Date.now(); + res.json({ + challenge: challenge, + complexity: getComplexity(), + }); } - res.json({ success: true }); + private async verifyChallenge(req: Request, res: Response): Promise { + const body: { + did: string; + challenge: string; + response: string; + } = req.body; + + const hash = createHash('sha256'); + hash.update(body.challenge); + hash.update(body.response); + + const complexity = getComplexity(); + if (!hash.digest('hex').startsWith('0'.repeat(complexity))) { + res.status(401).json({ success: false }); + return; + } + + try { + await this.#db + .insertInto('authorizedTenants') + .values({ did: body.did }) + .executeTakeFirst(); + } catch (e) { + console.log('error inserting did', e); + res.status(500).json({ success: false }); + return; + } + res.json({ success: true }); + } } const challengeCharacters = @@ -67,3 +98,10 @@ function generateChallenge(): string { function getComplexity(): number { return Object.keys(recentChallenges).length; } +interface AuthorizedTenants { + did: string; +} + +interface PowDatabase { + authorizedTenants: AuthorizedTenants; +} diff --git a/src/storage.ts b/src/storage.ts index 6719bd6..0bcac2c 100644 --- a/src/storage.ts +++ b/src/storage.ts @@ -113,14 +113,14 @@ function getStore(storeString: string, storeType: EStoreType): StoreType { case BackendTypes.SQLITE: case BackendTypes.MYSQL: case BackendTypes.POSTGRES: - return getDBStore(getDBFromURI(storeURI), storeType); + return getDBStore(getDialectFromURI(storeURI), storeType); default: throw invalidStorageSchemeMessage(storeURI.protocol); } } -function getDBFromURI(u: URL): Dialect { +export function getDialectFromURI(u: URL): Dialect { switch (u.protocol.slice(0, -1)) { case BackendTypes.SQLITE: return new SqliteDialect({ diff --git a/tests/http-api.spec.ts b/tests/http-api.spec.ts index f28b231..1ff828d 100644 --- a/tests/http-api.spec.ts +++ b/tests/http-api.spec.ts @@ -14,6 +14,7 @@ import { webcrypto } from 'node:crypto'; import request from 'supertest'; import { v4 as uuidv4 } from 'uuid'; +import { config } from '../src/config.js'; import { HttpApi } from '../src/http-api.js'; import type { JsonRpcErrorResponse, @@ -41,11 +42,12 @@ describe('http api', function () { let server: Server; before(async function () { + config.powRegistration = true; httpApi = new HttpApi(dwn); }); beforeEach(async function () { - server = httpApi.start(3000); + server = await httpApi.start(3000); }); afterEach(async function () { @@ -536,6 +538,7 @@ describe('http api', function () { body: JSON.stringify({ challenge: body.challenge, response: response, + did: 'aaa', }), }); @@ -574,6 +577,7 @@ describe('http api', function () { body: JSON.stringify({ challenge: body.challenge, response: response, + did: 'aaa', }), });