diff --git a/manual.pdf b/manual.pdf index c16fb56..5ab4caf 100644 Binary files a/manual.pdf and b/manual.pdf differ diff --git a/src/misc.typ b/src/misc.typ index 3a9178d..df51f3e 100644 --- a/src/misc.typ +++ b/src/misc.typ @@ -1,7 +1,8 @@ #import "utils.typ": xor-bytes #import "sha.typ": sha1 #import "md.typ": md4 -#import "utils.typ": utf8-to-utf16le +#import "utils.typ": utf8-to-utf16le, z-fill +#import "base.typ": b32-decode #let _compute-block-sized-key(key, hash-func: sha1, block-size: 64) = { if key.len() > block-size { @@ -15,6 +16,18 @@ return key } +#let _extract31(value) = { + let bytes = array(value) + let i = bytes.last().bit-and(0xf) + let selected-bytes = ( + bytes.at(i).bit-and(0x7f), + bytes.at(i+1), + bytes.at(i+2), + bytes.at(i+3) + ) + return selected-bytes.fold(0, (a, b) => a.bit-lshift(8).bit-or(b)) +} + /// Hash-based Message Authentication Code /// ```example /// #bytes-to-hex(hmac("Key", "Hello World!")) @@ -56,4 +69,50 @@ /// -> bytes #let ntlm(password) = { return md4(utf8-to-utf16le(password)) +} + +/// Time-based One-Time Password +/// ```example +/// #let epoch = datetime( +/// year: 1970, month: 1, day: 1, +/// hour: 0, minute: 0, second: 0 +/// ) +/// #let date = datetime( +/// year: 2025, month: 1, day: 4, +/// hour: 12, minute: 53, second: 30 +/// ) +/// #totp( +/// b32-encode(bytes("YOUPI")), +/// (date - epoch).seconds() +/// ) +/// ``` +/// -> str +#let totp( + /// Secret key. Either bytes or a base32-encode value + /// -> str | bytes + secret, + /// Current time (seconds since t0) + /// -> int + time, + /// Time origin + /// -> int + t0: 0, + /// Code duration + /// -> int + period: 30, + /// Code length + /// -> int + digits: 6 +) = { + let secret = if type(secret) == str {b32-decode(secret)} else {secret} + assert(type(secret) == bytes, message: "secret must be a string or bytes, but is " + repr(type(secret))) + + let count = int(calc.div-euclid(time - t0, period)) + let count-bytes = count.to-bytes(endian: "big", size: 8) + + let digest = hmac(secret, count-bytes) + let hotp = _extract31(digest) + let code = calc.rem(hotp, calc.pow(10, digits)) + + return z-fill(str(code), digits) } \ No newline at end of file