82 lines
2.3 KiB
JavaScript
82 lines
2.3 KiB
JavaScript
// Proof-of-Work Captcha Service
|
|
// Client must find nonce where SHA256(challenge + nonce) has N leading zeros
|
|
|
|
const DIFFICULTY = 4 // Number of leading zeros required (4 = ~65k attempts avg)
|
|
const CHALLENGE_EXPIRY = 5 * 60 * 1000 // 5 minutes
|
|
|
|
// Generate a challenge (call this from your API/backend)
|
|
export function generateChallenge() {
|
|
const challenge = crypto.randomUUID()
|
|
const timestamp = Date.now()
|
|
return {
|
|
challenge,
|
|
difficulty: DIFFICULTY,
|
|
timestamp,
|
|
// Sign to prevent tampering (simple HMAC alternative)
|
|
signature: btoa(`${challenge}:${timestamp}:${DIFFICULTY}`)
|
|
}
|
|
}
|
|
|
|
// Verify solution (call this from your API/backend)
|
|
export async function verifySolution(challenge, nonce, signature, timestamp) {
|
|
// Check expiry
|
|
if (Date.now() - timestamp > CHALLENGE_EXPIRY) {
|
|
return { valid: false, error: 'Challenge expired' }
|
|
}
|
|
|
|
// Verify signature
|
|
const expectedSig = btoa(`${challenge}:${timestamp}:${DIFFICULTY}`)
|
|
if (signature !== expectedSig) {
|
|
return { valid: false, error: 'Invalid signature' }
|
|
}
|
|
|
|
// Verify PoW
|
|
const hash = await sha256(`${challenge}${nonce}`)
|
|
const prefix = '0'.repeat(DIFFICULTY)
|
|
|
|
if (hash.startsWith(prefix)) {
|
|
return { valid: true }
|
|
}
|
|
|
|
return { valid: false, error: 'Invalid proof of work' }
|
|
}
|
|
|
|
// Solve challenge (runs in browser)
|
|
export async function solveChallenge(challenge, difficulty, onProgress) {
|
|
let nonce = 0
|
|
const prefix = '0'.repeat(difficulty)
|
|
const startTime = Date.now()
|
|
|
|
while (true) {
|
|
const hash = await sha256(`${challenge}${nonce}`)
|
|
|
|
if (hash.startsWith(prefix)) {
|
|
return {
|
|
nonce,
|
|
hash,
|
|
duration: Date.now() - startTime
|
|
}
|
|
}
|
|
|
|
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)
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
|
}
|