code wiki / (root) / nx_opaque_login.nx

nx_opaque_login.nx source

↩ module page · 131 lines · 8581 B

1// nx_opaque_login.nx -- no-cookie OPAQUE login LOGIC (the wiki's model, FULL OPAQUE / modauth, RFC 9807). 2// 3// "Opaque" = the no-cookie passphrase login the wiki uses: the server runs the OPAQUE aPAKE in-process 4// (storing only the OPAQUE envelope -- NO password-equivalent at rest), then mints an Ed25519-signed 5// no-cookie session token. The client holds it in sessionStorage and sends `X-Nishi-Session: <base64>` 6// per request -- NO cookies (cardinal C1). This module is the THIN seam between HTTP and the already- 7// RFC-KAT'd modauth API: it composes nx_modern_auth_{register,login,validate} + base64 wire-encoding so a 8// socket daemon (or a gate) can drive the full login over the wire. No shortcuts: full OPAQUE, production 9// argon2id KSF (caller-supplied so gates can run light while the daemon runs 64 MiB/t3/p4), every reject path. 10// 11// module: nishi-core.auth.opaque_login capability: ACCESS_CONTROL / auth 12import "hub/nx_modern_auth_flow.nx" // NxAuthContext + nx_auth_context_init + nx_modern_auth_* + NX_MAUTH_* 13import "hub/nx_user_account_store.nx" // nx_uas_server_keys_load_or_init + NX_UAS_OK 14import "nx_base64.nx" // b64_encode / b64_decode 15 16// Registration kill-switch (data-driven, rule 17): registration is CLOSED by default (fail-safe) and 17// opens ONLY when this flag file's first byte is '1'. Missing/empty/anything-else => closed. Lets ops 18// reopen without a rebuild; both the plain daemon and the shared olg_route consult it before olg_register. 19const OLG_REGFLAG_PATH: *u8 = "/volume1/homes/elderwesto/nishihost/registration_open" 20const OLG_LENBUF: i64 = 16 21const OLG_ASCII_ONE: i64 = 49 22 23// SINGLE EXIT so the length buffer is freed on EVERY path. The previous shape returned from four places 24// and freed on none, leaking a page per call -- and this is consulted ONCE PER REGISTRATION REQUEST, so 25// it was an unauthenticated remote memory-exhaustion drip. Semantics are byte-identical: fail-closed, 26// default 0, open ONLY when the flag file's first byte is '1'. 27// INJECTABLE PATH (2026-07-31, debt 1785445694). The flag path was a hardcoded ABSOLUTE production 28// path, which made every downstream gate VACUOUS: the register route checks this FIRST, so with the live 29// flag at 0 the LAN/invite branch is unreachable, and the only way to exercise it was to open production 30// registration to the internet. A guard that can only be tested by disabling itself is untestable by 31// construction. olg_registration_open() keeps the exact old contract (rule 19). 32func olg_registration_open() -> i64 { return olg_registration_open_at(OLG_REGFLAG_PATH) } 33 34func olg_registration_open_at(flagpath: *u8) -> i64 { 35 let lb: *i64 = sys_mmap(OLG_LENBUF) as *i64 36 lb[0] = 0 37 let data: *u8 = sys_read_file(flagpath, lb) 38 var open: i64 = 0 39 if (data as i64) != 0 { 40 if lb[0] >= 1 { 41 if (data[0] as i64) == OLG_ASCII_ONE { open = 1 } 42 } 43 } 44 sys_munmap(lb as *u8, OLG_LENBUF) 45 return open 46} 47 48// Load-or-init the server crown-jewel keys (oprf_seed + AKE + Ed25519) and init the auth context with an 49// EXPLICIT session TTL (seconds). Each realm chooses its own lifetime: short for high-value admin realms; 50// long (days) for a low-risk single-operator media realm so the user is not bounced to re-login every 15 min 51// (OWASP: 15-30 min is the LOW-RISK floor, not a ceiling; real consumer media sessions run for days). 52// caller owns ctx (mmap 256). m/t/p = argon2id KSF cost (daemon: 65536/3/4; gates may run light). Returns 0 OK. 53func olg_ctx_setup_ttl(ctx: *NxAuthContext, keys_path: *u8, store_path: *u8, 54 realm: *u8, realm_n: i64, disp: *u8, disp_n: i64, ttl_s: i64, 55 m_cost: i64, t_cost: i64, p_cost: i64) -> i64 { 56 let oprf: *u8 = sys_mmap(32); let akp: *u8 = sys_mmap(32); let akb: *u8 = sys_mmap(33) 57 let edp: *u8 = sys_mmap(32); let edb: *u8 = sys_mmap(32) 58 if nx_uas_server_keys_load_or_init(keys_path, oprf, akp, akb, edp, edb) != NX_UAS_OK { return 1 } 59 // rate_limit=5/min, allow_recovery=1 (BIP39 mnemonic); ttl_s = the caller-chosen session lifetime. 60 if nx_auth_context_init(ctx, realm, realm_n, disp, disp_n, store_path as i64, oprf, edp, edb, 61 ttl_s, m_cost, t_cost, p_cost, 5, 1) != NX_MAUTH_OK { return 2 } 62 return 0 63} 64// Back-compat: the original 900s (15-min) TTL contract, unchanged for wiki/family/admin realms. 65func olg_ctx_setup(ctx: *NxAuthContext, keys_path: *u8, store_path: *u8, 66 realm: *u8, realm_n: i64, disp: *u8, disp_n: i64, 67 m_cost: i64, t_cost: i64, p_cost: i64) -> i64 { 68 return olg_ctx_setup_ttl(ctx, keys_path, store_path, realm, realm_n, disp, disp_n, 900, m_cost, t_cost, p_cost) 69} 70 71// REGISTER: create the account; writes the 24-word BIP39 recovery mnemonic to out_mn. Returns modauth status. 72func olg_register(ctx: *NxAuthContext, handle: *u8, hn: i64, pw: *u8, pwn: i64, 73 out_mn: *u8, mn_cap: i64, out_mn_n: *i64) -> i64 { 74 return nx_modern_auth_register(ctx, handle, hn, pw, pwn, out_mn, mn_cap, out_mn_n) 75} 76 77// RECOVER: forgot-passphrase via the 24-word BIP39 mnemonic (the SECOND credential at handle||"|rec"). Verifies the 78// mnemonic, sets the NEW passphrase, and ROTATES the mnemonic (the old one stops working) -- writes the new one to 79// out_mn. Returns modauth status (0 = OK; negative on a bad/old mnemonic or if recovery is disabled). Fail-closed: 80// a wrong mnemonic never resets the passphrase. Thin seam over nx_modern_auth_recover (V-MODAUTH-3, already KAT'd). 81func olg_recover(ctx: *NxAuthContext, handle: *u8, hn: i64, mnemonic: *u8, mnn: i64, newpw: *u8, newpwn: i64, 82 out_mn: *u8, mn_cap: i64, out_mn_n: *i64) -> i64 { 83 return nx_modern_auth_recover(ctx, handle, hn, mnemonic, mnn, newpw, newpwn, out_mn, mn_cap, out_mn_n) 84} 85 86// LOGIN: OPAQUE in-process; on success writes base64(152-byte token) to out_b64. Returns modauth status 87// (0 = OK; negative = -NX_MAUTH_WRONG_PASSPHRASE / -NX_MAUTH_USER_NOT_FOUND / ...). No token leak on failure. 88func olg_login(ctx: *NxAuthContext, handle: *u8, hn: i64, pw: *u8, pwn: i64, 89 out_b64: *u8, b64_cap: i64, out_b64_n: *i64) -> i64 { 90 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES + 8) 91 let tn: *i64 = sys_mmap(OLG_LENBUF) as *i64 92 let rc: i64 = nx_modern_auth_login(ctx, handle, hn, pw, pwn, tok, NX_MAUTH_SESSION_TOKEN_BYTES, tn) 93 if rc == NX_MAUTH_OK { out_b64_n[0] = b64_encode(tok, tn[0], out_b64) } 94 sys_munmap(tok, NX_MAUTH_SESSION_TOKEN_BYTES + 8) 95 sys_munmap(tn as *u8, OLG_LENBUF) 96 return rc 97} 98 99// WHOAMI: base64-decode the X-Nishi-Session value -> validate -> write the user handle. Returns modauth status 100// (0 = OK; -NX_MAUTH_INVALID_SESSION on tamper/bad-b64; -NX_MAUTH_EXPIRED past TTL). 101func olg_whoami(ctx: *NxAuthContext, b64: *u8, b64n: i64, now_s: i64, 102 out_h: *u8, h_cap: i64, out_h_n: *i64) -> i64 { 103 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES + 8) 104 var rc: i64 = 0 - NX_MAUTH_INVALID_SESSION 105 let dl: i64 = b64_decode(b64, b64n, tok) 106 if dl > 0 { rc = nx_modern_auth_validate_session(ctx, tok, dl, now_s, out_h, h_cap, out_h_n) } 107 sys_munmap(tok, NX_MAUTH_SESSION_TOKEN_BYTES + 8) 108 return rc 109} 110 111// REFRESH: base64-decode the session -> re-mint with a bumped expiry (NO passphrase) -> base64(new token). 112// Sliding-window "stay logged in while active": a client heartbeat calls this and stores the new token. 113// Returns 0=OK; -NX_MAUTH_EXPIRED if the old token already lapsed; -NX_MAUTH_INVALID_SESSION on tamper/bad-b64. 114func olg_refresh(ctx: *NxAuthContext, b64: *u8, b64n: i64, now_s: i64, 115 out_b64: *u8, b64_cap: i64, out_b64_n: *i64) -> i64 { 116 let tok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES + 8) 117 var rc: i64 = 0 - NX_MAUTH_INVALID_SESSION 118 let dl: i64 = b64_decode(b64, b64n, tok) 119 // ntok/nn stay INSIDE the valid branch so a garbage token does no more allocation than before -- 120 // this is an unauthenticated path and must not become cheaper to abuse than it already was. 121 if dl > 0 { 122 let ntok: *u8 = sys_mmap(NX_MAUTH_SESSION_TOKEN_BYTES + 8) 123 let nn: *i64 = sys_mmap(OLG_LENBUF) as *i64 124 rc = nx_modern_auth_refresh_session(ctx, tok, dl, now_s, ntok, NX_MAUTH_SESSION_TOKEN_BYTES, nn) 125 if rc == NX_MAUTH_OK { out_b64_n[0] = b64_encode(ntok, nn[0], out_b64) } 126 sys_munmap(ntok, NX_MAUTH_SESSION_TOKEN_BYTES + 8) 127 sys_munmap(nn as *u8, OLG_LENBUF) 128 } 129 sys_munmap(tok, NX_MAUTH_SESSION_TOKEN_BYTES + 8) 130 return rc 131}