code wiki / (root) / otp.nx

otp.nx source

↩ module page · 113 lines · 4191 B

1// otp.nx -- HOTP (RFC 4226) + TOTP (RFC 6238) one-time passwords. 2// 3// Powers the 6-digit codes in Google Authenticator / Authy / 4// Yubico Authenticator / Microsoft Authenticator / 1Password. 5// 6// HOTP = HMAC(key, counter) -> 6-digit code (event-counter based) 7// TOTP = HOTP(key, floor(time / period)) -- time-based variant 8// 9// Standard RFC profiles use HMAC-SHA-1 for compatibility. We 10// ship HMAC-SHA-256 variants here because: 11// - SHA-1 is deprecated for new systems (rule: don't encourage) 12// - RFC 6238 ยง5 explicitly permits SHA-256 / SHA-512 13// - All major authenticator apps support SHA-256 profiles 14// Callers interoperating with legacy SHA-1 providers need a 15// separate hotp_sha1.nx we're not shipping. 16// 17// Algorithm (RFC 4226): 18// 1. HMAC = HMAC-SHA-256(key, counter as u64 big-endian) 19// 2. offset = HMAC[31] & 0x0F (dynamic truncation) 20// 3. truncated = read 4 bytes BE at HMAC[offset..offset+4] 21// 4. truncated &= 0x7FFFFFFF (clear top bit) 22// 5. code = truncated mod 10^digits 23// 24// Invariants: 25// OTP1 digits in [6, 10]; most deployments use 6. We don't 26// enforce; caller picks. 27// OTP2 Counter is u64 big-endian; identical on every platform. 28// OTP3 Code is zero-padded to `digits` length on render. 29 30import "syscalls.nx" 31import "nx_hmac.nx" // was hmac.nx -- CODE-IDENTICAL twin (49/49 stmts) on the LEGACY syscalls.nx+sha256.nx family. 32// Two files defining hmac_sha256 + main, with the expander deduping BY PATH NOT BY SYMBOL, made 33// every legacy importer a duplicate-symbol landmine for the nx_ family (debt 1785524913). 34 35// HOTP core: compute the numeric code (pre-rendering). 36func hotp_value(key: *u8, key_len: i64, counter: i64, digits: i64) -> i64 { 37 // Serialise counter as u64 big-endian. 38 let counter_bytes: *u8 = sys_mmap(16) 39 var i: i64 = 0 40 while i < 8 { 41 counter_bytes[i] = (counter >> ((7 - i) * 8)) & 0xFF 42 i = i + 1 43 } 44 // HMAC. 45 let hmac_out: *u8 = sys_mmap(32) 46 hmac_sha256(key, key_len, counter_bytes, 8, hmac_out) 47 48 // Dynamic truncation. 49 let offset: i64 = hmac_out[31] & 0x0F 50 let b0: i64 = hmac_out[offset] & 0x7F 51 let b1: i64 = hmac_out[offset + 1] & 0xFF 52 let b2: i64 = hmac_out[offset + 2] & 0xFF 53 let b3: i64 = hmac_out[offset + 3] & 0xFF 54 let truncated: i64 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 55 56 // Modulo 10^digits. 57 var modulus: i64 = 1 58 var d: i64 = 0 59 while d < digits { 60 modulus = modulus * 10 61 d = d + 1 62 } 63 return truncated % modulus 64} 65 66// Render an OTP value as zero-padded ASCII digits. Returns length 67// written = digits. 68func otp_render(value: i64, digits: i64, out: *u8) -> i64 { 69 var v: i64 = value 70 var i: i64 = digits - 1 71 while i >= 0 { 72 out[i] = 0x30 + (v % 10) 73 v = v / 10 74 i = i - 1 75 } 76 return digits 77} 78 79// Complete HOTP: compute + render. 80func hotp_sha256(key: *u8, key_len: i64, counter: i64, digits: i64, 81 out: *u8) -> i64 { 82 let v: i64 = hotp_value(key, key_len, counter, digits) 83 return otp_render(v, digits, out) 84} 85 86// TOTP = HOTP(key, floor(unix_time / period)). 87// Standard period is 30 seconds. `unix_time` is Unix seconds 88// since epoch (caller fetches via time.nx's monotonic/wall helper). 89func totp_sha256(key: *u8, key_len: i64, 90 unix_time: i64, period: i64, digits: i64, 91 out: *u8) -> i64 { 92 let counter: i64 = unix_time / period 93 return hotp_sha256(key, key_len, counter, digits, out) 94} 95 96// Compile-only smoke: HOTP with RFC 4226 test key + counter=0. 97// RFC 4226 uses SHA-1 so we can't match the RFC vector exactly 98// (we use SHA-256). But the algorithm structure is validated by 99// the code compiling + producing a deterministic digit. 100func main() -> i64 { 101 let key: *u8 = "12345678901234567890" 102 let out: *u8 = sys_mmap(16) 103 let len: i64 = hotp_sha256(key, 20, 0, 6, out) 104 if len != 6 { return 1 } 105 // Every char should be 0x30..0x39. 106 var i: i64 = 0 107 while i < 6 { 108 if out[i] < 0x30 { return 2 } 109 if out[i] > 0x39 { return 3 } 110 i = i + 1 111 } 112 return 0 113}