nx_base32.nx source
↩ module page · 225 lines · 8846 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// 2026-08-01 -- NEW CAPABILITY: RFC 4648 section 7 "base32hex" (the Extended Hex Alphabet).
47//
48// RFC 4648 publishes SEVEN base32hex test vectors in section 10 and this tree had NO base32hex encoder at
49// all -- only the standard alphabet above. That is a CAPABILITY GAP, not a defect: nothing was computing
50// base32hex wrongly, there was simply nothing to compute it.
51//
52// * IMPLEMENTED BY ALPHABET TRANSLATION OVER THE PROVEN ENCODER, NOT BY COPYING IT. base32 and base32hex
53// differ ONLY in the 32-character alphabet (RFC 4648 sec 6 vs sec 7); the bit-packing, grouping and
54// padding are identical. So this runs the existing base32_encode -- already GREEN 7/7 against RFC 4648
55// section 10 -- and then remaps each output character through its 5-bit value.
56// A hand-copied second bit-packer would have duplicated the single most error-prone part of the encoder
57// for zero benefit. * WHEN TWO ENCODINGS DIFFER ONLY IN THEIR ALPHABET, TRANSLATE THE ALPHABET -- DO NOT
58// FORK THE BIT LOGIC (Rule 15, and the reason nine copies of nx_body_off exist in this tree).
59//
60// ADDITIVE (Rule 19): base32_encode keeps its exact signature and behaviour; the existing base32 gate is
61// the regression check for this change.
62// Extended Hex Alphabet: '0'-'9' then 'A'-'V'. Note it is UPPERCASE, like base16 (RFC 4648 sec 8) --
63// and unlike our hex_encode, which is lowercase by its own separate contract.
64func b32hex_enc_char(v: i64) -> i64 {
65 if v < 10 { return 0x30 + v } // '0'..'9'
66 return 0x41 + (v - 10) // 'A'..'V'
67}
68
69func base32hex_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
70 let len: i64 = base32_encode(in_bytes, n, out)
71 var i: i64 = 0
72 while i < len {
73 let c: i64 = out[i] as i64
74 if c != B32_PAD {
75 let v: i64 = b32_dec_val(c)
76 // b32_dec_val is case-insensitive and base32_encode emits uppercase, so this cannot fail for
77 // well-formed output. Checked anyway: a silent -1 would become character 0x2F ('/'), which is
78 // in NEITHER alphabet and would produce a plausible-looking wrong string.
79 if v < 0 { return B32_ERR_BAD }
80 out[i] = b32hex_enc_char(v) as u8
81 }
82 i = i + 1
83 }
84 return len
85}
86
87// Encode n bytes to base32. Output length = ceil(n/5) * 8.
88func base32_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
89 var pos: i64 = 0
90 var out_pos: i64 = 0
91 while pos + 5 <= n {
92 let b0: i64 = in_bytes[pos]
93 let b1: i64 = in_bytes[pos + 1]
94 let b2: i64 = in_bytes[pos + 2]
95 let b3: i64 = in_bytes[pos + 3]
96 let b4: i64 = in_bytes[pos + 4]
97 out[out_pos + 0] = b32_enc_char((b0 >> 3) & 0x1F)
98 out[out_pos + 1] = b32_enc_char(((b0 << 2) | (b1 >> 6)) & 0x1F)
99 out[out_pos + 2] = b32_enc_char((b1 >> 1) & 0x1F)
100 out[out_pos + 3] = b32_enc_char(((b1 << 4) | (b2 >> 4)) & 0x1F)
101 out[out_pos + 4] = b32_enc_char(((b2 << 1) | (b3 >> 7)) & 0x1F)
102 out[out_pos + 5] = b32_enc_char((b3 >> 2) & 0x1F)
103 out[out_pos + 6] = b32_enc_char(((b3 << 3) | (b4 >> 5)) & 0x1F)
104 out[out_pos + 7] = b32_enc_char(b4 & 0x1F)
105 pos = pos + 5
106 out_pos = out_pos + 8
107 }
108 let rem: i64 = n - pos
109 if rem > 0 {
110 let b0: i64 = in_bytes[pos]
111 var b1: i64 = 0
112 var b2: i64 = 0
113 var b3: i64 = 0
114 if rem > 1 { b1 = in_bytes[pos + 1] }
115 if rem > 2 { b2 = in_bytes[pos + 2] }
116 if rem > 3 { b3 = in_bytes[pos + 3] }
117 out[out_pos + 0] = b32_enc_char((b0 >> 3) & 0x1F)
118 out[out_pos + 1] = b32_enc_char(((b0 << 2) | (b1 >> 6)) & 0x1F)
119 if rem == 1 {
120 out[out_pos + 2] = B32_PAD
121 out[out_pos + 3] = B32_PAD
122 out[out_pos + 4] = B32_PAD
123 out[out_pos + 5] = B32_PAD
124 out[out_pos + 6] = B32_PAD
125 out[out_pos + 7] = B32_PAD
126 } else {
127 out[out_pos + 2] = b32_enc_char((b1 >> 1) & 0x1F)
128 out[out_pos + 3] = b32_enc_char(((b1 << 4) | (b2 >> 4)) & 0x1F)
129 if rem == 2 {
130 out[out_pos + 4] = B32_PAD
131 out[out_pos + 5] = B32_PAD
132 out[out_pos + 6] = B32_PAD
133 out[out_pos + 7] = B32_PAD
134 } else {
135 out[out_pos + 4] = b32_enc_char(((b2 << 1) | (b3 >> 7)) & 0x1F)
136 if rem == 3 {
137 out[out_pos + 5] = B32_PAD
138 out[out_pos + 6] = B32_PAD
139 out[out_pos + 7] = B32_PAD
140 } else {
141 out[out_pos + 5] = b32_enc_char((b3 >> 2) & 0x1F)
142 out[out_pos + 6] = b32_enc_char((b3 << 3) & 0x1F)
143 out[out_pos + 7] = B32_PAD
144 }
145 }
146 }
147 out_pos = out_pos + 8
148 }
149 return out_pos
150}
151
152// Decode n base32 chars. Returns bytes written or -ERR.
153func base32_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
154 var pos: i64 = 0
155 var out_pos: i64 = 0
156 let q_raw: *u8 = sys_mmap(8)
157 let quintets: *i64 = q_raw as *i64
158 while pos < n {
159 // Fill 8 quintets (or fewer if padding encountered).
160 var got: i64 = 0
161 var k: i64 = 0
162 while k < 8 {
163 quintets[k] = -1
164 k = k + 1
165 }
166 k = 0
167 while k < 8 {
168 if pos >= n { k = 8 }
169 else {
170 let c: i64 = in_chars[pos]
171 if c == B32_PAD { pos = n; k = 8 }
172 else {
173 let v: i64 = b32_dec_val(c)
174 if v < 0 { return B32_ERR_BAD }
175 quintets[got] = v
176 got = got + 1
177 pos = pos + 1
178 k = k + 1
179 }
180 }
181 }
182 // Reconstruct bytes from quintets. got = 2 -> 1 byte,
183 // got = 4 -> 2, got = 5 -> 3, got = 7 -> 4, got = 8 -> 5.
184 if got == 0 { return out_pos }
185 let q0: i64 = quintets[0]
186 let q1: i64 = quintets[1]
187 let q2: i64 = quintets[2]
188 let q3: i64 = quintets[3]
189 let q4: i64 = quintets[4]
190 let q5: i64 = quintets[5]
191 let q6: i64 = quintets[6]
192 let q7: i64 = quintets[7]
193 if got >= 2 {
194 out[out_pos] = ((q0 << 3) | (q1 >> 2)) & 0xFF
195 out_pos = out_pos + 1
196 }
197 if got >= 4 {
198 out[out_pos] = ((q1 << 6) | (q2 << 1) | (q3 >> 4)) & 0xFF
199 out_pos = out_pos + 1
200 }
201 if got >= 5 {
202 out[out_pos] = ((q3 << 4) | (q4 >> 1)) & 0xFF
203 out_pos = out_pos + 1
204 }
205 if got >= 7 {
206 out[out_pos] = ((q4 << 7) | (q5 << 2) | (q6 >> 3)) & 0xFF
207 out_pos = out_pos + 1
208 }
209 if got >= 8 {
210 out[out_pos] = ((q6 << 5) | q7) & 0xFF
211 out_pos = out_pos + 1
212 }
213 }
214 return out_pos
215}
216
217// Compile-only smoke: "foobar" (6 bytes) -> "MZXW6YTBOI======" (16 chars).
218func main() -> i64 {
219 let input: *u8 = "foobar"
220 let out: *u8 = sys_mmap(32)
221 let n: i64 = base32_encode(input, 6, out)
222 if n != 16 { return 1 }
223 if out[0] != 0x4D { return 2 } // 'M'
224 return 0
225}