base32.nx source
↩ module page · 178 lines · 6512 B
1// base32.nx -- RFC 4648 base32 encoder + decoder.
2//
3// Alphabet: A-Z, 2-7 (case-insensitive on decode).
4// Used by: TOTP 2FA secrets (Google Authenticator, Authy, etc.),
5// Matrix recovery keys, Steam Guard, DNS TXT record binary payload,
6// Base32-encoded Onion v3 addresses (minus checksum).
7//
8// Less efficient than base64 (5/8 vs 3/4 ratio) but phone-typable
9// (no mixed case) and larger alphabet margin of error vs case-
10// sensitive encodings.
11//
12// Format: 8 output chars per 5 input bytes; padding '=' to fill
13// incomplete groups. Unpadded variant tolerated on decode.
14//
15// Invariants:
16// B32_1 Encoder is uppercase-only; decoder accepts both cases.
17// B32_2 Padding reinstated to 8-char groups on encode; missing
18// padding accepted on decode.
19// B32_3 Round-trip exact for any byte sequence.
20
21import "syscalls.nx"
22
23const B32_PAD: i64 = 0x3D // '='
24const B32_ERR_BAD: i64 = -1
25
26// 5-bit value -> uppercase base32 char.
27func b32_enc_char(v: i64) -> i64 {
28 if v < 26 { return 0x41 + v } // 'A'..'Z'
29 return 0x32 + (v - 26) // '2'..'7'
30}
31
32// Base32 char -> 5-bit value, -1 on invalid. Case-insensitive.
33func b32_dec_val(c: i64) -> i64 {
34 if c >= 0x41 { if c <= 0x5A { return c - 0x41 } } // A-Z
35 if c >= 0x61 { if c <= 0x7A { return c - 0x61 } } // a-z
36 if c >= 0x32 { if c <= 0x37 { return c - 0x32 + 26 } } // 2-7
37 return B32_ERR_BAD
38}
39
40// Encode n bytes to base32. Output length = ceil(n/5) * 8.
41func base32_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
42 var pos: i64 = 0
43 var out_pos: i64 = 0
44 while pos + 5 <= n {
45 let b0: i64 = in_bytes[pos]
46 let b1: i64 = in_bytes[pos + 1]
47 let b2: i64 = in_bytes[pos + 2]
48 let b3: i64 = in_bytes[pos + 3]
49 let b4: i64 = in_bytes[pos + 4]
50 out[out_pos + 0] = b32_enc_char((b0 >> 3) & 0x1F)
51 out[out_pos + 1] = b32_enc_char(((b0 << 2) | (b1 >> 6)) & 0x1F)
52 out[out_pos + 2] = b32_enc_char((b1 >> 1) & 0x1F)
53 out[out_pos + 3] = b32_enc_char(((b1 << 4) | (b2 >> 4)) & 0x1F)
54 out[out_pos + 4] = b32_enc_char(((b2 << 1) | (b3 >> 7)) & 0x1F)
55 out[out_pos + 5] = b32_enc_char((b3 >> 2) & 0x1F)
56 out[out_pos + 6] = b32_enc_char(((b3 << 3) | (b4 >> 5)) & 0x1F)
57 out[out_pos + 7] = b32_enc_char(b4 & 0x1F)
58 pos = pos + 5
59 out_pos = out_pos + 8
60 }
61 let rem: i64 = n - pos
62 if rem > 0 {
63 let b0: i64 = in_bytes[pos]
64 var b1: i64 = 0
65 var b2: i64 = 0
66 var b3: i64 = 0
67 if rem > 1 { b1 = in_bytes[pos + 1] }
68 if rem > 2 { b2 = in_bytes[pos + 2] }
69 if rem > 3 { b3 = in_bytes[pos + 3] }
70 out[out_pos + 0] = b32_enc_char((b0 >> 3) & 0x1F)
71 out[out_pos + 1] = b32_enc_char(((b0 << 2) | (b1 >> 6)) & 0x1F)
72 if rem == 1 {
73 out[out_pos + 2] = B32_PAD
74 out[out_pos + 3] = B32_PAD
75 out[out_pos + 4] = B32_PAD
76 out[out_pos + 5] = B32_PAD
77 out[out_pos + 6] = B32_PAD
78 out[out_pos + 7] = B32_PAD
79 } else {
80 out[out_pos + 2] = b32_enc_char((b1 >> 1) & 0x1F)
81 out[out_pos + 3] = b32_enc_char(((b1 << 4) | (b2 >> 4)) & 0x1F)
82 if rem == 2 {
83 out[out_pos + 4] = B32_PAD
84 out[out_pos + 5] = B32_PAD
85 out[out_pos + 6] = B32_PAD
86 out[out_pos + 7] = B32_PAD
87 } else {
88 out[out_pos + 4] = b32_enc_char(((b2 << 1) | (b3 >> 7)) & 0x1F)
89 if rem == 3 {
90 out[out_pos + 5] = B32_PAD
91 out[out_pos + 6] = B32_PAD
92 out[out_pos + 7] = B32_PAD
93 } else {
94 out[out_pos + 5] = b32_enc_char((b3 >> 2) & 0x1F)
95 out[out_pos + 6] = b32_enc_char((b3 << 3) & 0x1F)
96 out[out_pos + 7] = B32_PAD
97 }
98 }
99 }
100 out_pos = out_pos + 8
101 }
102 return out_pos
103}
104
105// Decode n base32 chars. Returns bytes written or -ERR.
106func base32_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
107 var pos: i64 = 0
108 var out_pos: i64 = 0
109 let q_raw: *u8 = sys_mmap(8)
110 let quintets: *i64 = q_raw as *i64
111 while pos < n {
112 // Fill 8 quintets (or fewer if padding encountered).
113 var got: i64 = 0
114 var k: i64 = 0
115 while k < 8 {
116 quintets[k] = -1
117 k = k + 1
118 }
119 k = 0
120 while k < 8 {
121 if pos >= n { k = 8 }
122 else {
123 let c: i64 = in_chars[pos]
124 if c == B32_PAD { pos = n; k = 8 }
125 else {
126 let v: i64 = b32_dec_val(c)
127 if v < 0 { return B32_ERR_BAD }
128 quintets[got] = v
129 got = got + 1
130 pos = pos + 1
131 k = k + 1
132 }
133 }
134 }
135 // Reconstruct bytes from quintets. got = 2 -> 1 byte,
136 // got = 4 -> 2, got = 5 -> 3, got = 7 -> 4, got = 8 -> 5.
137 if got == 0 { return out_pos }
138 let q0: i64 = quintets[0]
139 let q1: i64 = quintets[1]
140 let q2: i64 = quintets[2]
141 let q3: i64 = quintets[3]
142 let q4: i64 = quintets[4]
143 let q5: i64 = quintets[5]
144 let q6: i64 = quintets[6]
145 let q7: i64 = quintets[7]
146 if got >= 2 {
147 out[out_pos] = ((q0 << 3) | (q1 >> 2)) & 0xFF
148 out_pos = out_pos + 1
149 }
150 if got >= 4 {
151 out[out_pos] = ((q1 << 6) | (q2 << 1) | (q3 >> 4)) & 0xFF
152 out_pos = out_pos + 1
153 }
154 if got >= 5 {
155 out[out_pos] = ((q3 << 4) | (q4 >> 1)) & 0xFF
156 out_pos = out_pos + 1
157 }
158 if got >= 7 {
159 out[out_pos] = ((q4 << 7) | (q5 << 2) | (q6 >> 3)) & 0xFF
160 out_pos = out_pos + 1
161 }
162 if got >= 8 {
163 out[out_pos] = ((q6 << 5) | q7) & 0xFF
164 out_pos = out_pos + 1
165 }
166 }
167 return out_pos
168}
169
170// Compile-only smoke: "foobar" (6 bytes) -> "MZXW6YTBOI======" (16 chars).
171func main() -> i64 {
172 let input: *u8 = "foobar"
173 let out: *u8 = sys_mmap(32)
174 let n: i64 = base32_encode(input, 6, out)
175 if n != 16 { return 1 }
176 if out[0] != 0x4D { return 2 } // 'M'
177 return 0
178}