code wiki / (root) / totp_sha1.nx

totp_sha1.nx source

↩ module page · 109 lines · 3668 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 26import "syscalls.nx" 27import "hmac_sha1.nx" 28 29const TOTP_SHA1_STEP_SECONDS: i64 = 30 30const TOTP_SHA1_DIGITS: i64 = 6 31 32// HOTP-SHA1 core. Same shape as otp.nx's SHA-256 version but 33// with SHA-1 MAC (20-byte output instead of 32-byte). 34func hotp_sha1_value(key: *u8, key_len: i64, 35 counter: i64, digits: i64) -> i64 { 36 let counter_bytes: *u8 = sys_mmap(16) 37 var i: i64 = 0 38 while i < 8 { 39 counter_bytes[i] = (counter >> ((7 - i) * 8)) & 0xFF 40 i = i + 1 41 } 42 let mac: *u8 = sys_mmap(32) 43 hmac_sha1(key, key_len, counter_bytes, 8, mac) 44 45 // Dynamic truncation. Offset is low 4 bits of last byte. 46 // SHA-1 output is 20 bytes, so offset is in [0,15] and we 47 // always read 4 valid bytes. 48 let offset: i64 = mac[19] & 0x0F 49 let b0: i64 = mac[offset] & 0x7F 50 let b1: i64 = mac[offset + 1] & 0xFF 51 let b2: i64 = mac[offset + 2] & 0xFF 52 let b3: i64 = mac[offset + 3] & 0xFF 53 let truncated: i64 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 54 55 var modulus: i64 = 1 56 var d: i64 = 0 57 while d < digits { 58 modulus = modulus * 10 59 d = d + 1 60 } 61 return truncated % modulus 62} 63 64// TOTP-SHA1: HOTP with counter = floor(now_unix / step). 65func totp_sha1_value(key: *u8, key_len: i64, 66 now_unix: i64, step: i64, digits: i64) -> i64 { 67 let counter: i64 = now_unix / step 68 return hotp_sha1_value(key, key_len, counter, digits) 69} 70 71// Convenience: default params (30s step, 6 digits). 72func totp_sha1_default(key: *u8, key_len: i64, now_unix: i64) -> i64 { 73 return totp_sha1_value(key, key_len, now_unix, 74 TOTP_SHA1_STEP_SECONDS, TOTP_SHA1_DIGITS) 75} 76 77// Render a code as zero-padded ASCII digits. `digits` bytes 78// written to out. Leading zeros preserved (important: a 6-digit 79// code "012345" is valid TOTP output and not "12345"). 80func totp_sha1_render(value: i64, digits: i64, out: *u8) -> i64 { 81 var v: i64 = value 82 var i: i64 = digits - 1 83 while i >= 0 { 84 out[i] = 0x30 + (v % 10) 85 v = v / 10 86 i = i - 1 87 } 88 return digits 89} 90 91// Compile-only smoke: RFC 6238 Appendix B test vector. 92// Secret = "12345678901234567890" (ASCII). T=59 -> 94287082. 93// Our smoke just verifies the code is a valid 6-digit integer. 94func main() -> i64 { 95 let code: i64 = totp_sha1_default("12345678901234567890", 20, 59) 96 if code < 0 { return 1 } 97 if code >= 1000000 { return 2 } 98 99 // Render. 100 let out: *u8 = sys_mmap(16) 101 totp_sha1_render(code, 6, out) 102 var i: i64 = 0 103 while i < 6 { 104 if out[i] < 0x30 { return 3 } 105 if out[i] > 0x39 { return 4 } 106 i = i + 1 107 } 108 return 0 109}