Skip to content

Commit

Permalink
issue challenges, check them, increase complexity, and test it
Browse files Browse the repository at this point in the history
  • Loading branch information
finn-block committed Nov 1, 2023
1 parent bc51fe3 commit e83c349
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 14 deletions.
57 changes: 43 additions & 14 deletions src/pow.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,69 @@
import { createHash } from 'crypto';
import type { Request, Response } from 'express';

const outstandingHashes: { [challenge: string]: Date } = {};
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];
}
}
}, 30000);

export async function getChallenge(
_req: Request,
res: Response,
): Promise<void> {
// sign a JWT with an expiration date shortly in the future (1-2 min) and a complexity
// make the complexity go up when get challenge requests increase in frequency

res.status(500).json({ error: 'unimplemented' });
const challenge = generateChallenge();
recentChallenges[challenge] = Date.now();
res.json({
challenge: challenge,
complexity: getComplexity(),
});
}

export async function verifyChallenge(
req: Request,
res: Response,
): Promise<void> {
console.log(req.body);
console.log('verifying challenge:', req.body);
const body: {
challenge: string;
response: string;
} = req.body;

const hash = createHash('sha1');
const hash = createHash('sha256');
hash.update(body.challenge);
hash.update(body.response);

const hex = hash.digest('hex');
const complexity = Object.keys(outstandingHashes).length;
for (let i = 0; i < complexity; i++) {
if (hex[i] != '0') {
res.status(401).json({ success: false });
return;
}
const complexity = getComplexity();
if (!hash.digest('hex').startsWith('0'.repeat(complexity))) {
res.status(401).json({ success: false });
return;
}

res.json({ success: true });
}

const challengeCharacters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generateChallenge(): string {
let challenge = '';
while (challenge.length < 10) {
challenge += challengeCharacters.charAt(
Math.floor(Math.random() * challengeCharacters.length),
);
}
return challenge;
}

function getComplexity(): number {
return Object.keys(recentChallenges).length;
}
105 changes: 105 additions & 0 deletions tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@tbd54566975/dwn-sdk-js';

import { expect } from 'chai';
import { createHash } from 'crypto';
import type { Server } from 'http';
import fetch from 'node-fetch';
import { webcrypto } from 'node:crypto';
Expand Down Expand Up @@ -500,4 +501,108 @@ describe('http api', function () {
expect(response.status).to.equal(404);
});
});

describe('/register', function () {
it('returns a register challenge', async function () {
const response = await fetch('http://localhost:3000/register');
expect(response.status).to.equal(200);
const body = (await response.json()) as {
challenge: string;
complexity: number;
};
expect(body.challenge.length).to.equal(10);
expect(body.complexity).to.equal(1);
});

it('accepts a correct registration challenge', async function () {
const challengeResponse = await fetch('http://localhost:3000/register');
expect(challengeResponse.status).to.equal(200);
const body = (await challengeResponse.json()) as {
challenge: string;
complexity: number;
};
expect(body.challenge.length).to.equal(10);
expect(body.complexity).to.equal(2);

// solve the challenge
let response = '';
while (!checkNonce(body.challenge, response, body.complexity)) {
response = generateNonce(5);
}

const submitResponse = await fetch('http://localhost:3000/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge: body.challenge,
response: response,
}),
});

expect(submitResponse.status).to.equal(200);
});

it('increase complexity as more challenges are issued', async function () {
const challengeResponse = await fetch('http://localhost:3000/register');
expect(challengeResponse.status).to.equal(200);
const body = (await challengeResponse.json()) as {
challenge: string;
complexity: number;
};
expect(body.challenge.length).to.equal(10);
expect(body.complexity).to.equal(3);
});

it('rejects an invalid challenge', async function () {
const challengeResponse = await fetch('http://localhost:3000/register');
expect(challengeResponse.status).to.equal(200);
const body = (await challengeResponse.json()) as {
challenge: string;
complexity: number;
};
expect(body.challenge.length).to.equal(10);

// solve the challenge
let response = generateNonce(5);
while (checkNonce(body.challenge, response, body.complexity)) {
response = generateNonce(5);
}

const submitResponse = await fetch('http://localhost:3000/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
challenge: body.challenge,
response: response,
}),
});

expect(submitResponse.status).to.equal(401);
});
});
});

const nonceChars =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

function generateNonce(size: number): string {
let challenge = '';
while (challenge.length < size) {
challenge += nonceChars.charAt(
Math.floor(Math.random() * nonceChars.length),
);
}
return challenge;
}

function checkNonce(
challenge: string,
nonce: string,
complexity: number,
): boolean {
const hash = createHash('sha256');
hash.update(challenge);
hash.update(nonce);

return hash.digest('hex').startsWith('0'.repeat(complexity));
}

0 comments on commit e83c349

Please sign in to comment.