| 1 | package home |
| 2 | |
| 3 | import ( |
| 4 | "crypto/rand" |
| 5 | "encoding/base32" |
| 6 | "strings" |
| 7 | ) |
| 8 | |
| 9 | // The person ref is the subject of every session, credential and |
| 10 | // recovery code, and later the assertion's sub and the directory's join |
| 11 | // key. It is opaque, stable across an email change, and never derived |
| 12 | // from the address. |
| 13 | // |
| 14 | // "Never derived" is the load-bearing word. A ref computed from the |
| 15 | // address — even hashed, even peppered — puts the address back at rest |
| 16 | // in a reversible-by-dictionary form under another name, which is the |
| 17 | // whole thing §3 exists to prevent. NewPersonRef therefore takes no |
| 18 | // argument, and there is no variant that does. |
| 19 | const ( |
| 20 | // personRefBytes is the entropy behind a ref. 16 bytes makes a |
| 21 | // collision implausible at any number of people this will ever hold, |
| 22 | // and stays short enough to read in a log line — which it is allowed |
| 23 | // to appear in, per §3.4: "Errors carry person refs and codes." |
| 24 | personRefBytes = 16 |
| 25 | |
| 26 | // personRefPrefix makes a ref self-describing in a log or an error, |
| 27 | // so a value that turns up somewhere unexpected can be recognised |
| 28 | // for what it is rather than guessed at. |
| 29 | personRefPrefix = "pr_" |
| 30 | ) |
| 31 | |
| 32 | // personRefEnc is unpadded base32, lowercased on the way out. Base32 |
| 33 | // rather than base64 because the alphabet has no /, + or =: the ref goes |
| 34 | // into URL paths and log lines, and a value that needs escaping in |
| 35 | // either is a value somebody will forget to escape. |
| 36 | var personRefEnc = base32.StdEncoding.WithPadding(base32.NoPadding) |
| 37 | |
| 38 | // personRefBodyLen is how many characters personRefBytes encodes to. |
| 39 | // Computed rather than written as 26, so changing the entropy cannot |
| 40 | // leave the validator silently checking the old length. |
| 41 | var personRefBodyLen = personRefEnc.EncodedLen(personRefBytes) |
| 42 | |
| 43 | // NewPersonRef mints a fresh, opaque person ref. |
| 44 | func NewPersonRef() (string, error) { |
| 45 | b := make([]byte, personRefBytes) |
| 46 | if _, err := rand.Read(b); err != nil { |
| 47 | return "", err |
| 48 | } |
| 49 | return personRefPrefix + strings.ToLower(personRefEnc.EncodeToString(b)), nil |
| 50 | } |
| 51 | |
| 52 | // ValidPersonRef reports whether s has the exact shape NewPersonRef |
| 53 | // produces: the prefix, the exact length, and nothing outside the |
| 54 | // lowercased base32 alphabet. |
| 55 | // |
| 56 | // It is deliberately a shape check and not a lookup. Callers use it to |
| 57 | // refuse rubbish at the edge — a path segment, a form field — before it |
| 58 | // reaches a query; whether a well-shaped ref names anybody is the |
| 59 | // database's answer, not this function's. |
| 60 | func ValidPersonRef(s string) bool { |
| 61 | body, ok := strings.CutPrefix(s, personRefPrefix) |
| 62 | if !ok || len(body) != personRefBodyLen { |
| 63 | return false |
| 64 | } |
| 65 | for _, r := range body { |
| 66 | switch { |
| 67 | case r >= 'a' && r <= 'z': |
| 68 | case r >= '2' && r <= '7': |
| 69 | default: |
| 70 | return false |
| 71 | } |
| 72 | } |
| 73 | return true |
| 74 | } |
| 75 | |