code wiki / _hdl_build / nx_site_auth.nx
nx_site_auth.nx source
↩ module page · 156 lines · 7659 B
1// nx_site_auth.nx -- THE ONE access gate every Nishi site composes (build-once; no per-site auth).
2//
3// module: nishi-core.hub.site_auth
4// capability: CORE_COMPUTE (the unified per-request access wall for ALL sites)
5//
6// Operator (2026-06-16): "lets have that be how all the sites work" -- the canonical Modern Auth
7// (NISHI_MODERN_AUTH_CHARTER: OPAQUE-3DH + Argon2id KSF + BIP39 recovery + NO-COOKIE Ed25519 session)
8// is the SINGLE access method ecosystem-wide. This is the realm-agnostic hot path: extract the
9// X-Nishi-Session token (base64 152B, NEVER a cookie -- C1 cardinal), validate it against the route's
10// NxAuthContext via nx_modern_auth_validate_session. Returns NX_MAUTH_OK to ALLOW, a negative NX_MAUTH_*
11// code to DENY (NO_SESSION / INVALID_SESSION / EXPIRED / REALM_MISMATCH). Every site daemon (wiki admin,
12// /status, andelinwest CMS, future sites) calls THIS for every protected request -- one gate, one auth.
13// Composes (no duplicate primitives): nx_modern_auth_flow + nx_http_header_find + nx_base64. license_tier: ORIGINAL
14
15import "nx_syscalls.nx"
16import "nx_http_header_find.nx"
17import "nx_base64.nx"
18import "hub/nx_modern_auth_flow.nx"
19
20// Validate the no-cookie session token in req's headers against `ctx`. headers_end = byte length to
21// scan for the header (the request length, or the CRLFCRLF offset). now_s = wall clock (TTL check).
22// ALLOW iff == NX_MAUTH_OK. The caller emits 200/302 on allow, 401 ("POST passphrase to <realm>/login")
23// on deny -- NEVER a Set-Cookie, NEVER a WWW-Authenticate Basic.
24func nx_sa_validate(ctx: *NxAuthContext, req: *u8, headers_end: i64, now_s: i64) -> i64 {
25 let off: *i64 = sys_mmap(8) as *i64
26 let ln: *i64 = sys_mmap(8) as *i64
27 off[0] = 0
28 ln[0] = 0
29 if nx_http_header_find(req, headers_end, "X-Nishi-Session" as *u8, 15, off, ln) != NXHF_FOUND {
30 return 0 - NX_MAUTH_NO_SESSION
31 }
32 if ln[0] < 1 { return 0 - NX_MAUTH_NO_SESSION }
33 let tok: *u8 = sys_mmap(256)
34 let tok_n: i64 = b64_decode(((req as i64) + off[0]) as *u8, ln[0], tok)
35 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES { return 0 - NX_MAUTH_INVALID_SESSION }
36 let uh_n: *i64 = sys_mmap(8) as *i64
37 uh_n[0] = 0
38 return nx_modern_auth_validate_session(ctx, tok, tok_n, now_s, 0 as *u8, 0, uh_n)
39}
40
41// Same wall, but on ALLOW also writes the authenticated user's HANDLE into handle_out (cap handle_cap) and its
42// length into handle_n -- so the caller can map handle -> permission level (nx_authz authz_level_of) and gate by
43// LEVEL (e.g. the /gallery NSFW section). Returns NX_MAUTH_OK to allow, negative NX_MAUTH_* to deny.
44func nx_sa_validate_handle(ctx: *NxAuthContext, req: *u8, headers_end: i64, now_s: i64, handle_out: *u8, handle_cap: i64, handle_n: *i64) -> i64 {
45 let off: *i64 = sys_mmap(8) as *i64
46 let ln: *i64 = sys_mmap(8) as *i64
47 off[0] = 0
48 ln[0] = 0
49 handle_n[0] = 0
50 if nx_http_header_find(req, headers_end, "X-Nishi-Session" as *u8, 15, off, ln) != NXHF_FOUND {
51 return 0 - NX_MAUTH_NO_SESSION
52 }
53 if ln[0] < 1 { return 0 - NX_MAUTH_NO_SESSION }
54 let tok: *u8 = sys_mmap(256)
55 let tok_n: i64 = b64_decode(((req as i64) + off[0]) as *u8, ln[0], tok)
56 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES { return 0 - NX_MAUTH_INVALID_SESSION }
57 return nx_modern_auth_validate_session(ctx, tok, tok_n, now_s, handle_out, handle_cap, handle_n)
58}
59
60// ===== NISHI-FIRST no-cookie QUERY session (2026-07-05 doctrine: nishi os/browser first; JS = the
61// third-party last-mile shim). The no-cookie C1 cardinal is PRESERVED -- no Set-Cookie ever. The Nishi
62// browser renders + follows same-portal LINKS today (no fetch/JS needed), so a no-JS surface carries the
63// Ed25519 session token in the `?s=` query of its own links (short-TTL, realm-scoped, SAME validator as
64// the header path). One shared home so docportal/mail/siteedit/every-future-surface inherit it (DRY). =====
65
66// hex nibble or -1
67func sa_hexval(c: i64) -> i64 {
68 if c >= 48 { if c <= 57 { return c - 48 } }
69 if c >= 97 { if c <= 102 { return c - 87 } }
70 if c >= 65 { if c <= 70 { return c - 55 } }
71 return 0 - 1
72}
73
74// percent-DECODE src[0..n) into out (cap-bounded). Our encoder emits %XX for + / = and passes alnum
75// through, so %XX-only decode is exact (a literal char is copied). Returns decoded length.
76func nx_sa_pct_decode(src: *u8, n: i64, out: *u8, cap: i64) -> i64 {
77 var i: i64 = 0
78 var o: i64 = 0
79 while i < n {
80 if o >= cap { return o }
81 let c: i64 = src[i] as i64
82 var handled: i64 = 0
83 if c == 37 {
84 if i + 2 < n {
85 let hi: i64 = sa_hexval(src[i+1] as i64)
86 let lo: i64 = sa_hexval(src[i+2] as i64)
87 if hi >= 0 { if lo >= 0 {
88 let b: i64 = hi * 16 + lo
89 out[o] = b as u8
90 o = o + 1
91 i = i + 3
92 handled = 1
93 } }
94 }
95 }
96 if handled == 0 { out[o] = c as u8; o = o + 1; i = i + 1 }
97 }
98 return o
99}
100
101// percent-ENCODE a base64 token src[0..n) into out starting at `start` ('+' '/' '=' -> %2B %2F %3D,
102// alnum passes through). Returns the new offset. Use to build a same-portal link: .../ui?s=<here>.
103func nx_sa_tok_urlenc(src: *u8, n: i64, out: *u8, start: i64) -> i64 {
104 var o: i64 = start
105 var i: i64 = 0
106 while i < n {
107 let c: i64 = src[i] as i64
108 if c == 43 { out[o] = 37 as u8; out[o+1] = 50 as u8; out[o+2] = 66 as u8; o = o + 3 }
109 else { if c == 47 { out[o] = 37 as u8; out[o+1] = 50 as u8; out[o+2] = 70 as u8; o = o + 3 }
110 else { if c == 61 { out[o] = 37 as u8; out[o+1] = 51 as u8; out[o+2] = 68 as u8; o = o + 3 }
111 else { out[o] = c as u8; o = o + 1 } } }
112 i = i + 1
113 }
114 return o
115}
116
117// find the RAW (still percent-encoded) value of s= in a path's query (path includes ?query). Writes the
118// value offset to voff[0]; returns value length (0 if no s= param). Boundary: s= must follow ? or &.
119func nx_sa_qs_raw(path: *u8, pn: i64, voff: *i64) -> i64 {
120 voff[0] = 0
121 var q: i64 = 0
122 while q < pn { if (path[q] as i64) == 63 { break } q = q + 1 }
123 if q >= pn { return 0 }
124 var i: i64 = q
125 while i + 2 < pn {
126 var boundary: i64 = 0
127 if (path[i] as i64) == 63 { boundary = 1 }
128 if (path[i] as i64) == 38 { boundary = 1 }
129 if boundary == 1 { if (path[i+1] as i64) == 115 { if (path[i+2] as i64) == 61 {
130 let vs: i64 = i + 3
131 var ve: i64 = vs
132 while ve < pn { if (path[ve] as i64) == 38 { break } ve = ve + 1 }
133 voff[0] = vs
134 return ve - vs
135 } } }
136 i = i + 1
137 }
138 return 0
139}
140
141// Validate the no-cookie session carried in `?s=` of the request path (same Ed25519 validator as the
142// header path). Writes the authenticated handle. NX_MAUTH_OK to allow, negative NX_MAUTH_* to deny.
143func nx_sa_validate_qs(ctx: *NxAuthContext, path: *u8, pn: i64, now_s: i64, handle_out: *u8, handle_cap: i64, handle_n: *i64) -> i64 {
144 handle_n[0] = 0
145 let voff: *i64 = sys_mmap(8) as *i64
146 let vlen: i64 = nx_sa_qs_raw(path, pn, voff)
147 if vlen < 1 { return 0 - NX_MAUTH_NO_SESSION }
148 let raw: *u8 = ((path as i64) + voff[0]) as *u8
149 let dec: *u8 = sys_mmap(512)
150 let dn: i64 = nx_sa_pct_decode(raw, vlen, dec, 511)
151 if dn < 1 { return 0 - NX_MAUTH_NO_SESSION }
152 let tok: *u8 = sys_mmap(256)
153 let tok_n: i64 = b64_decode(dec, dn, tok)
154 if tok_n != NX_MAUTH_SESSION_TOKEN_BYTES { return 0 - NX_MAUTH_INVALID_SESSION }
155 return nx_modern_auth_validate_session(ctx, tok, tok_n, now_s, handle_out, handle_cap, handle_n)
156}