Go · 1686 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 // Package token mints high-entropy random tokens and stores their sha256
4 // hashes. The pattern: generate a random token, send it in URLs/emails,
5 // store only the hash. A DB dump leaks no usable tokens; lookups remain
6 // O(1) via a UNIQUE index on the hash column.
7 package token
8
9 import (
10 "crypto/rand"
11 "crypto/sha256"
12 "crypto/subtle"
13 "encoding/base64"
14 "errors"
15 )
16
17 // SizeBytes is the random payload length. 32 bytes (256 bits) is far
18 // beyond brute-force budgets for any plausible attacker. base64url-encoded
19 // it produces a 43-character ASCII-safe URL fragment.
20 const SizeBytes = 32
21
22 // New mints a fresh token. Returns the URL-safe encoding for inclusion in
23 // emails and links and the sha256 hash for storage in the DB.
24 func New() (encoded string, hash []byte, err error) {
25 raw := make([]byte, SizeBytes)
26 if _, err := rand.Read(raw); err != nil {
27 return "", nil, err
28 }
29 encoded = base64.RawURLEncoding.EncodeToString(raw)
30 sum := sha256.Sum256(raw)
31 return encoded, sum[:], nil
32 }
33
34 // HashOf returns the sha256 of the raw bytes encoded in s. Used by lookup
35 // paths: parse the URL fragment, hash, query by hash.
36 func HashOf(s string) ([]byte, error) {
37 raw, err := base64.RawURLEncoding.DecodeString(s)
38 if err != nil {
39 return nil, errors.New("token: malformed encoding")
40 }
41 if len(raw) != SizeBytes {
42 return nil, errors.New("token: wrong length")
43 }
44 sum := sha256.Sum256(raw)
45 return sum[:], nil
46 }
47
48 // Equal compares two hashes in constant time. Use this instead of
49 // bytes.Equal anywhere a comparison's timing could leak information.
50 func Equal(a, b []byte) bool {
51 return subtle.ConstantTimeCompare(a, b) == 1
52 }
53