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}