code wiki / (root) / nx_base32.nx

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}