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}