From ee7ec5107ca0f7b908e53afd9738b6cf8f671bdd Mon Sep 17 00:00:00 2001 From: Kharec Date: Thu, 2 Apr 2026 17:31:35 +0200 Subject: [PATCH] feat: add main source file --- src/teotipi.gleam | 51 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/teotipi.gleam diff --git a/src/teotipi.gleam b/src/teotipi.gleam new file mode 100644 index 0000000..3698466 --- /dev/null +++ b/src/teotipi.gleam @@ -0,0 +1,51 @@ +import argv +import gleam/bit_array +import gleam/crypto +import gleam/int +import gleam/io +import gleam/result +import gleam/string +import gleam/time/timestamp +import thirtytwo + +pub fn main() { + case argv.load().arguments { + [secret, ..] -> + case totp_string(secret) { + Ok(code) -> io.println(code) + Error(_) -> io.println("Error: invalid secret") + } + [] -> io.println("Usage: teotipi ") + } +} + +pub fn totp_string(secret_b32: String) -> Result(String, Nil) { + use code <- result.map(totp(secret_b32)) + code |> int.to_string |> string.pad_start(6, "0") +} + +pub fn totp(secret_b32: String) -> Result(Int, Nil) { + use secret <- result.try(thirtytwo.decode(secret_b32)) + let now = timestamp.system_time() + let #(seconds, _nanoseconds) = timestamp.to_unix_seconds_and_nanoseconds(now) + let counter = seconds / 30 + + let counter_bits = <> + let mac = crypto.hmac(counter_bits, crypto.Sha1, secret) + + Ok(truncate(mac)) +} + +fn truncate(mac: BitArray) -> Int { + let assert <<_:bytes-size(19), last:int>> = mac + let offset = int.bitwise_and(last, 0x0f) + let assert Ok(slice) = bit_array.slice(mac, offset, 4) + let assert <> = slice + let code = + int.bitwise_and(b0, 0x7f) + |> int.bitwise_shift_left(24) + |> int.bitwise_or(int.bitwise_shift_left(b1, 16)) + |> int.bitwise_or(int.bitwise_shift_left(b2, 8)) + |> int.bitwise_or(b3) + code % 1_000_000 +}