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}