Skip to content

Commit

Permalink
confirm PoW
Browse files Browse the repository at this point in the history
  • Loading branch information
daroczig committed Feb 11, 2025
1 parent c4776fa commit b0b8e83
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 20 deletions.
45 changes: 39 additions & 6 deletions server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import bootstrap from './src/main.server';
import { REQUEST, RESPONSE } from './src/express.tokens';
import crypto from 'crypto';

const POW_NONCE = process.env['POW_NONCE'] || '';
const POW_SECRET_KEY = process.env['POW_SECRET_KEY'] || '';

// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
Expand Down Expand Up @@ -62,18 +62,51 @@ export function app(): express.Express {
res.status(200).json(stats);
});

// Generate PoW challenge
// generate PoW challenge for the contact form
server.get('/api/generate-pow-challenge', (req, res) => {
const timestamp = Date.now();
const challenge = crypto.randomBytes(16).toString('hex');
const hmac = crypto.createHmac('sha256', POW_NONCE);
const hmac = crypto.createHmac('sha256', POW_SECRET_KEY);
hmac.update(`${challenge}:${timestamp}`);
const signature = hmac.digest('hex');

res.json({ challenge, timestamp, signature });
console.log(JSON.stringify({"event": "generate-pow-challenge", "challenge": challenge, "timestamp": timestamp, "signature": signature}));
res.status(200).json({ challenge, timestamp, signature });
});

// TODO add post handler for contact form
// handle contact form submission
server.post('/api/contact', express.json(), (req, res) => {
console.log(JSON.stringify({"event": "contact", "body": req.body}));

const { powChallenge, powTimestamp, powSignature, powSolution } = req.body;

// verify that the PoW challenge was not tampered with
const hmac = crypto.createHmac('sha256', POW_SECRET_KEY);
hmac.update(`${powChallenge}:${powTimestamp}`);
const expectedSignature = hmac.digest('hex');
if (expectedSignature !== powSignature) {
return res.status(400).json({ error: 'Invalid PoW signature' });
}

// verify that the PoW challenge is not too old
const currentTime = Date.now();
const fiveMinutesInMillis = 5 * 60 * 1000;
if (currentTime - powTimestamp > fiveMinutesInMillis) {
return res.status(400).json({ error: 'PoW timestamp is too old' });
}

// verify the PoW solution
const hash = crypto.createHash('sha256');
hash.update(powChallenge + powSolution);
const powHash = hash.digest('hex');
const difficulty = '0000';
if (!powHash.startsWith(difficulty)) {
return res.status(400).json({ error: 'Invalid PoW solution' });
}

// TODO emal using env variables
res.status(200).json({ status: 'Message sent' });
return;
});

// redirect from www
server.use((req, res, next) => {
Expand Down
56 changes: 42 additions & 14 deletions src/app/pages/contact/contact.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,27 +38,31 @@ export class ContactComponent {
phone: [''],
message: ['', Validators.required],
privacyPolicy: [false, Validators.requiredTrue],
// received from server
powChallenge: [''],
powSolution: [''],
powTimestamp: [0],
powSignature: [''],
// calculated by client
powSolution: [0],
powDuration: [0]
});
}

async fetchChallengeFromServer(): Promise<string> {
async fetchChallengeFromServer(): Promise<{ challenge: string, timestamp: number, signature: string }> {
try {
const response = await fetch('/api/generate-pow-challenge');
const data = await response.json();
// don't care about the random challenge and timestamp, just the signature
return data.signature;
return data;
} catch (error) {
this.toastService.show({ title: 'Failed to fetch PoW challenge. Please try again later!', type: 'error' })
this.toastService.show({ title: 'Failed to fetch PoW challenge. Please try again later!', type: 'error' });
throw error;
}
}

solvePoW(challenge: string, difficulty: number): void {
solvePoW(challenge: string): void {
let solution = 0;
const target = '0'.repeat(difficulty);
// difficulty set to 4
const target = '0'.repeat(4);
const startTime = Date.now();
while (true) {
const hash = crypto.SHA256(challenge + solution).toString();
Expand All @@ -80,19 +84,43 @@ export class ContactComponent {
});
}

async submitFormToServer(): Promise<void> {
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(this.contactForm.value)
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(`Failed to send the message: ${errorData.error || 'Unknown error'}`);
}

this.toastService.show({ title: 'Message sent!', type: 'success' });
} catch (error) {
this.toastService.show({ title: 'Failed to send message. Please try again later!', type: 'error' });
throw error;
}
}

onSubmit(): void {
if (this.contactForm.valid) {
this.isLoading = true;
this.disableAllInputs();
this.fetchChallengeFromServer().then(challenge => {
this.fetchChallengeFromServer().then(({ challenge, timestamp, signature }) => {
this.powChallenge = challenge;
this.contactForm.patchValue({ powChallenge: this.powChallenge });
this.solvePoW(this.powChallenge, 4);

// TODO Handle form submission after solving PoW
console.log('Form submitted', this.contactForm.value);
this.isSubmitted = true;
this.toastService.show({ title: 'Message sent!', type: 'success' });
this.contactForm.patchValue({ powTimestamp: timestamp });
this.contactForm.patchValue({ powSignature: signature });
this.solvePoW(this.powChallenge);
this.submitFormToServer().then(() => {
this.isSubmitted = true;
}).catch(() => {
this.isLoading = false;
});
}).catch(() => {
this.isLoading = false;
});
Expand Down

0 comments on commit b0b8e83

Please sign in to comment.