carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1package crypto
2
3import (
4 "crypto/sha256"
5 "errors"
6 "strings"
7)
8
9// Alphabet is Crockford's Base32 set: no I, L, O or U.
10//
11// Case-insensitive, and decoding maps O to 0 and I and L to 1 — the exact
12// confusions humans and OCR make reading aged paper, which is what these codes
13// are printed on.
14const Alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
15
16// The printed credential form: tag(2) || data(26) || checksum(4).
17const (
18 TagAccountKey = "A1"
19 TagRecovery = "C1"
20 dataSymbols = 26
21 checkSymbols = 4
22 codeSymbols = 2 + dataSymbols + checkSymbols
23)
24
25var (
26 ErrAlphabet = errors.New("crypto: character outside the Crockford alphabet")
27 ErrLength = errors.New("crypto: wrong number of symbols")
28 ErrPadding = errors.New("crypto: non-zero pad bits in the final symbol")
29 ErrChecksum = errors.New("crypto: checksum does not match")
30 ErrTag = errors.New("crypto: wrong tag")
31)
32
33// symbolValue applies the case folding and the O/I/L substitutions, returning
34// -1 for anything outside the alphabet.
35func symbolValue(c byte) int {
36 if c >= 'a' && c <= 'z' {
37 c -= 'a' - 'A'
38 }
39 switch c {
40 case 'O':
41 c = '0'
42 case 'I', 'L':
43 c = '1'
44 }
45 if i := strings.IndexByte(Alphabet, c); i >= 0 {
46 return i
47 }
48 return -1
49}
50
51// Normalize strips hyphens and whitespace, uppercases, and applies the O/I/L
52// mappings. The two-character tag survives it unchanged, so callers normalise
53// first and match the tag afterwards.
54func Normalize(s string) (string, error) {
55 var b strings.Builder
56 for i := 0; i < len(s); i++ {
57 c := s[i]
58 if c == '-' || c == ' ' || c == '\t' || c == '\n' || c == '\r' {
59 continue
60 }
61 v := symbolValue(c)
62 if v < 0 {
63 return "", ErrAlphabet
64 }
65 b.WriteByte(Alphabet[v])
66 }
67 return b.String(), nil
68}
69
70// Base32Encode packs bytes MSB-first and left-aligned, so 16 bytes become 26
71// symbols whose last one carries two zero pad bits.
72//
73// Byte packing rather than Crockford's big-integer form: no language needs a
74// bignum to read a recovery code, which matters most in the browser, where the
75// JS modules are capped at 300 lines by test.
76func Base32Encode(in []byte) string {
77 n := (len(in)*8 + 4) / 5
78 out := make([]byte, n)
79 for i := 0; i < n; i++ {
80 bit := i * 5
81 by, off := bit/8, bit%8
82 v := uint32(in[by]) << 8
83 if by+1 < len(in) {
84 v |= uint32(in[by+1])
85 }
86 out[i] = Alphabet[(v>>(11-off))&0x1f]
87 }
88 return string(out)
89}
90
91// Base32Decode is strict: it rejects a non-zero pad in the final symbol, so
92// every value has exactly one encoding. Without that two different strings
93// decode to one key, and the checksum stops meaning what it says.
94func Base32Decode(sym string, nbytes int) ([]byte, error) {
95 if len(sym) != (nbytes*8+4)/5 {
96 return nil, ErrLength
97 }
98 out := make([]byte, 0, nbytes)
99 var acc uint32
100 var bits int
101 for i := 0; i < len(sym); i++ {
102 v := symbolValue(sym[i])
103 if v < 0 {
104 return nil, ErrAlphabet
105 }
106 acc = acc<<5 | uint32(v)
107 bits += 5
108 if bits >= 8 {
109 bits -= 8
110 out = append(out, byte(acc>>bits))
111 }
112 }
113 if acc&((1<<bits)-1) != 0 {
114 return nil, ErrPadding
115 }
116 return out, nil
117}
118
119// Checksum20 is the first 20 bits, MSB-first, of SHA-256 over the raw payload
120// bytes — not over the string. Twenty bits is enough to catch a mistyped
121// character and small enough to fit four symbols on a printed line.
122func Checksum20(payload []byte) uint32 {
123 d := sha256.Sum256(payload)
124 return uint32(d[0])<<12 | uint32(d[1])<<4 | uint32(d[2])>>4
125}
126
127func checksumSymbols(c uint32) string {
128 out := make([]byte, checkSymbols)
129 for i := 0; i < checkSymbols; i++ {
130 out[i] = Alphabet[(c>>(15-5*i))&0x1f]
131 }
132 return string(out)
133}
134
135// NormalizedCode renders the 32-character canonical form: tag, data, checksum.
136func NormalizedCode(tag string, payload []byte) string {
137 return tag + Base32Encode(payload) + checksumSymbols(Checksum20(payload))
138}
139
140// FormatCode renders what actually goes on the paper:
141// TAG-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX.
142func FormatCode(tag string, payload []byte) string {
143 norm := NormalizedCode(tag, payload)
144 var b strings.Builder
145 b.WriteString(tag)
146 for i := 2; i < len(norm); i++ {
147 if (i-2)%5 == 0 {
148 b.WriteByte('-')
149 }
150 b.WriteByte(norm[i])
151 }
152 return b.String()
153}
154
155// ParseCode normalises, checks the tag and the checksum, and returns the
156// payload. A checksum failure is where the caller reaches for RepairCode.
157func ParseCode(tag, s string) ([]byte, error) {
158 norm, err := Normalize(s)
159 if err != nil {
160 return nil, err
161 }
162 return parseNormalized(tag, norm)
163}
164
165func parseNormalized(tag, norm string) ([]byte, error) {
166 if len(norm) != codeSymbols {
167 return nil, ErrLength
168 }
169 if norm[:2] != tag {
170 return nil, ErrTag
171 }
172 payload, err := Base32Decode(norm[2:2+dataSymbols], 16)
173 if err != nil {
174 return nil, err
175 }
176 if checksumSymbols(Checksum20(payload)) != norm[2+dataSymbols:] {
177 return nil, ErrChecksum
178 }
179 return payload, nil
180}
181
182// RepairCode brute-forces every single-character substitution over the 30
183// payload positions — about a thousand candidates — and returns every one
184// whose checksum passes, in position-then-alphabet order.
185//
186// It deliberately does not pick a winner. Twenty bits collides roughly once in
187// eleven hundred attempts, so the caller tries each candidate against the KCV,
188// which does not collide. Choosing here would sometimes choose wrong and
189// silently hand back the wrong key.
190func RepairCode(tag, s string) ([]string, error) {
191 norm, err := Normalize(s)
192 if err != nil {
193 return nil, err
194 }
195 if len(norm) != codeSymbols {
196 return nil, ErrLength
197 }
198
199 var out []string
200 buf := []byte(norm)
201 for pos := 2; pos < codeSymbols; pos++ {
202 orig := buf[pos]
203 for i := 0; i < len(Alphabet); i++ {
204 if Alphabet[i] == orig {
205 continue
206 }
207 buf[pos] = Alphabet[i]
208 if _, err := parseNormalized(tag, string(buf)); err == nil {
209 out = append(out, string(buf))
210 }
211 }
212 buf[pos] = orig
213 }
214 return out, nil
215}
216