nx_base32.nx source
↩ module page · 184 lines · 6588 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
21// nx_safety_envelope:
22// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
23// sil_target: SIL1
24// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
25// verdict: NOT_YET_EVALUATED
26
27import "nx_syscalls.nx"
28
29const B32_PAD: i64 = 0x3D // '='
30const B32_ERR_BAD: i64 = -1
31
32// 5-bit value -> uppercase base32 char.
33func b32_enc_char(v: i64) -> i64 {
34 if v < 26 { return 0x41 + v } // 'A'..'Z'
35 return 0x32 + (v - 26) // '2'..'7'
36}
37
38// Base32 char -> 5-bit value, -1 on invalid. Case-insensitive.
39func b32_dec_val(c: i64) -> i64 {
40 if c >= 0x41 { if c <= 0x5A { return c - 0x41 } } // A-Z
41 if c >= 0x61 { if c <= 0x7A { return c - 0x61 } } // a-z
42 if c >= 0x32 { if c <= 0x37 { return c - 0x32 + 26 } } // 2-7
43 return B32_ERR_BAD
44}
45
46// Encode n bytes to base32. Output length = ceil(n/5) * 8.
47func base32_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
48 var pos: i64 = 0
49 var out_pos: i64 = 0
50 while pos + 5 <= n {
51 let b0: i64 = in_bytes[pos]
52 let b1: i64 = in_bytes[pos + 1]
53 let b2: i64 = in_bytes[pos + 2]
54 let b3: i64 = in_bytes[pos + 3]
55 let b4: i64 = in_bytes[pos + 4]
56 out[out_pos + 0] = b32_enc_char((b0 >> 3) & 0x1F)
57 out[out_pos + 1] = b32_enc_char(((b0 << 2) | (b1 >> 6)) & 0x1F)
58 out[out_pos + 2] = b32_enc_char((b1 >> 1) & 0x1F)
59 out[out_pos + 3] = b32_enc_char(((b1 << 4) | (b2 >> 4)) & 0x1F)
60 out[out_pos + 4] = b32_enc_char(((b2 << 1) | (b3 >> 7)) & 0x1F)
61 out[out_pos + 5] = b32_enc_char((b3 >> 2) & 0x1F)
62 out[out_pos + 6] = b32_enc_char(((b3 << 3) | (b4 >> 5)) & 0x1F)
63 out[out_pos + 7] = b32_enc_char(b4 & 0x1F)
64 pos = pos + 5
65 out_pos = out_pos + 8
66 }
67 let rem: i64 = n - pos
68 if rem > 0 {
69 let b0: i64 = in_bytes[pos]
70 var b1: i64 = 0
71 var b2: i64 = 0
72 var b3: i64 = 0
73 if rem > 1 { b1 = in_bytes[pos + 1] }
74 if rem > 2 { b2 = in_bytes[pos + 2] }
75 if rem > 3 { b3 = in_bytes[pos + 3] }
76 out[out_pos + 0] = b32_enc_char((b0 >> 3) & 0x1F)
77 out[out_pos + 1] = b32_enc_char(((b0 << 2) | (b1 >> 6)) & 0x1F)
78 if rem == 1 {
79 out[out_pos + 2] = B32_PAD
80 out[out_pos + 3] = B32_PAD
81 out[out_pos + 4] = B32_PAD
82 out[out_pos + 5] = B32_PAD
83 out[out_pos + 6] = B32_PAD
84 out[out_pos + 7] = B32_PAD
85 } else {
86 out[out_pos + 2] = b32_enc_char((b1 >> 1) & 0x1F)
87 out[out_pos + 3] = b32_enc_char(((b1 << 4) | (b2 >> 4)) & 0x1F)
88 if rem == 2 {
89 out[out_pos + 4] = B32_PAD
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 + 4] = b32_enc_char(((b2 << 1) | (b3 >> 7)) & 0x1F)
95 if rem == 3 {
96 out[out_pos + 5] = B32_PAD
97 out[out_pos + 6] = B32_PAD
98 out[out_pos + 7] = B32_PAD
99 } else {
100 out[out_pos + 5] = b32_enc_char((b3 >> 2) & 0x1F)
101 out[out_pos + 6] = b32_enc_char((b3 << 3) & 0x1F)
102 out[out_pos + 7] = B32_PAD
103 }
104 }
105 }
106 out_pos = out_pos + 8
107 }
108 return out_pos
109}
110
111// Decode n base32 chars. Returns bytes written or -ERR.
112func base32_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
113 var pos: i64 = 0
114 var out_pos: i64 = 0
115 let q_raw: *u8 = sys_mmap(8)
116 let quintets: *i64 = q_raw as *i64
117 while pos < n {
118 // Fill 8 quintets (or fewer if padding encountered).
119 var got: i64 = 0
120 var k: i64 = 0
121 while k < 8 {
122 quintets[k] = -1
123 k = k + 1
124 }
125 k = 0
126 while k < 8 {
127 if pos >= n { k = 8 }
128 else {
129 let c: i64 = in_chars[pos]
130 if c == B32_PAD { pos = n; k = 8 }
131 else {
132 let v: i64 = b32_dec_val(c)
133 if v < 0 { return B32_ERR_BAD }
134 quintets[got] = v
135 got = got + 1
136 pos = pos + 1
137 k = k + 1
138 }
139 }
140 }
141 // Reconstruct bytes from quintets. got = 2 -> 1 byte,
142 // got = 4 -> 2, got = 5 -> 3, got = 7 -> 4, got = 8 -> 5.
143 if got == 0 { return out_pos }
144 let q0: i64 = quintets[0]
145 let q1: i64 = quintets[1]
146 let q2: i64 = quintets[2]
147 let q3: i64 = quintets[3]
148 let q4: i64 = quintets[4]
149 let q5: i64 = quintets[5]
150 let q6: i64 = quintets[6]
151 let q7: i64 = quintets[7]
152 if got >= 2 {
153 out[out_pos] = ((q0 << 3) | (q1 >> 2)) & 0xFF
154 out_pos = out_pos + 1
155 }
156 if got >= 4 {
157 out[out_pos] = ((q1 << 6) | (q2 << 1) | (q3 >> 4)) & 0xFF
158 out_pos = out_pos + 1
159 }
160 if got >= 5 {
161 out[out_pos] = ((q3 << 4) | (q4 >> 1)) & 0xFF
162 out_pos = out_pos + 1
163 }
164 if got >= 7 {
165 out[out_pos] = ((q4 << 7) | (q5 << 2) | (q6 >> 3)) & 0xFF
166 out_pos = out_pos + 1
167 }
168 if got >= 8 {
169 out[out_pos] = ((q6 << 5) | q7) & 0xFF
170 out_pos = out_pos + 1
171 }
172 }
173 return out_pos
174}
175
176// Compile-only smoke: "foobar" (6 bytes) -> "MZXW6YTBOI======" (16 chars).
177func main() -> i64 {
178 let input: *u8 = "foobar"
179 let out: *u8 = sys_mmap(32)
180 let n: i64 = base32_encode(input, 6, out)
181 if n != 16 { return 1 }
182 if out[0] != 0x4D { return 2 } // 'M'
183 return 0
184}