nx_base64.nx source
↩ module page · 179 lines · 6829 B
1// base64.nx -- RFC 4648 base64 encoder + decoder.
2//
3// Canonical: this is the substrate-wide canonical Base64 (RFC 4648
4// §4 standard alphabet) per [[feedback-no-tool-proliferation-bit-
5// level]]. Variants that need URL-safe alphabet (RFC 4648 §5) are
6// candidates for ONE distinct sibling primitive nx_base64_url.nx
7// (queued) that imports THIS file's encode/decode skeleton; all
8// other consumers compose THIS file's encode/decode primitives.
9// Re-implementing the Base64 alphabet or quantum-loop inline is
10// refused.
11//
12// Used for:
13// - PEM decoding of X.509 certs (thin ASCII wrapper around DER)
14// - TLS 1.3 pre-shared key encoding
15// - HTTP Basic auth, OAuth tokens, JWT
16// - Web content (data: URIs, JSON-embedded bytes)
17//
18// Standard alphabet (RFC 4648 §4):
19// 0-25 : A-Z
20// 26-51 : a-z
21// 52-61 : 0-9
22// 62 : +
23// 63 : /
24// pad : =
25//
26// URL-safe alphabet variant (§5) swaps +/ for -_; provided as
27// b64url_encode / b64url_decode.
28//
29// Invariants:
30// B1 Input/output lengths are predictable:
31// encode(n bytes) -> 4 * ceil(n / 3) chars
32// decode(n chars) -> 3 * (n / 4) - padding bytes
33// B2 Decoder rejects invalid input (non-alphabet chars) by
34// returning a negative length. No silent skip.
35// B3 Decoder is tolerant of missing padding (RFC 4648 §3.2
36// permits this as "unpadded" variant).
37// B4 Encoder is deterministic; same input -> same output. No
38// trailing whitespace, no line breaks inserted. Callers
39// that want MIME-style 76-char wrap do it outside.
40//
41// license_tier: INDEPENDENT_REDERIVE
42// genealogy_id: international-research-sources/ietf/rfc_8446
43//
44
45// nx_safety_envelope:
46// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
47// sil_target: SIL1
48// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
49// verdict: NOT_YET_EVALUATED
50//
51// F-meta-4 refactor 2026-05-17: removed `import "nx_syscalls.nx"`.
52// This file is pure -- all functions operate on caller-provided
53// buffers + stack-local counters. Composing with any syscall layer
54// (nx_syscalls.nx OR nx_syscalls_x86_64.nx) no longer hits the
55// duplicate-symbol bug class. Unblocks crypto chain (nx_jwt ->
56// nx_jose / jwk / acme / csr) for x86_64 native runtime testing.
57
58const B64_PAD: i64 = 0x3D // '='
59
60// Encode one 6-bit index to an ASCII char. Standard alphabet.
61func b64_enc_char(n: i64) -> i64 {
62 let v: i64 = n & 0x3F
63 if v < 26 { return 0x41 + v } // 'A'..'Z'
64 if v < 52 { return 0x61 + (v - 26) } // 'a'..'z'
65 if v < 62 { return 0x30 + (v - 52) } // '0'..'9'
66 if v == 62 { return 0x2B } // '+'
67 return 0x2F // '/'
68}
69
70// URL-safe variant: replace + / with - _
71func b64url_enc_char(n: i64) -> i64 {
72 let v: i64 = n & 0x3F
73 if v < 26 { return 0x41 + v }
74 if v < 52 { return 0x61 + (v - 26) }
75 if v < 62 { return 0x30 + (v - 52) }
76 if v == 62 { return 0x2D } // '-'
77 return 0x5F // '_'
78}
79
80// Decode one ASCII char to 6-bit value; returns -1 if invalid.
81// Accepts either standard (+,/) or URL-safe (-,_) variants.
82func b64_dec_char(c: i64) -> i64 {
83 if c >= 0x41 { if c <= 0x5A { return c - 0x41 } } // A-Z
84 if c >= 0x61 { if c <= 0x7A { return c - 0x61 + 26 } } // a-z
85 if c >= 0x30 { if c <= 0x39 { return c - 0x30 + 52 } } // 0-9
86 if c == 0x2B { return 62 } // +
87 if c == 0x2F { return 63 } // /
88 if c == 0x2D { return 62 } // - (URL-safe)
89 if c == 0x5F { return 63 } // _ (URL-safe)
90 return -1
91}
92
93// Encode `n` bytes from `in_bytes` to `out`; returns written length.
94// Output size: 4 * ceil(n / 3) chars. Pads with '=' to full groups.
95func b64_encode(in_bytes: *u8, n: i64, out: *u8) -> i64 {
96 var pos: i64 = 0
97 var out_pos: i64 = 0
98 while pos + 3 <= n {
99 let b0: i64 = in_bytes[pos]
100 let b1: i64 = in_bytes[pos + 1]
101 let b2: i64 = in_bytes[pos + 2]
102 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F)
103 out[out_pos + 1] = b64_enc_char(((b0 << 4) | (b1 >> 4)) & 0x3F)
104 out[out_pos + 2] = b64_enc_char(((b1 << 2) | (b2 >> 6)) & 0x3F)
105 out[out_pos + 3] = b64_enc_char(b2 & 0x3F)
106 pos = pos + 3
107 out_pos = out_pos + 4
108 }
109 let remain: i64 = n - pos
110 if remain == 1 {
111 let b0: i64 = in_bytes[pos]
112 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F)
113 out[out_pos + 1] = b64_enc_char((b0 << 4) & 0x3F)
114 out[out_pos + 2] = B64_PAD
115 out[out_pos + 3] = B64_PAD
116 out_pos = out_pos + 4
117 }
118 if remain == 2 {
119 let b0: i64 = in_bytes[pos]
120 let b1: i64 = in_bytes[pos + 1]
121 out[out_pos + 0] = b64_enc_char((b0 >> 2) & 0x3F)
122 out[out_pos + 1] = b64_enc_char(((b0 << 4) | (b1 >> 4)) & 0x3F)
123 out[out_pos + 2] = b64_enc_char((b1 << 2) & 0x3F)
124 out[out_pos + 3] = B64_PAD
125 out_pos = out_pos + 4
126 }
127 return out_pos
128}
129
130// Grab one sextet: return 0..63 on valid, -1 on '=' or end-of-input,
131// -2 on any other invalid char. Advances *pos on success.
132func b64_grab(in_chars: *u8, n: i64, pos: *i64) -> i64 {
133 let p: i64 = *pos
134 if p >= n { return -1 }
135 let c: i64 = in_chars[p]
136 if c == B64_PAD {
137 *pos = n + 1
138 return -1
139 }
140 let v: i64 = b64_dec_char(c)
141 if v < 0 { return -2 }
142 *pos = p + 1
143 return v
144}
145
146// Decode `n` base64 chars into raw bytes. Returns bytes written on
147// success or -1 on invalid input. Tolerant of missing padding (B3).
148// Whitespace is NOT skipped.
149//
150// F-meta-4 refactor: position counter is a stack-local; no sys_mmap.
151func b64_decode(in_chars: *u8, n: i64, out: *u8) -> i64 {
152 var pos: i64 = 0
153 var out_pos: i64 = 0
154 while pos < n {
155 let s0: i64 = b64_grab(in_chars, n, &pos)
156 if s0 == -2 { return -1 }
157 if s0 < 0 { return out_pos }
158 let s1: i64 = b64_grab(in_chars, n, &pos)
159 if s1 == -2 { return -1 }
160 if s1 < 0 { return -1 } // single lonely char invalid
161 out[out_pos] = ((s0 << 2) | (s1 >> 4)) & 0xFF
162 out_pos = out_pos + 1
163 let s2: i64 = b64_grab(in_chars, n, &pos)
164 if s2 == -2 { return -1 }
165 if s2 < 0 { return out_pos }
166 out[out_pos] = ((s1 << 4) | (s2 >> 2)) & 0xFF
167 out_pos = out_pos + 1
168 let s3: i64 = b64_grab(in_chars, n, &pos)
169 if s3 == -2 { return -1 }
170 if s3 < 0 { return out_pos }
171 out[out_pos] = ((s2 << 6) | s3) & 0xFF
172 out_pos = out_pos + 1
173 }
174 return out_pos
175}
176
177// Self-test main() removed by F-meta-4 refactor (used sys_mmap; this
178// file is now syscall-free). Round-trip smoke lives in a separate
179// nx_base64_test.nx that imports a caller-chosen syscall layer.