code wiki / (root) / nx_mfa.nx

nx_mfa.nx source

↩ module page · 73 lines · 4196 B

1// nx_mfa.nx -- ADMIN MFA / second factor (closes hosting_research gap #12 "mfa-admin", 2/0 CONFIRMED: 2// admin login is single-factor password only). Sovereign TOTP verification (RFC 6238, HMAC-SHA1 -- the 3// Google Authenticator / Authy / Microsoft Authenticator / Duo default) on top of the already-KAT'd 4// nx_totp_sha1 + nx_base32. The login composes mfa_verify AFTER the argon2id password check, so a stolen 5// password alone is not enough. Why SHA-1 (not nx_otp's SHA-256): authenticator apps default to and widely 6// ONLY support HMAC-SHA1 TOTP; SHA-1's collision weakness is irrelevant to HMAC's PRF security (NIST + RFC 7// 6238 both keep HMAC-SHA1 for OTP). Standard params: 6 digits, 30s step, +/-1 step skew window. 8// 9// module: nishi-core.auth.mfa capability: ACCESS_CONTROL / auth 10import "nx_totp_sha1.nx" // hotp_sha1_value / totp_sha1_render 11import "nx_base32.nx" // base32_decode / base32_encode 12import "nx_syscalls.nx" 13 14const MFA_STEP: i64 = 30 // RFC 6238 default time-step (seconds) 15const MFA_DIGITS: i64 = 6 // authenticator-app default 16const MFA_WINDOW: i64 = 1 // accept the code for step-1 / step / step+1 (RFC 6238 SS5.2 skew tolerance) 17const MFA_KEY_BYTES: i64 = 20 // 160-bit secret (RFC 4226 SS4 recommends >= 128 bits, 160 preferred) 18 19// constant-time compare of two equal-length digit strings (no early-out timing oracle on the code). 20func mfa_ct_eq(a: *u8, b: *u8, n: i64) -> i64 { 21 var diff: i64 = 0; var i: i64 = 0 22 while i < n { diff = diff | ((a[i] as i64) ^ (b[i] as i64)); i = i + 1 } 23 if diff == 0 { return 1 } 24 return 0 25} 26 27// verify a submitted code (ASCII digits, code_len long) against a base32 secret at now_unix, within 28// +/-window steps. Returns 1 = valid, 0 = invalid. Decodes the secret, walks the skew window, ct-compares. 29func mfa_verify(secret_b32: *u8, b32_len: i64, code: *u8, code_len: i64, now_unix: i64, window: i64) -> i64 { 30 if code_len != MFA_DIGITS { return 0 } 31 let key: *u8 = sys_mmap(64) 32 let klen: i64 = base32_decode(secret_b32, b32_len, key) 33 if klen <= 0 { return 0 } 34 let base_counter: i64 = now_unix / MFA_STEP 35 let cand: *u8 = sys_mmap(16) 36 var w: i64 = 0 - window 37 var ok: i64 = 0 38 while w <= window { 39 let v: i64 = hotp_sha1_value(key, klen, base_counter + w, MFA_DIGITS) 40 totp_sha1_render(v, MFA_DIGITS, cand) 41 if mfa_ct_eq(cand, code, MFA_DIGITS) == 1 { ok = 1 } // no early break: walk all steps (timing-flat) 42 w = w + 1 43 } 44 return ok 45} 46 47// is MFA enrolled? -- a non-empty base32 secret is present. (login: enrolled -> require code; not -> password-only) 48func mfa_enrolled(secret_b32: *u8, b32_len: i64) -> i64 { if b32_len >= 16 { return 1 } return 0 } 49 50// ENROLLMENT: generate a fresh 160-bit secret from /dev/urandom and base32-encode it (-> out_b32). Returns 51// the base32 length, or 0 on failure. The operator enters/scans this into their authenticator once. 52func mfa_gen_secret(out_b32: *u8) -> i64 { 53 let raw: *u8 = sys_mmap(64) 54 let fd: i64 = sys_openat_rd("/dev/urandom" as *u8); if fd < 0 { return 0 } 55 var got: i64 = 0 56 while got < MFA_KEY_BYTES { let r: i64 = sys_read(fd, ((raw as i64)+got) as *u8, MFA_KEY_BYTES-got); if r <= 0 { got = MFA_KEY_BYTES } else { got = got + r } } 57 sys_close(fd) 58 return base32_encode(raw, MFA_KEY_BYTES, out_b32) 59} 60 61func mfa_p(s: *u8) -> i64 { var n: i64=0; while s[n]!=(0 as u8){n=n+1} sys_write(1,s,n); return 0 } 62 63func main() -> i64 { 64 // demo enrollment + a live code for the just-generated secret (shows the round trip end-to-end). 65 let sec: *u8 = sys_mmap(64); let sl: i64 = mfa_gen_secret(sec) 66 mfa_p("=== NISHI ADMIN MFA (TOTP-SHA1, RFC 6238) ===\n" as *u8) 67 mfa_p("enrollment secret (base32, put in your authenticator): " as *u8); sys_write(1, sec, sl); mfa_p("\n" as *u8) 68 let now: i64 = sys_now_realtime_sec() 69 let key: *u8 = sys_mmap(64); let kl: i64 = base32_decode(sec, sl, key) 70 let cur: *u8 = sys_mmap(16); totp_sha1_render(hotp_sha1_value(key, kl, now / MFA_STEP, MFA_DIGITS), MFA_DIGITS, cur) 71 mfa_p("current 6-digit code (this 30s window): " as *u8); sys_write(1, cur, 6); mfa_p("\n" as *u8) 72 sys_exit(0); return 0 73}