Compare commits
4 Commits
a158c82788
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ee7ec5107c | |||
| 0b41603ffb | |||
| e9177b0047 | |||
| 32bfe51687 |
+18
@@ -0,0 +1,18 @@
|
||||
name = "teotipi"
|
||||
version = "1.0.0"
|
||||
|
||||
licences = ["MIT"]
|
||||
repository = { type = "gitea", host = "git.kharec.info", user = "Kharec", repo = "teotipi" }
|
||||
links = [
|
||||
{ title = "Repository", href = "https://git.kharec.info/Kharec/teotipi" },
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
||||
thirtytwo = ">= 1.0.0 and < 2.0.0"
|
||||
gleam_time = ">= 1.8.0 and < 2.0.0"
|
||||
gleam_crypto = ">= 1.5.1 and < 2.0.0"
|
||||
argv = ">= 1.0.0 and < 2.0.0"
|
||||
|
||||
[dev_dependencies]
|
||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||
@@ -0,0 +1,19 @@
|
||||
# This file was generated by Gleam
|
||||
# You typically do not need to edit this file
|
||||
|
||||
packages = [
|
||||
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
|
||||
{ name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" },
|
||||
{ name = "gleam_stdlib", version = "0.70.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "86949BF5D1F0E4AC0AB5B06F235D8A5CC11A2DFC33BF22F752156ED61CA7D0FF" },
|
||||
{ name = "gleam_time", version = "1.8.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "533D8723774D61AD4998324F5DD1DABDCDBFABAFB9E87CB5D03C6955448FC97D" },
|
||||
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
|
||||
{ name = "thirtytwo", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "thirtytwo", source = "hex", outer_checksum = "AB56E6C649BF73E6102F7DFCB5B24556CD77E87EA4DAA8E83D2BAACBE064FFC5" },
|
||||
]
|
||||
|
||||
[requirements]
|
||||
argv = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
gleam_crypto = { version = ">= 1.5.1 and < 2.0.0" }
|
||||
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
|
||||
gleam_time = { version = ">= 1.8.0 and < 2.0.0" }
|
||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
thirtytwo = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
@@ -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 <base32-secret>")
|
||||
}
|
||||
}
|
||||
|
||||
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 = <<counter:big-int-size(64)>>
|
||||
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 <<b0:int, b1:int, b2:int, b3:int>> = 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
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import gleam/string
|
||||
import gleeunit
|
||||
import gleeunit/should
|
||||
import teotipi
|
||||
|
||||
pub fn main() -> Nil {
|
||||
gleeunit.main()
|
||||
}
|
||||
|
||||
pub fn totp_string_test() {
|
||||
"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|> teotipi.totp_string
|
||||
|> should.be_ok
|
||||
|> string.length
|
||||
|> should.equal(6)
|
||||
}
|
||||
|
||||
pub fn totp_string_padding_test() {
|
||||
"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|> teotipi.totp_string
|
||||
|> should.be_ok
|
||||
}
|
||||
|
||||
pub fn totp_string_invalid_secret_test() {
|
||||
teotipi.totp_string("INVALID!!!")
|
||||
|> should.be_error
|
||||
}
|
||||
|
||||
pub fn totp_function_test() {
|
||||
let code =
|
||||
"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|> teotipi.totp
|
||||
|> should.be_ok
|
||||
|
||||
should.be_true(code > 0)
|
||||
should.be_true(code < 1_000_000)
|
||||
}
|
||||
|
||||
pub fn totp_invalid_secret_test() {
|
||||
teotipi.totp("INVALID!!!")
|
||||
|> should.be_error
|
||||
}
|
||||
|
||||
pub fn totp_consistency_test() {
|
||||
let secret = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
let result1 = teotipi.totp(secret)
|
||||
let result2 = teotipi.totp(secret)
|
||||
|
||||
should.equal(result1, result2)
|
||||
}
|
||||
|
||||
pub fn totp_different_secrets_test() {
|
||||
let code1 =
|
||||
"GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"
|
||||
|> teotipi.totp
|
||||
|> should.be_ok
|
||||
|
||||
let code2 =
|
||||
"JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"
|
||||
|> teotipi.totp
|
||||
|> should.be_ok
|
||||
|
||||
should.be_true(code1 > 0)
|
||||
should.be_true(code2 > 0)
|
||||
}
|
||||
Reference in New Issue
Block a user