HTTP Hashcash implemented in Go.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

285 lines
6.6 KiB

package hashcash
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"errors"
"strconv"
"strings"
"time"
)
// ErrParse is returned when fewer than 6 or more than 7 segments are split
var ErrParse = errors.New("could not split the hashcash parts")
// ErrInvalidTag is returned when the Hashcash version is unsupported
var ErrInvalidTag = errors.New("expected tag to be 'H'")
// ErrInvalidDifficulty is returned when the difficulty is outside of the acceptable range
var ErrInvalidDifficulty = errors.New("the number of bits of difficulty is too low or too high")
// ErrInvalidDate is returned when the date cannot be parsed as a positive int64
var ErrInvalidDate = errors.New("invalid date")
// ErrExpired is returned when the current time is past that of ExpiresAt
var ErrExpired = errors.New("expired hashcash")
// ErrInvalidSubject is returned when the subject is invalid or does not match that passed to Verify()
var ErrInvalidSubject = errors.New("the subject is invalid or rejected")
// ErrInvalidNonce is returned when the nonce
//var ErrInvalidNonce = errors.New("the nonce has been used or is invalid")
// ErrUnsupportedAlgorithm is returned when the given algorithm is not supported
var ErrUnsupportedAlgorithm = errors.New("the given algorithm is invalid or not supported")
// ErrInvalidSolution is returned when the given hashcash is not properly solved
var ErrInvalidSolution = errors.New("the given solution is not valid")
// MaxDifficulty is the upper bound for all Solve() operations
var MaxDifficulty = 26
// Sep is the separator character to use
var Sep = ":"
// no milliseconds
//var isoTS = "2006-01-02T15:04:05Z"
// Hashcash represents a parsed Hashcash string
type Hashcash struct {
Tag string `json:"tag"` // Always "H" for "HTTP"
Difficulty int `json:"difficulty"` // Number of "partial pre-image" (zero) bits in the hashed code
ExpiresAt time.Time `json:"exp"` // The timestamp that the hashcash expires, as seconds since the Unix epoch
Subject string `json:"sub"` // Resource data string being transmitted, e.g., a domain or URL
Nonce string `json:"nonce"` // Unique string of random characters, encoded as url-safe base-64
Alg string `json:"alg"` // always SHA-256 for now
Solution string `json:"solution"` // Binary counter, encoded as url-safe base-64
}
// New returns a Hashcash with reasonable defaults
func New(h Hashcash) *Hashcash {
h.Tag = "H"
if 0 == h.Difficulty {
// safe for WebCrypto
h.Difficulty = 10
}
if h.ExpiresAt.IsZero() {
h.ExpiresAt = time.Now().Add(5 * time.Minute)
}
h.ExpiresAt = h.ExpiresAt.UTC().Truncate(time.Second)
if "" == h.Subject {
h.Subject = "*"
}
if "" == h.Nonce {
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); nil != err {
panic(err)
return nil
}
h.Nonce = base64.RawURLEncoding.EncodeToString(nonce)
}
if "" == h.Alg {
h.Alg = "SHA-256"
}
/*
if "SHA-256" != h.Alg {
// TODO error
}
*/
return &h
}
// Parse will (obviously) parse the hashcash string, without verifying any
// of the parameters.
func Parse(hc string) (*Hashcash, error) {
parts := strings.Split(hc, Sep)
n := len(parts)
if n < 6 || n > 7 {
return nil, ErrParse
}
tag := parts[0]
if "H" != tag {
return nil, ErrInvalidTag
}
bits, err := strconv.Atoi(parts[1])
if nil != err || bits < 0 {
return nil, ErrInvalidDifficulty
}
// Allow empty ExpiresAt
var exp time.Time
if "" != parts[2] {
expAt, err := strconv.ParseInt(parts[2], 10, 64)
if nil != err || expAt < 0 {
return nil, ErrInvalidDate
}
exp = time.Unix(int64(expAt), 0).UTC()
}
/*
exp, err := time.ParseInLocation(isoTS, parts[2], time.UTC)
if nil != err {
return nil, ErrInvalidDate
}
*/
sub := parts[3]
nonce := parts[4]
alg := parts[5]
var solution string
if n > 6 {
solution = parts[6]
}
h := &Hashcash{
Tag: tag,
Difficulty: bits,
ExpiresAt: exp.UTC().Truncate(time.Second),
Subject: sub,
Nonce: nonce,
Alg: alg,
Solution: solution,
}
return h, nil
}
// String will return the formatted Hashcash, omitting the solution if it has not be solved.
func (h *Hashcash) String() string {
var solution string
if "" != h.Solution {
solution = Sep + h.Solution
}
var expAt string
if !h.ExpiresAt.IsZero() {
expAt = strconv.FormatInt(h.ExpiresAt.UTC().Truncate(time.Second).Unix(), 10)
}
return strings.Join(
[]string{
"H",
strconv.Itoa(h.Difficulty),
//h.ExpiresAt.UTC().Format(isoTS),
expAt,
h.Subject,
h.Nonce,
h.Alg,
},
Sep,
) + solution
}
// Verify the Hashcash based on Difficulty, Algorithm, ExpiresAt, Subject and,
// of course, the Solution and hash.
func (h *Hashcash) Verify(subject string) error {
if h.Difficulty < 0 {
return ErrInvalidDifficulty
}
if "SHA-256" != h.Alg {
return ErrUnsupportedAlgorithm
}
if !h.ExpiresAt.IsZero() && h.ExpiresAt.Sub(time.Now()) < 0 {
return ErrExpired
}
if subject != h.Subject {
return ErrInvalidSubject
}
bits := h.Difficulty
hash := sha256.Sum256([]byte(h.String()))
n := bits / 8 // 10 / 8 = 1
m := bits % 8 // 10 % 8 = 2
if m > 0 {
n++ // 10 bits = 2 bytes
}
if !verifyBits(hash[:n], bits, n) {
return ErrInvalidSolution
}
return nil
}
func verifyBits(hash []byte, bits, n int) bool {
if 0 == bits {
return true
}
for i := 0; i < n; i++ {
if bits > 8 {
bits -= 8
if 0 != hash[i] {
return false
}
continue
}
// (bits % 8) == bits
pad := 8 - bits
if 0 == hash[i]>>pad {
return true
}
}
return false
}
// Solve will search for a solution, returning an error if the difficulty is
// above the local or global MaxDifficulty, the Algorithm is unsupported.
func (h *Hashcash) Solve(maxDifficulty int) error {
if "SHA-256" != h.Alg {
return ErrUnsupportedAlgorithm
}
if h.Difficulty < 0 {
return ErrInvalidDifficulty
}
if h.Difficulty > maxDifficulty || h.Difficulty > MaxDifficulty {
return ErrInvalidDifficulty
}
if "" != h.Solution {
if nil == h.Verify(h.Subject) {
return nil
}
h.Solution = ""
}
hashcash := h.String()
bits := h.Difficulty
n := bits / 8 // 10 / 8 = 1
m := bits % 8 // 10 % 8 = 2
if m > 0 {
n++ // 10 bits = 2 bytes
}
var solution uint32 = 0
sb := make([]byte, 4)
for {
// Note: it's not actually important what method of change or encoding is used
// but incrementing by 1 on an int32 is good enough, and makes for a small base64 encoding
binary.LittleEndian.PutUint32(sb, solution)
h.Solution = base64.RawURLEncoding.EncodeToString(sb)
hash := sha256.Sum256([]byte(hashcash + Sep + h.Solution))
if verifyBits(hash[:n], bits, n) {
return nil
}
solution++
}
}