code wiki / (root) / base32.nx

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}