// Package token makes item 3's credentials, stores only a hash, and shows each once. package token import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "fmt" "strings" ) // Kind is a token's scope and its visible prefix, so a leaked one can be found by grep. type Kind string const ( Git Kind = "gt" // push and clone over https Runner Kind = "rt" // one machine, one repository Feed Kind = "ft" // read one atom feed, and nothing else Session Kind = "se" // a browser session cookie Claim Kind = "cl" // one use, trades a signed nonce for a browser session ) func (k Kind) prefix() string { return string(k) + "_live_" } // New returns the only copy of a token, plus the hash to store, and nothing recovers it later. func New(k Kind) (tok, hash string, err error) { buf := make([]byte, 16) if _, err := rand.Read(buf); err != nil { return "", "", err } tok = k.prefix() + base64.RawURLEncoding.EncodeToString(buf) return tok, Hash(tok), nil } // Hash is unsalted SHA-256, right for 128 machine-generated bits with no dictionary to fear. func Hash(tok string) string { sum := sha256.Sum256([]byte(tok)) return hex.EncodeToString(sum[:]) } // KindOf reads the claimed kind, so a lookup can be scoped before it happens. func KindOf(tok string) (Kind, error) { for _, k := range []Kind{Git, Runner, Feed, Session, Claim} { if strings.HasPrefix(tok, k.prefix()) { return k, nil } } return "", fmt.Errorf("that does not look like a barerepo token") } // Equal compares two hashes in constant time. func Equal(a, b string) bool { return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 }