code wiki / (root) / nx_hotp_sha1.nx

nx_hotp_sha1.nx source

↩ module page · 95 lines · 3071 B

1// hotp_sha1.nx -- RFC 4226 HOTP with SHA-1. 2// 3// Counter-based OTP. Used by: 4// - Yubico OTP hardware tokens (YubiKey in HOTP mode) 5// - Microsoft ActiveSync + Exchange legacy provisioning 6// - Some corporate 2FA where server tracks a per-user counter 7// 8// TOTP (totp_sha1.nx) is the time-based variant that builds on 9// HOTP by setting counter = floor(unix_time / step). HOTP is 10// the lower-level primitive. 11// 12// Algorithm: 13// HOTP(K, C) = DT(HMAC-SHA-1(K, C_be64)) mod 10^digits 14// DT(mac) = big-endian u32 at offset (mac[19] & 0x0F) 15// with high bit of first byte masked off 16// 17// Invariants: 18// H1 Counter is serialised as 8-byte big-endian u64. 19// H2 Dynamic truncation: offset = low 4 bits of mac[19] 20// (valid 0..15; always 4 bytes left in 20-byte MAC). 21// H3 Output value in range [0, 10^digits). 22 23// nx_safety_envelope: 24// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 25// sil_target: SIL1 26// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 27// verdict: NOT_YET_EVALUATED 28 29import "nx_syscalls.nx" 30import "nx_hmac_sha1.nx" 31const K_MAGIC_755224: i64 = 755224 32const K_MAGIC_287082: i64 = 287082 33const K_MAGIC_359152: i64 = 359152 34 35// Compute a HOTP code. Same math as totp_sha1 but caller owns 36// the counter. 37func hotp_sha1_code(key: *u8, key_len: i64, 38 counter: i64, digits: i64) -> i64 { 39 let counter_bytes: *u8 = sys_mmap(16) 40 var i: i64 = 0 41 while i < 8 { 42 counter_bytes[i] = (counter >> ((7 - i) * 8)) & 0xFF 43 i = i + 1 44 } 45 let mac: *u8 = sys_mmap(32) 46 hmac_sha1(key, key_len, counter_bytes, 8, mac) 47 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// Render as zero-padded ASCII digits. 65func hotp_sha1_render(value: i64, digits: i64, out: *u8) -> i64 { 66 var v: i64 = value 67 var i: i64 = digits - 1 68 while i >= 0 { 69 out[i] = 0x30 + (v % 10) 70 v = v / 10 71 i = i - 1 72 } 73 return digits 74} 75 76// Compile-only smoke. RFC 4226 Appendix D test values: 77// secret = "12345678901234567890" (ASCII = 20 bytes) 78// counter=0 -> 755224 79// counter=1 -> 287082 80// counter=2 -> 359152 81func main() -> i64 { 82 let k: *u8 = "12345678901234567890" 83 let c0: i64 = hotp_sha1_code(k, 20, 0, 6) 84 if c0 != K_MAGIC_755224 { return 1 } 85 let c1: i64 = hotp_sha1_code(k, 20, 1, 6) 86 if c1 != K_MAGIC_287082 { return 2 } 87 let c2: i64 = hotp_sha1_code(k, 20, 2, 6) 88 if c2 != K_MAGIC_359152 { return 3 } 89 90 let out: *u8 = sys_mmap(16) 91 hotp_sha1_render(c0, 6, out) 92 if out[0] != 0x37 { return 4 } // '7' 93 if out[5] != 0x34 { return 5 } // '4' 94 return 0 95}