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}