Skip to content

Commit

Permalink
track pow-authorized DIDs in a database table
Browse files Browse the repository at this point in the history
  • Loading branch information
finn-block committed Nov 7, 2023
1 parent e83c349 commit da05193
Show file tree
Hide file tree
Showing 8 changed files with 110 additions and 46 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
2 changes: 1 addition & 1 deletion src/dwn-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
});

Expand Down
21 changes: 17 additions & 4 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,29 @@ 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) {
this.#api = express();
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();
}
Expand Down Expand Up @@ -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<http.Server> {
if (this.#pow) {
await this.#pow.initialize();
}
this.#listen(port, callback);
return this.#server;
}
Expand Down
114 changes: 76 additions & 38 deletions src/pow.ts
Original file line number Diff line number Diff line change
@@ -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<PowDatabase>;

constructor(dialect: Dialect) {
this.#db = new Kysely<PowDatabase>({ dialect: dialect });
}
}, 30000);

export async function getChallenge(
_req: Request,
res: Response,
): Promise<void> {
const challenge = generateChallenge();
recentChallenges[challenge] = Date.now();
res.json({
challenge: challenge,
complexity: getComplexity(),
});
}
async initialize(): Promise<void> {
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<void> {
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<void> {
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<void> {
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 =
Expand All @@ -67,3 +98,10 @@ function generateChallenge(): string {
function getComplexity(): number {
return Object.keys(recentChallenges).length;
}
interface AuthorizedTenants {
did: string;
}

interface PowDatabase {
authorizedTenants: AuthorizedTenants;
}
4 changes: 2 additions & 2 deletions src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
6 changes: 5 additions & 1 deletion tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 () {
Expand Down Expand Up @@ -536,6 +538,7 @@ describe('http api', function () {
body: JSON.stringify({
challenge: body.challenge,
response: response,
did: 'aaa',
}),
});

Expand Down Expand Up @@ -574,6 +577,7 @@ describe('http api', function () {
body: JSON.stringify({
challenge: body.challenge,
response: response,
did: 'aaa',
}),
});

Expand Down

0 comments on commit da05193

Please sign in to comment.