feat: self-host TweetNaCl, add server-side PoW captcha (PHP), activate categoriesService
This commit is contained in:
@@ -14,10 +14,9 @@ class CryptoService {
|
||||
}
|
||||
|
||||
async init() {
|
||||
// Dynamically import TweetNaCl from CDN
|
||||
if (!window.nacl) {
|
||||
await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl@1.0.3/nacl-fast.min.js')
|
||||
await this.loadScript('https://cdn.jsdelivr.net/npm/tweetnacl-util@0.15.1/nacl-util.min.js')
|
||||
await this.loadScript('/js/vendor/nacl-fast.min.js')
|
||||
await this.loadScript('/js/vendor/nacl-util.min.js')
|
||||
}
|
||||
|
||||
this.nacl = window.nacl
|
||||
|
||||
@@ -1,22 +1,67 @@
|
||||
// Proof-of-Work Captcha Service
|
||||
// Client must find nonce where SHA256(challenge + nonce) has N leading zeros
|
||||
// Server-first: tries /pow/challenge endpoint, falls back to local generation
|
||||
|
||||
const DIFFICULTY = 4 // Number of leading zeros required (4 = ~65k attempts avg)
|
||||
const POW_SERVER = 'https://pow.dgray.io'
|
||||
const DIFFICULTY = 4
|
||||
const SERVER_TIMEOUT_MS = 1500
|
||||
|
||||
// TODO: Replace with a server-side endpoint. Currently generates challenge
|
||||
// client-side with a btoa() "signature" that provides no real security.
|
||||
export function generateChallenge() {
|
||||
function localGenerateChallenge() {
|
||||
const challenge = crypto.randomUUID()
|
||||
const timestamp = Date.now()
|
||||
return {
|
||||
challenge,
|
||||
difficulty: DIFFICULTY,
|
||||
timestamp,
|
||||
signature: btoa(`${challenge}:${timestamp}:${DIFFICULTY}`)
|
||||
signature: btoa(`${challenge}:${timestamp}:${DIFFICULTY}`),
|
||||
source: 'local'
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateChallenge() {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), SERVER_TIMEOUT_MS)
|
||||
|
||||
const response = await fetch(`${POW_SERVER}/challenge`, {
|
||||
method: 'GET',
|
||||
signal: controller.signal
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
|
||||
const data = await response.json()
|
||||
return { ...data, source: 'server' }
|
||||
} catch {
|
||||
return localGenerateChallenge()
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifySolution(solution) {
|
||||
try {
|
||||
const controller = new AbortController()
|
||||
const timeout = setTimeout(() => controller.abort(), SERVER_TIMEOUT_MS)
|
||||
|
||||
const response = await fetch(`${POW_SERVER}/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(solution),
|
||||
signal: controller.signal
|
||||
})
|
||||
clearTimeout(timeout)
|
||||
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
||||
|
||||
const data = await response.json()
|
||||
return { ok: data.ok === true, token: data.captcha_token || null }
|
||||
} catch {
|
||||
const hash = await sha256(`${solution.challenge}${solution.nonce}`)
|
||||
const prefix = '0'.repeat(solution.difficulty || DIFFICULTY)
|
||||
return { ok: hash.startsWith(prefix), token: null }
|
||||
}
|
||||
}
|
||||
|
||||
// Solve challenge (runs in browser)
|
||||
export async function solveChallenge(challenge, difficulty, onProgress) {
|
||||
let nonce = 0
|
||||
const prefix = '0'.repeat(difficulty)
|
||||
@@ -35,19 +80,16 @@ export async function solveChallenge(challenge, difficulty, onProgress) {
|
||||
|
||||
nonce++
|
||||
|
||||
// Report progress every 1000 attempts
|
||||
if (onProgress && nonce % 1000 === 0) {
|
||||
onProgress({ attempts: nonce, elapsed: Date.now() - startTime })
|
||||
}
|
||||
|
||||
// Yield to main thread every 100 attempts
|
||||
if (nonce % 100 === 0) {
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SHA256 helper
|
||||
async function sha256(message) {
|
||||
const msgBuffer = new TextEncoder().encode(message)
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
|
||||
|
||||
Reference in New Issue
Block a user