hotp_sha1.nx source
↩ module page · 86 lines · 2771 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
23import "syscalls.nx"
24import "hmac_sha1.nx"
25
26// Compute a HOTP code. Same math as totp_sha1 but caller owns
27// the counter.
28func hotp_sha1_code(key: *u8, key_len: i64,
29 counter: i64, digits: i64) -> i64 {
30 let counter_bytes: *u8 = sys_mmap(16)
31 var i: i64 = 0
32 while i < 8 {
33 counter_bytes[i] = (counter >> ((7 - i) * 8)) & 0xFF
34 i = i + 1
35 }
36 let mac: *u8 = sys_mmap(32)
37 hmac_sha1(key, key_len, counter_bytes, 8, mac)
38
39 let offset: i64 = mac[19] & 0x0F
40 let b0: i64 = mac[offset] & 0x7F
41 let b1: i64 = mac[offset + 1] & 0xFF
42 let b2: i64 = mac[offset + 2] & 0xFF
43 let b3: i64 = mac[offset + 3] & 0xFF
44 let truncated: i64 = (b0 << 24) | (b1 << 16) | (b2 << 8) | b3
45
46 var modulus: i64 = 1
47 var d: i64 = 0
48 while d < digits {
49 modulus = modulus * 10
50 d = d + 1
51 }
52 return truncated % modulus
53}
54
55// Render as zero-padded ASCII digits.
56func hotp_sha1_render(value: i64, digits: i64, out: *u8) -> i64 {
57 var v: i64 = value
58 var i: i64 = digits - 1
59 while i >= 0 {
60 out[i] = 0x30 + (v % 10)
61 v = v / 10
62 i = i - 1
63 }
64 return digits
65}
66
67// Compile-only smoke. RFC 4226 Appendix D test values:
68// secret = "12345678901234567890" (ASCII = 20 bytes)
69// counter=0 -> 755224
70// counter=1 -> 287082
71// counter=2 -> 359152
72func main() -> i64 {
73 let k: *u8 = "12345678901234567890"
74 let c0: i64 = hotp_sha1_code(k, 20, 0, 6)
75 if c0 != 755224 { return 1 }
76 let c1: i64 = hotp_sha1_code(k, 20, 1, 6)
77 if c1 != 287082 { return 2 }
78 let c2: i64 = hotp_sha1_code(k, 20, 2, 6)
79 if c2 != 359152 { return 3 }
80
81 let out: *u8 = sys_mmap(16)
82 hotp_sha1_render(c0, 6, out)
83 if out[0] != 0x37 { return 4 } // '7'
84 if out[5] != 0x34 { return 5 } // '4'
85 return 0
86}