code wiki / (root) / base64.nx

base64.nx source

↩ module page · 163 lines · 5967 B

1// base64.nx -- RFC 4648 base64 encoder + decoder. 2// 3// Used for: 4// - PEM decoding of X.509 certs (thin ASCII wrapper around DER) 5// - TLS 1.3 pre-shared key encoding 6// - HTTP Basic auth, OAuth tokens, JWT 7// - Web content (data: URIs, JSON-embedded bytes) 8// 9// Standard alphabet (RFC 4648 §4): 10// 0-25 : A-Z 11// 26-51 : a-z 12// 52-61 : 0-9 13// 62 : + 14// 63 : / 15// pad : = 16// 17// URL-safe alphabet variant (§5) swaps +/ for -_; provided as 18// b64url_encode / b64url_decode. 19// 20// Invariants: 21// B1 Input/output lengths are predictable: 22// encode(n bytes) -> 4 * ceil(n / 3) chars 23// decode(n chars) -> 3 * (n / 4) - padding bytes 24// B2 Decoder rejects invalid input (non-alphabet chars) by 25// returning a negative length. No silent skip. 26// B3 Decoder is tolerant of missing padding (RFC 4648 §3.2 27// permits this as "unpadded" variant). 28// B4 Encoder is deterministic; same input -> same output. No 29// trailing whitespace, no line breaks inserted. Callers 30// that want MIME-style 76-char wrap do it outside. 31 32import "syscalls.nx" 33 34const B64_PAD: i64 = 0x3D // '=' 35 36// Encode one 6-bit index to an ASCII char. Standard alphabet. 37func b64_enc_char(n: i64) -> i64 { 38 let v: i64 = n & 0x3F 39 if v < 26 { return 0x41 + v } // 'A'..'Z' 40 if v < 52 { return 0x61 + (v - 26) } // 'a'..'z' 41 if v < 62 { return 0x30 + (v - 52) } // '0'..'9' 42 if v == 62 { return 0x2B } // '+' 43 return 0x2F // '/' 44} 45 46// URL-safe variant: replace + / with - _ 47func b64url_enc_char(n: i64) -> i64 { 48 let v: i64 = n & 0x3F 49 if v < 26 { return 0x41 + v } 50 if v < 52 { return 0x61 + (v - 26) } 51 if v < 62 { return 0x30 + (v - 52) } 52 if v == 62 { return 0x2D } // '-' 53 return 0x5F // '_' 54} 55 56// Decode one ASCII char to 6-bit value; returns -1 if invalid. 57// Accepts either standard (+,/) or URL-safe (-,_) variants. 58func b64_dec_char(c: i64) -> i64 { 59 if c >= 0x41 { if c <= 0x5A { return c - 0x41 } } // A-Z 60 if c >= 0x61 { if c <= 0x7A { return c - 0x61 + 26 } } // a-z 61 if c >= 0x30 { if c <= 0x39 { return c - 0x30 + 52 } } // 0-9 62 if c == 0x2B { return 62 } // + 63 if c == 0x2F { return 63 } // / 64 if c == 0x2D { return 62 } // - (URL-safe) 65 if c == 0x5F { return 63 } // _ (URL-safe) 66 return -1 67} 68 69// Encode `n` bytes from `in_bytes` to `out`; returns written length. 70// Output size: 4 * ceil(n / 3) chars. Pads with '=' to full groups. 71func b64_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 { 72 var pos: i64 = 0 73 var out_pos: i64 = 0 74 while pos + 3 <= n { 75 let b0: i64 = in_bytes[pos] 76 let b1: i64 = in_bytes[pos + 1] 77 let b2: i64 = in_bytes[pos + 2] 78 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F) 79 out[out_pos + 1] = b64_enc_char(((b0 << 4) | (b1 >> 4)) & 0x3F) 80 out[out_pos + 2] = b64_enc_char(((b1 << 2) | (b2 >> 6)) & 0x3F) 81 out[out_pos + 3] = b64_enc_char(b2 & 0x3F) 82 pos = pos + 3 83 out_pos = out_pos + 4 84 } 85 let remain: i64 = n - pos 86 if remain == 1 { 87 let b0: i64 = in_bytes[pos] 88 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F) 89 out[out_pos + 1] = b64_enc_char((b0 << 4) & 0x3F) 90 out[out_pos + 2] = B64_PAD 91 out[out_pos + 3] = B64_PAD 92 out_pos = out_pos + 4 93 } 94 if remain == 2 { 95 let b0: i64 = in_bytes[pos] 96 let b1: i64 = in_bytes[pos + 1] 97 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F) 98 out[out_pos + 1] = b64_enc_char(((b0 << 4) | (b1 >> 4)) & 0x3F) 99 out[out_pos + 2] = b64_enc_char((b1 << 2) & 0x3F) 100 out[out_pos + 3] = B64_PAD 101 out_pos = out_pos + 4 102 } 103 return out_pos 104} 105 106// Grab one sextet: return 0..63 on valid, -1 on '=' or end-of-input, 107// -2 on any other invalid char. Advances *pos on success. 108func b64_grab(in_chars: *u8, n: i64, pos: *i64) -> i64 { 109 let p: i64 = *pos 110 if p >= n { return -1 } 111 let c: i64 = in_chars[p] 112 if c == B64_PAD { 113 *pos = n + 1 114 return -1 115 } 116 let v: i64 = b64_dec_char(c) 117 if v < 0 { return -2 } 118 *pos = p + 1 119 return v 120} 121 122// Decode `n` base64 chars into raw bytes. Returns bytes written on 123// success or -1 on invalid input. Tolerant of missing padding (B3). 124// Whitespace is NOT skipped. 125func b64_decode(in_chars: *u8, n: i64, out: *u8) -> i64 { 126 let pos_raw: *u8 = sys_mmap(16) 127 let pos_p: *i64 = pos_raw as *i64 128 *pos_p = 0 129 var out_pos: i64 = 0 130 while *pos_p < n { 131 let s0: i64 = b64_grab(in_chars, n, pos_p) 132 if s0 == -2 { return -1 } 133 if s0 < 0 { return out_pos } 134 let s1: i64 = b64_grab(in_chars, n, pos_p) 135 if s1 == -2 { return -1 } 136 if s1 < 0 { return -1 } // single lonely char invalid 137 out[out_pos] = ((s0 << 2) | (s1 >> 4)) & 0xFF 138 out_pos = out_pos + 1 139 let s2: i64 = b64_grab(in_chars, n, pos_p) 140 if s2 == -2 { return -1 } 141 if s2 < 0 { return out_pos } 142 out[out_pos] = ((s1 << 4) | (s2 >> 2)) & 0xFF 143 out_pos = out_pos + 1 144 let s3: i64 = b64_grab(in_chars, n, pos_p) 145 if s3 == -2 { return -1 } 146 if s3 < 0 { return out_pos } 147 out[out_pos] = ((s2 << 6) | s3) & 0xFF 148 out_pos = out_pos + 1 149 } 150 return out_pos 151} 152 153// Compile-only smoke. Known answer: encode("foobar") -> "Zm9vYmFy". 154func main() -> i64 { 155 let input: *u8 = "foobar" 156 let encoded: *u8 = sys_mmap(16) 157 let n_enc: i64 = b64_encode(input, 6, encoded) 158 let decoded: *u8 = sys_mmap(16) 159 let n_dec: i64 = b64_decode(encoded, n_enc, decoded) 160 // Round-trip length should match input. 161 if n_dec != 6 { return 1 } 162 return 0 163}