code wiki / (root) / nx_opaque_login_routes.nx

nx_opaque_login_routes.nx source

↩ module page · 187 lines · 17415 B

1// nx_opaque_login_routes.nx -- the SHARED route table for the no-cookie OPAQUE login. Builds a full HTTP 2// response into a caller buffer (NOT a socket write), so BOTH transports reuse one table (rule 15 DRY): 3// - plain HTTP daemon: sys_read -> olg_route -> sys_write 4// - TLS daemon: tls handshake -> app_recv -> olg_route -> app_send 5// Routes: GET /login (page) | POST /login | GET /whoami | GET /audio (session-gated) | POST /register. 6// No shortcuts: full OPAQUE, every reject path returns a clean status, no token leak on failure. 7import "nx_opaque_login.nx" // olg_ctx_setup / olg_register / olg_login / olg_whoami + NxAuthContext + NX_MAUTH_* 8import "nx_http_form.nx" // nx_http_form_get_field 9import "nx_audio_web.nx" // nx_audio_web_page -- the protected sovereign-audio page 10import "nx_finance_web.nx" // fw_render -- the protected sovereign zero-JS finance dashboard (nishifamily.com/finance) 11import "nx_intake_form.nx" // if_render -- the zero-JS intake form (GET /finance/intake) 12import "nx_intake_handler.nx" // ih_record -- parse the submitted form -> sv_record (POST /finance/intake) 13import "nx_lan_signup.nx" // ls_signup_allowed -- LAN-AND-INVITED signup predicate (lifted to runtime/ 2026-07-31) 14import "nx_bills_entry.nx" // be_record -- parse a submitted bill -> bs_add (POST /finance/bills) 15const OLR_MAGIC_2026: i64 = 2026 16const OLR_MAGIC_131072: i64 = 131072 17 18func olr_slen(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} return n } 19func olr_find(buf: *u8, n: i64, needle: *u8, nl: i64) -> i64 { 20 if nl==0 { return 0 } 21 var i: i64=0 22 while i+nl<=n { var j: i64=0; var ok: i64=1; while j<nl { if buf[i+j]!=needle[j]{ok=0; j=nl} else {j=j+1} } if ok==1 {return i} i=i+1 } 23 return 0-1 24} 25func olr_starts(buf: *u8, n: i64, pre: *u8) -> i64 { var i: i64=0; while pre[i]!=(0 as u8){ if i>=n {return 0} if buf[i]!=pre[i]{return 0} i=i+1 } return 1 } 26func olr_cat(dst: *u8, off: i64, s: *u8) -> i64 { var o: i64=off; var i: i64=0; while s[i]!=(0 as u8){dst[o]=s[i]; o=o+1; i=i+1} return o } 27func olr_catb(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { var o: i64=off; var i: i64=0; while i<n {dst[o]=src[i]; o=o+1; i=i+1} return o } 28func olr_itoa(dst: *u8, off: i64, v: i64) -> i64 { let t: *u8=sys_mmap(28); var m: i64=v; var k: i64=0; if m==0{t[0]=48 as u8;k=1} while m>0{t[k]=(48+(m%10)) as u8;m=m/10;k=k+1} var o: i64=off; var q: i64=k-1; while q>=0{dst[o]=t[q];o=o+1;q=q-1} return o } 29func olr_hex(dst: *u8, off: i64, src: *u8, n: i64) -> i64 { let hx: *u8="0123456789abcdef" as *u8; var o: i64=off; var i: i64=0; while i<n { let c: i64=(src[i] as i64)&0xff; dst[o]=hx[(c>>4)&15]; dst[o+1]=hx[c&15]; o=o+2; i=i+1 } return o } 30func olr_hdr_val(req: *u8, hend: i64, name: *u8, nl: i64, out: *u8, cap: i64) -> i64 { 31 let p: i64 = olr_find(req, hend, name, nl) 32 if p < 0 { return 0 } 33 var i: i64 = p + nl 34 if i < hend { if req[i]==(32 as u8) { i=i+1 } } 35 var o: i64 = 0 36 while i < hend { let c: u8 = req[i]; if c==(13 as u8){i=hend} else { if c==(10 as u8){i=hend} else { if o<cap-1 {out[o]=c; o=o+1} i=i+1 } } } 37 out[o]=0 as u8; return o 38} 39// build a full HTTP/1.1 response into buf; return total length. status="200 OK", ctype="application/json". 40func olr_resp(buf: *u8, status: *u8, ctype: *u8, body: *u8, blen: i64) -> i64 { 41 var o: i64 = 0 42 o = olr_cat(buf, o, "HTTP/1.1 " as *u8); o = olr_cat(buf, o, status) 43 o = olr_cat(buf, o, "\r\nContent-Type: " as *u8); o = olr_cat(buf, o, ctype) 44 o = olr_cat(buf, o, "\r\nContent-Length: " as *u8); o = olr_itoa(buf, o, blen) 45 o = olr_cat(buf, o, "\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 46 o = olr_catb(buf, o, body, blen) 47 return o 48} 49 50// 303 See Other -> turn a POST into a GET (post-redirect-get): refreshing the result won't re-submit. 51func olr_resp303(buf: *u8, location: *u8) -> i64 { 52 var o: i64 = 0 53 o = olr_cat(buf, o, "HTTP/1.1 303 See Other\r\nLocation: " as *u8); o = olr_cat(buf, o, location) 54 o = olr_cat(buf, o, "\r\nContent-Length: 0\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n" as *u8) 55 return o 56} 57 58const OLR_HTML: *u8 = "<!doctype html><html><head><meta charset=utf-8><meta name=viewport content=\"width=device-width,initial-scale=1\"><title>Nishi OPAQUE login</title><style>body{font-family:system-ui,sans-serif;max-width:480px;margin:5vh auto;padding:0 18px;color:#16202e}h1{font-size:1.2rem}h3{margin:18px 0 4px}input{width:100%;padding:9px;margin:5px 0;box-sizing:border-box;border:1px solid #b9c2d6;border-radius:5px}button{padding:9px 16px;margin:6px 6px 6px 0;background:#0b2545;color:#fff;border:0;border-radius:5px;cursor:pointer;font-size:.95rem}#msg{margin:14px 0;padding:12px;background:#f1f4fa;border-left:3px solid #0b2545;word-break:break-all;min-height:1.2em}</style></head><body><h1>Nishi &mdash; no-cookie OPAQUE login</h1><p style=\"color:#667;font-size:.9rem\">Full OPAQUE aPAKE (RFC 9807) over TLS. The server never stores your passphrase; sessions are Ed25519 no-cookie tokens (sessionStorage + X-Nishi-Session header), never cookies.</p><div id=msg>Register a handle + passphrase, then log in.</div><h3>1. Register</h3><input id=rh placeholder=\"handle (e.g. elder)\"><input id=rp type=password placeholder=\"passphrase\"><button onclick=reg()>Register</button><h3>2. Login</h3><input id=lh placeholder=handle><input id=lp type=password placeholder=passphrase><button onclick=login()>Login</button> <button onclick=who()>Who am I?</button> <button onclick=aud()>Open audio</button> <button onclick=out()>Sign out</button><script>function $(i){return document.getElementById(i)}function M(t){$('msg').textContent=t}async function G(p,b){try{var o={method:b?'POST':'GET',headers:{}};if(b){o.headers['Content-Type']='application/x-www-form-urlencoded';o.body=b}else{o.headers['X-Nishi-Session']=sessionStorage.getItem('nsess')||''}var r=await fetch(p,o);var j={};try{j=await r.json()}catch(e){}return{ok:r.ok,s:r.status,j:j}}catch(e){return{ok:false,s:0,j:{error:'network: '+e}}}}async function reg(){M('Registering\\u2026 (memory-hard OPAQUE step \\u2014 a moment)');var r=await G('/register','handle='+encodeURIComponent($('rh').value)+'&pw='+encodeURIComponent($('rp').value));M(r.ok?('Registered! SAVE THIS RECOVERY MNEMONIC: '+r.j.mnemonic):('Register failed ('+r.s+'): '+(r.j.error||'unknown')))}async function login(){M('Logging in\\u2026 (a moment)');var r=await G('/login','handle='+encodeURIComponent($('lh').value)+'&pw='+encodeURIComponent($('lp').value));if(r.ok){sessionStorage.setItem('nsess',r.j.token);var R=new URLSearchParams(location.search).get('return');if(!R&&document.referrer){try{var u=new URL(document.referrer);if(u.origin==location.origin&&u.pathname!='/login'&&u.pathname!='/'){R=u.pathname}}catch(e){}}if(R){location.href=R;return}M('Logged in \\u2014 NO cookie. Token saved ('+r.j.token.length+' chars). Open /torrent or /gallery.')}else{M('Login failed ('+r.s+'): '+(r.j.error||'unknown'))}}async function who(){if(!sessionStorage.getItem('nsess')){M('No session \\u2014 log in first.');return}M('Validating\\u2026');var r=await G('/whoami');M(r.ok?('Valid no-cookie session. user-id hash: '+r.j.uid):('Session rejected ('+r.s+'): '+(r.j.error||'unknown')))}async function aud(){if(!sessionStorage.getItem('nsess')){M('Log in first.');return}M('Opening audio\\u2026');try{var r=await fetch('/audio',{headers:{'X-Nishi-Session':sessionStorage.getItem('nsess')}});if(r.ok){var h=await r.text();var x=window.open('','_blank');x.document.write(h);x.document.close();M('Audio page opened (protected by your session).')}else{M('Audio denied ('+r.status+') \\u2014 login required.')}}catch(e){M('audio error: '+e)}}function out(){sessionStorage.removeItem('nsess');M('Signed out (token cleared).')}</script></body></html>" as *u8 59 60// THE SHARED ROUTE HANDLER. req[0..rn] = the plaintext HTTP request; writes the full HTTP response to 61// resp_out and returns its length. now = sys_now_realtime_sec(). resp_out must be >= ~256 KiB (audio page). 62// ORIGIN-AWARE ROUTE (2026-07-31, debt 1785445694). Signup must be gated on LAN-AND-INVITED, not on a 63// single global boolean. Callers that know the peer address pass it; olg_route below keeps the old 64// 6-arg contract (rule 19) and passes ip4=0, which this file treats as UNKNOWN ORIGIN = WAN = DENY. 65func olg_route_ip(ctx: *NxAuthContext, req: *u8, rn: i64, now: i64, resp_out: *u8, resp_cap: i64, 66 ip4: *u8, hr_store: *u8, realm: *u8, realm_n: i64, regflag: *u8) -> i64 { 67 let he: i64 = olr_find(req, rn, "\r\n\r\n" as *u8, 4) 68 var body: *u8 = req; var bn: i64 = 0 69 if he >= 0 { body = ((req as i64) + he + 4) as *u8; bn = rn - he - 4 } 70 71 if olr_starts(req, rn, "POST /login" as *u8) == 1 { 72 let hb: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64 73 let pb: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64 74 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hb, 127, hl) 75 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pb, 319, pl) 76 let b64: *u8 = sys_mmap(512); let b64n: *i64 = sys_mmap(16) as *i64 77 if olg_login(ctx, hb, hl[0], pb, pl[0], b64, 512, b64n) == NX_MAUTH_OK { 78 let jb: *u8 = sys_mmap(640); var o: i64 = olr_cat(jb, 0, "{\"token\":\"" as *u8); o = olr_catb(jb, o, b64, b64n[0]); o = olr_cat(jb, o, "\"}" as *u8) 79 return olr_resp(resp_out, "200 OK" as *u8, "application/json" as *u8, jb, o) 80 } 81 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"invalid credentials\"}" as *u8, 31) 82 } 83 if olr_starts(req, rn, "GET /whoami" as *u8) == 1 { 84 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 85 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 86 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 87 let jb: *u8 = sys_mmap(128); var o: i64 = olr_cat(jb, 0, "{\"uid\":\"" as *u8); o = olr_hex(jb, o, uh, uhn[0]); o = olr_cat(jb, o, "\"}" as *u8) 88 return olr_resp(resp_out, "200 OK" as *u8, "application/json" as *u8, jb, o) 89 } 90 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"session rejected\"}" as *u8, 28) 91 } 92 if olr_starts(req, rn, "GET /audio" as *u8) == 1 { 93 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 94 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 95 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 96 let page: *u8 = sys_mmap(OLR_MAGIC_131072); let plen: i64 = nx_audio_web_page(page, OLR_MAGIC_131072) 97 return olr_resp(resp_out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, page, plen) 98 } 99 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 26) 100 } 101 if olr_starts(req, rn, "GET /finance/intake" as *u8) == 1 { 102 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 103 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 104 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 105 let page: *u8 = sys_mmap(OLR_MAGIC_131072); let plen: i64 = if_render(page) 106 return olr_resp(resp_out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, page, plen) 107 } 108 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 26) 109 } 110 if olr_starts(req, rn, "POST /finance/intake" as *u8) == 1 { 111 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 112 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 113 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 114 let rid: *u8 = sys_mmap(160); let rlen: i64 = olr_hex(rid, 0, uh, uhn[0]); rid[rlen] = 0 as u8 115 let icid: *u8 = sys_mmap(96) 116 if ih_record("knowledge/store/intake-" as *u8, rid, body, bn, icid) == 0 { 117 let svb: *u8 = "<!doctype html><meta charset=utf-8><title>Saved</title><p>Saved. <a href=\"/finance\">See your plan</a></p>" as *u8 118 return olr_resp(resp_out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, svb, olr_slen(svb)) 119 } 120 return olr_resp(resp_out, "500 Internal Server Error" as *u8, "application/json" as *u8, "{\"error\":\"save failed\"}" as *u8, 23) 121 } 122 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 26) 123 } 124 if olr_starts(req, rn, "POST /finance/bills" as *u8) == 1 { 125 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 126 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 127 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 128 let rid: *u8 = sys_mmap(160); let rlen: i64 = olr_hex(rid, 0, uh, uhn[0]); rid[rlen] = 0 as u8 129 be_record("knowledge/store/bills-" as *u8, rid, body, bn) 130 return olr_resp303(resp_out, "/finance" as *u8) 131 } 132 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 26) 133 } 134 if olr_starts(req, rn, "POST /finance/progress" as *u8) == 1 { 135 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 136 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 137 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 138 let rid: *u8 = sys_mmap(160); let rlen: i64 = olr_hex(rid, 0, uh, uhn[0]); rid[rlen] = 0 as u8 139 fw_progress_advance(rid) 140 return olr_resp303(resp_out, "/finance" as *u8) 141 } 142 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 26) 143 } 144 if olr_starts(req, rn, "GET /finance" as *u8) == 1 { 145 let tb: *u8 = sys_mmap(512); let tl: i64 = olr_hdr_val(req, he, "X-Nishi-Session:" as *u8, 16, tb, 512) 146 let uh: *u8 = sys_mmap(64); let uhn: *i64 = sys_mmap(16) as *i64 147 if olg_whoami(ctx, tb, tl, now, uh, 64, uhn) == NX_MAUTH_OK { 148 let rid: *u8 = sys_mmap(160); let rlen: i64 = olr_hex(rid, 0, uh, uhn[0]); rid[rlen] = 0 as u8 149 let page: *u8 = sys_mmap(OLR_MAGIC_131072); let plen: i64 = fw_render(page, rid) 150 return olr_resp(resp_out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, page, plen) 151 } 152 return olr_resp(resp_out, "401 Unauthorized" as *u8, "application/json" as *u8, "{\"error\":\"login required\"}" as *u8, 26) 153 } 154 if olr_starts(req, rn, "POST /register" as *u8) == 1 { 155 if olg_registration_open_at(regflag) == 0 { 156 return olr_resp(resp_out, "403 Forbidden" as *u8, "application/json" as *u8, "{\"error\":\"registration closed\"}" as *u8, 31) 157 } 158 let hb: *u8 = sys_mmap(128); let hl: *i64 = sys_mmap(16) as *i64 159 let pb: *u8 = sys_mmap(320); let pl: *i64 = sys_mmap(16) as *i64 160 nx_http_form_get_field(body, bn, "handle" as *u8, 6, hb, 127, hl) 161 nx_http_form_get_field(body, bn, "pw" as *u8, 2, pb, 319, pl) 162 // LAN + INVITE GATE. ip4==0 = caller supplied no peer address = UNKNOWN ORIGIN, treated as WAN 163 // and DENIED. This check MUST precede ls_signup_allowed, which dereferences ip4 immediately via 164 // ls_ip_is_lan -- passing a null through would segfault the login daemon on every request. 165 if (ip4 as i64) == 0 { 166 return olr_resp(resp_out, "403 Forbidden" as *u8, "application/json" as *u8, "{\"error\":\"signup requires LAN origin\"}" as *u8, 38) 167 } 168 if ls_signup_allowed(ip4, hr_store, realm, realm_n, hb, hl[0]) == 0 { 169 return olr_resp(resp_out, "403 Forbidden" as *u8, "application/json" as *u8, "{\"error\":\"signup requires LAN origin and an invite\"}" as *u8, 52) 170 } 171 let mn: *u8 = sys_mmap(512); let mnn: *i64 = sys_mmap(16) as *i64 172 if olg_register(ctx, hb, hl[0], pb, pl[0], mn, 512, mnn) == NX_MAUTH_OK { 173 let jb: *u8 = sys_mmap(640); var o: i64 = olr_cat(jb, 0, "{\"mnemonic\":\"" as *u8); o = olr_catb(jb, o, mn, mnn[0]); o = olr_cat(jb, o, "\"}" as *u8) 174 return olr_resp(resp_out, "200 OK" as *u8, "application/json" as *u8, jb, o) 175 } 176 return olr_resp(resp_out, "400 Bad Request" as *u8, "application/json" as *u8, "{\"error\":\"register failed\"}" as *u8, 27) 177 } 178 // default: the login page 179 return olr_resp(resp_out, "200 OK" as *u8, "text/html; charset=utf-8" as *u8, OLR_HTML, olr_slen(OLR_HTML)) 180} 181 182// RULE-19 WRAPPER: the original 6-arg contract, unchanged for every existing caller 183// (nx_opaque_login_tls_daemon, nx_fin_route_gate, ...). It passes ip4=0 = UNKNOWN ORIGIN, so signup is 184// DENIED through this door by construction. A caller that can prove a LAN peer calls olg_route_ip. 185func olg_route(ctx: *NxAuthContext, req: *u8, rn: i64, now: i64, resp_out: *u8, resp_cap: i64) -> i64 { 186 return olg_route_ip(ctx, req, rn, now, resp_out, resp_cap, 0 as *u8, "" as *u8, "" as *u8, 0, OLG_REGFLAG_PATH) 187}