///|
/// HOTP: HMAC-based one-time password, RFC 4226.
fn zero_pad(code : Int, digits : Int) -> String {
let s = code.to_string()
let b = Buffer()
for _ in 0..<(digits - s.length()) {
b.write_byte(0x30)
}
b.write_string_utf8(s)
@utf8.decode_lossy(b.to_bytes())
}
///|
/// Compute an HOTP code.
///
/// - `algorithm`: hash function used for the HMAC.
/// - `key`: shared secret (RFC test keys are ASCII; real deployments use random bytes).
/// - `counter`: 8-byte moving factor.
/// - `digits`: number of output digits (RFC 4226 recommends 6; supported 1..8).
pub fn hotp(
algorithm : HashAlgorithm,
key : Bytes,
counter : UInt64,
digits? : Int = 6,
) -> String raise OtpError {
if digits < 1 || digits > 8 {
raise InvalidDigits(digits)
}
if key.length() == 0 {
raise EmptySecret
}
let counter_buf = Buffer()
counter_buf.write_uint64_be(counter)
let mac = hmac(algorithm, key, counter_buf.to_bytes())
// Dynamic truncation (RFC 4226 §5.3).
let offset = (mac[mac.length() - 1] & 0x0f).to_int()
let bin_code = ((mac[offset] & 0x7f).to_uint() << 24) |
(mac[offset + 1].to_uint() << 16) |
(mac[offset + 2].to_uint() << 8) |
mac[offset + 3].to_uint()
let mut modulus = 1
for _ in 0.. UInt64? raise OtpError {
if window < 0 {
raise InvalidUri("window must be non-negative")
}
let mut i = 0
while i <= window {
let counter = current_counter + i.to_uint64()
if hotp(algorithm, key, counter) == code1 &&
hotp(algorithm, key, counter + 1UL) == code2 {
return Some(counter)
}
i += 1
}
None
}