code wiki / (root) / nx_totp_sha1.nx

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}