285 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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 {
 | |
| 	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 false
 | |
| 		}
 | |
| 
 | |
| 		return true
 | |
| 	}
 | |
| 
 | |
| 	// 0 == bits
 | |
| 	return true
 | |
| }
 | |
| 
 | |
| // 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++
 | |
| 	}
 | |
| }
 |