Files
cerberus/cerberus.go
T
y a t s 840a63f727 Simplify bitmask creation
Separate clearnet and onion tests
2026-06-03 12:45:15 -04:00

152 lines
3.5 KiB
Go

package cerberus
import (
"context"
"fmt"
"math/rand"
"net/url"
"runtime"
"github.com/minio/sha256-simd"
)
type Challenge struct {
Salt string // Challenge salt from server.
Diff uint32 // Difficulty level.
Steps uint8 // Each step consists of a Challenge and a Solution. More than 1 may be required.
host *url.URL
}
// Convenience wrapper, equivalent to Solve(ctx, c).
func (c Challenge) Solve(ctx context.Context) (Solution, error) {
return Solve(ctx, c)
}
type Solution struct {
Hash []byte // Not required in POST, but provided for reference.
Salt string // Challenge salt.
Nonce uint32 // Solution nonce. This is the "answer" to the problem.
Redirect string // Relative path to redirect to after Solution is accepted.
// TODO: Maybe make the redirect shit auto, idk.
host *url.URL
}
// Given difficulty is measured in number of leading 0 bits.
func checkZeros(diff uint32, hash []byte) bool {
// I am using this big ugly check to avoid needing to hardcode the max difficulty (32, by the looks of it).
// Otherwise, it's a simple < comparison to (1 << (32 - diff))
var (
rem = diff % 8 // Remainder after dividing diff (given in bits) to bytes.
nbytes = (diff - rem) / 8 // Amount of 0x0 bytes we can divide difficulty bits into.
// Bitmask by setting a 1 bit (on LHS) for each remaining bit to check.
mask uint8 = ^((1 << (8 - rem)) - 1)
// Example (rem = 3): mask = ^((1 << (8 - 3)) - 1)
// = ^(00100000 - 1)
// = ^(00011111)
// = 11100000
)
// Check bounds for the loops found below.
if lh := uint32(len(hash)); lh < nbytes || (rem > 0 && lh < nbytes+1) {
return false
}
// First, we count the number of leading 0x0 bytes.
for i := range nbytes {
if b := hash[i]; b != 0x0 {
return false
}
}
// If we don't have any more bits to check, return.
if rem == 0 {
return true
}
return hash[nbytes]&mask == 0x0
}
// Brute force nonces until a valid solution is found.
func genHashes(ctx context.Context, c Challenge) <-chan Solution {
var (
out = make(chan Solution, 1) // Probably unnecessary buffering for questionable efficiency.
sha = sha256.New()
nonce = rand.Uint32()
)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
default:
}
sha.Write(fmt.Append(nil, c.Salt, nonce))
sol := Solution{
Hash: sha.Sum(nil),
Salt: c.Salt,
Nonce: nonce,
Redirect: "/", // Use sensible default for placeholder, for now.
}
// Ensure we don't hang if out channel is full on ctx close.
select {
case <-ctx.Done():
return
case out <- sol:
}
// Reset hasher input for next iteration.
sha.Reset()
nonce++
}
}()
return out
}
// Solve Challenge c. Returns Solution that can be submitted.
func Solve(ctx context.Context, c Challenge) (Solution, error) {
sol := make(chan Solution, 1)
ctx, cancel := context.WithCancel(ctx)
defer cancel()
worker := func() {
hf := genHashes(ctx, c)
// Loop until answer has been found.
// Should break when hash worker terminates.
for h := range hf {
if checkZeros(c.Diff, h.Hash) {
h.Salt = c.Salt
// Set host url to submit this to.
h.host = c.host
sol <- h
}
}
}
go func() {
// A reasonable hardware-based job limiter.
// Use 1 worker per thread at most.
threads := runtime.NumCPU()
for range threads {
go worker()
}
}()
select {
case <-ctx.Done():
return Solution{}, ctx.Err()
case s := <-sol:
return s, nil
}
}