nx_totp_sha1.nx source
↩ module page · 116 lines · 3867 B
1// totp_sha1.nx -- RFC 6238 TOTP with SHA-1 (Google Authenticator
2// default, Authy, Microsoft Authenticator, Duo, etc.).
3//
4// Companion to otp.nx which shipped SHA-256 variants. The real
5// world runs on HMAC-SHA-1 TOTP because RFC 6238 specified it as
6// the default and every authenticator app followed suit.
7//
8// Protocol:
9// HOTP(K, C) = truncate(HMAC-SHA-1(K, C_be64)) mod 10^digits
10// TOTP(K) = HOTP(K, floor(now_unix / step))
11//
12// Standard params:
13// digits = 6
14// step = 30 seconds
15// T0 = 0 (unix epoch)
16//
17// Composes hmac_sha1.nx.
18//
19// Invariants:
20// TS1 Dynamic truncation per RFC 4226 ยง5.3: last 4 bits of
21// hash = offset; take 4 bytes @ offset with high bit of
22// first masked off (31-bit unsigned).
23// TS2 Leading zeros are preserved by otp_render (6-digit codes
24// starting with 0 are common and still valid).
25
26// nx_safety_envelope:
27// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
28// sil_target: SIL1
29// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
30// verdict: NOT_YET_EVALUATED
31
32import "nx_syscalls.nx"
33import "nx_hmac_sha1.nx"
34const TOTP_MAGIC_1000000: i64 = 1000000
35
36const TOTP_SHA1_STEP_SECONDS: i64 = 30
37const TOTP_SHA1_DIGITS: i64 = 6
38
39// HOTP-SHA1 core. Same shape as otp.nx's SHA-256 version but
40// with SHA-1 MAC (20-byte output instead of 32-byte).
41func hotp_sha1_value(key: *u8, key_len: i64,
42 counter: i64, digits: i64) -> i64 {
43 let counter_bytes: *u8 = sys_mmap(16)
44 var i: i64 = 0
45 while i < 8 {
46 counter_bytes[i] = (counter >> ((7 - i) * 8)) & 0xFF
47 i = i + 1
48 }
49 let mac: *u8 = sys_mmap(32)
50 hmac_sha1(key, key_len, counter_bytes, 8, mac)
51
52 // Dynamic truncation. Offset is low 4 bits of last byte.
53 // SHA-1 output is 20 bytes, so offset is in [0,15] and we
54 // always read 4 valid bytes.
55 let offset: i64 = mac[19] & 0x0F
56 let b0: i64 = mac[offset] & 0x7F
57 let b1: i64 = mac[offset + 1] & 0xFF
58 let b2: i64 = mac[offset + 2] & 0xFF
59 let b3: i64 = mac[offset + 3] & 0xFF
60 let truncated: i64 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
61
62 var modulus: i64 = 1
63 var d: i64 = 0
64 while d < digits {
65 modulus = modulus * 10
66 d = d + 1
67 }
68 return truncated % modulus
69}
70
71// TOTP-SHA1: HOTP with counter = floor(now_unix / step).
72func totp_sha1_value(key: *u8, key_len: i64,
73 now_unix: i64, step: i64, digits: i64) -> i64 {
74 let counter: i64 = now_unix / step
75 return hotp_sha1_value(key, key_len, counter, digits)
76}
77
78// Convenience: default params (30s step, 6 digits).
79func totp_sha1_default(key: *u8, key_len: i64, now_unix: i64) -> i64 {
80 return totp_sha1_value(key, key_len, now_unix,
81 TOTP_SHA1_STEP_SECONDS, TOTP_SHA1_DIGITS)
82}
83
84// Render a code as zero-padded ASCII digits. `digits` bytes
85// written to out. Leading zeros preserved (important: a 6-digit
86// code "012345" is valid TOTP output and not "12345").
87func totp_sha1_render(value: i64, digits: i64, out: *u8) -> i64 {
88 var v: i64 = value
89 var i: i64 = digits - 1
90 while i >= 0 {
91 out[i] = 0x30 + (v % 10)
92 v = v / 10
93 i = i - 1
94 }
95 return digits
96}
97
98// Compile-only smoke: RFC 6238 Appendix B test vector.
99// Secret = "12345678901234567890" (ASCII). T=59 -> 94287082.
100// Our smoke just verifies the code is a valid 6-digit integer.
101func main() -> i64 {
102 let code: i64 = totp_sha1_default("12345678901234567890", 20, 59)
103 if code < 0 { return 1 }
104 if code >= TOTP_MAGIC_1000000 { return 2 }
105
106 // Render.
107 let out: *u8 = sys_mmap(16)
108 totp_sha1_render(code, 6, out)
109 var i: i64 = 0
110 while i < 6 {
111 if out[i] < 0x30 { return 3 }
112 if out[i] > 0x39 { return 4 }
113 i = i + 1
114 }
115 return 0
116}