code wiki / (root) / nx_base32.nx

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}