oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
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.
19const (
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.
36var 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.
41var personRefBodyLen = personRefEnc.EncodedLen(personRefBytes)
42
43// NewPersonRef mints a fresh, opaque person ref.
44func 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.
60func 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