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}