-module(totally). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch]). -export([default_config/0, set_secret/2, set_issuer/2, set_account/2, set_time/2, set_time_now/1, set_period/2, set_last_use/2, set_last_use_now/1, set_digits/2, set_algorithm/2, secret/0, secret_with_size/1, otp_to_string/1, string_to_otp/1, otpauth_uri_from_config/1, otpauth_uri/3, totp_from_config/1, totp/1, verify/2, verify_from_config/2, verify_with_last_use/3]). -export_type([otp/0, totp_algorithm/0, totp_config/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -opaque otp() :: {otp, binary()}. -type totp_algorithm() :: sha1 | sha256 | sha512. -type totp_config() :: {totp_config, bitstring(), integer(), integer(), integer(), integer(), totp_algorithm(), binary(), binary()}. -file("src/totally.gleam", 46). ?DOC( " Creates a default configuration for TOTP with the following values:\n" " algorithm: Sha1, period: 30, digits: 6. These are the most commonly used TOTP settings.\n" " Please set the secret and time with the `set_secret` and `set_time_now` or manually `set_time` functions.\n" ). -spec default_config() -> totp_config(). default_config() -> {totp_config, gleam_stdlib:identity(<<""/utf8>>), 0, 30, 0, 6, sha1, <<""/utf8>>, <<""/utf8>>}. -file("src/totally.gleam", 60). ?DOC(" Sets the secret for the TOTP configuration.\n"). -spec set_secret(totp_config(), bitstring()) -> totp_config(). set_secret(Config, Secret) -> _record = Config, {totp_config, Secret, erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 66). ?DOC( " Sets the issuer for the TOTP configuration.\n" " Used for the otpauth URI.\n" ). -spec set_issuer(totp_config(), binary()) -> totp_config(). set_issuer(Config, Issuer) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), Issuer, erlang:element(9, _record)}. -file("src/totally.gleam", 71). ?DOC(" Sets the account for the TOTP configuration.\n"). -spec set_account(totp_config(), binary()) -> totp_config(). set_account(Config, Account) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), Account}. -file("src/totally.gleam", 76). ?DOC(" Sets the time in unix timestamp seconds for the TOTP configuration.\n"). -spec set_time(totp_config(), integer()) -> totp_config(). set_time(Config, Time) -> _record = Config, {totp_config, erlang:element(2, _record), Time, erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 81). ?DOC(" Sets the time for the TOTP configuration to the current time.\n"). -spec set_time_now(totp_config()) -> totp_config(). set_time_now(Config) -> _record = Config, {totp_config, erlang:element(2, _record), begin _pipe = gleam@time@timestamp:system_time(), _pipe@1 = gleam@time@timestamp:to_unix_seconds(_pipe), erlang:trunc(_pipe@1) end, erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 92). ?DOC( " Sets the refresh period in seconds for the TOTP configuration.\n" " Most commonly used is 30 seconds.\n" ). -spec set_period(totp_config(), integer()) -> totp_config(). set_period(Config, Period) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), Period, erlang:element(5, _record), erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 98). ?DOC( " Sets the last use time in unix timestamp seconds for the TOTP configuration.\n" " Used to prevent replay attacks.\n" ). -spec set_last_use(totp_config(), integer()) -> totp_config(). set_last_use(Config, Last_use) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), Last_use, erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 103). ?DOC(" Sets the last use time for the TOTP configuration to the current time.\n"). -spec set_last_use_now(totp_config()) -> totp_config(). set_last_use_now(Config) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), begin _pipe = gleam@time@timestamp:system_time(), _pipe@1 = gleam@time@timestamp:to_unix_seconds(_pipe), erlang:trunc(_pipe@1) end, erlang:element(6, _record), erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 115). ?DOC( " Sets the digits for the TOTP configuration.\n" " Most commonly used is 6 digits.\n" " The spec allows for 6 to 8 digits.\n" ). -spec set_digits(totp_config(), integer()) -> totp_config(). set_digits(Config, Digits) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), Digits, erlang:element(7, _record), erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 121). ?DOC( " Sets the algorithm for the TOTP configuration.\n" " Most commonly used is Sha1.\n" ). -spec set_algorithm(totp_config(), totp_algorithm()) -> totp_config(). set_algorithm(Config, Algorithm) -> _record = Config, {totp_config, erlang:element(2, _record), erlang:element(3, _record), erlang:element(4, _record), erlang:element(5, _record), erlang:element(6, _record), Algorithm, erlang:element(8, _record), erlang:element(9, _record)}. -file("src/totally.gleam", 127). ?DOC( " Generates a random 20 byte secret.\n" " 20 bytes is the recommended size according to the HOTP RFC4226 (https://tools.ietf.org/html/rfc4226#section-4).\n" ). -spec secret() -> bitstring(). secret() -> crypto:strong_rand_bytes(20). -file("src/totally.gleam", 132). ?DOC(" Generates a random secret with the given size.\n"). -spec secret_with_size(integer()) -> bitstring(). secret_with_size(Size) -> crypto:strong_rand_bytes(Size). -file("src/totally.gleam", 202). ?DOC(" Converts the OTP to a string.\n"). -spec otp_to_string(otp()) -> binary(). otp_to_string(Otp) -> {otp, Otp@1} = Otp, Otp@1. -file("src/totally.gleam", 265). ?DOC(" Checks if the string fits the otp format.\n"). -spec valid_otp_code(binary()) -> boolean(). valid_otp_code(Otp) -> _assert_subject = gleam@regexp:from_string(<<"^[0-9]{6,8}$"/utf8>>), {ok, Re} = case _assert_subject of {ok, _} -> _assert_subject; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail, module => <<"totally"/utf8>>, function => <<"valid_otp_code"/utf8>>, line => 266}) end, gleam@regexp:check(Re, Otp). -file("src/totally.gleam", 208). ?DOC(" Converts a valid OTP string to an OTP type.\n"). -spec string_to_otp(binary()) -> {ok, otp()} | {error, binary()}. string_to_otp(Otp) -> case string:length(Otp) of 6 -> case valid_otp_code(Otp) of true -> {ok, {otp, Otp}}; false -> {error, <<"Invalid OTP"/utf8>>} end; 7 -> case valid_otp_code(Otp) of true -> {ok, {otp, Otp}}; false -> {error, <<"Invalid OTP"/utf8>>} end; 8 -> case valid_otp_code(Otp) of true -> {ok, {otp, Otp}}; false -> {error, <<"Invalid OTP"/utf8>>} end; _ -> {error, <<"Invalid OTP length"/utf8>>} end. -file("src/totally.gleam", 234). ?DOC(" Generates an otpauth URI for the given TOTP configuration.\n"). -spec otpauth_uri_from_config(totp_config()) -> binary(). otpauth_uri_from_config(Config) -> Issuer = gleam_stdlib:percent_encode(erlang:element(8, Config)), Algo = case erlang:element(7, Config) of sha1 -> <<"SHA1"/utf8>>; sha256 -> <<"SHA256"/utf8>>; sha512 -> <<"SHA512"/utf8>> end, gleam@string:join( [<<"otpauth://totp/"/utf8>>, Issuer, <<":"/utf8>>, gleam_stdlib:percent_encode(erlang:element(9, Config)), <<"?secret="/utf8>>, totally_ffi:encode32(erlang:element(2, Config)), <<"&issuer="/utf8>>, Issuer, <<"&algorithm="/utf8>>, Algo, <<"&digits="/utf8>>, erlang:integer_to_binary(erlang:element(6, Config)), <<"&period="/utf8>>, erlang:integer_to_binary(erlang:element(4, Config))], <<""/utf8>> ). -file("src/totally.gleam", 221). ?DOC( " Generates an otpauth URI for the given secret, issuer and account name.\n" " The otpauth URI is used to generate QR codes for TOTP.\n" ). -spec otpauth_uri(bitstring(), binary(), binary()) -> binary(). otpauth_uri(Secret, Issuer, Account_name) -> _pipe = default_config(), _pipe@1 = set_secret(_pipe, Secret), _pipe@2 = set_issuer(_pipe@1, Issuer), _pipe@3 = set_account(_pipe@2, Account_name), otpauth_uri_from_config(_pipe@3). -file("src/totally.gleam", 277). ?DOC(" Extracts the OTP bits from the HMAC hash.\n"). -spec extract_otp_bits(bitstring()) -> integer(). extract_otp_bits(Hmac) -> Off_offset = (erlang:byte_size(Hmac) * 8) - 4, <<_:Off_offset, Offset:4/integer>> = case Hmac of <<_:Off_offset, _:4/integer>> -> Hmac; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 279}) end, <<_:Offset/binary, Part:4/binary, _/binary>> = case Hmac of <<_:Offset/binary, _:4/binary, _/binary>> -> Hmac; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail@1, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 280}) end, <<_:1, Bits:31/integer>> = case Part of <<_:1, _:31/integer>> -> Part; _assert_fail@2 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, value => _assert_fail@2, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 281}) end, Bits. -file("src/totally.gleam", 145). ?DOC(" Generates a TOTP using the given TOTP configuration.\n"). -spec totp_from_config(totp_config()) -> otp(). totp_from_config(Config) -> Payload = begin _pipe = gleam@int:floor_divide( erlang:element(3, Config), erlang:element(4, Config) ), gleam@result:unwrap(_pipe, 0) end, Rem_digits = begin _pipe@1 = gleam@int:power(10, erlang:float(erlang:element(6, Config))), _pipe@2 = gleam@result:unwrap(_pipe@1, +0.0), erlang:trunc(_pipe@2) end, Algo = case erlang:element(7, Config) of sha1 -> sha1; sha256 -> sha256; sha512 -> sha512 end, _pipe@3 = gleam_crypto_ffi:hmac( <>, Algo, erlang:element(2, Config) ), _pipe@4 = extract_otp_bits(_pipe@3), _pipe@5 = gleam@int:remainder(_pipe@4, Rem_digits), _pipe@6 = gleam@result:unwrap(_pipe@5, 0), _pipe@7 = erlang:integer_to_binary(_pipe@6), _pipe@8 = gleam@string:pad_start( _pipe@7, erlang:element(6, Config), <<"0"/utf8>> ), {otp, _pipe@8}. -file("src/totally.gleam", 137). ?DOC(" Generates a TOTP using the given secret and default configuration.\n"). -spec totp(bitstring()) -> otp(). totp(Secret) -> _pipe = default_config(), _pipe@1 = set_secret(_pipe, Secret), _pipe@2 = set_time_now(_pipe@1), totp_from_config(_pipe@2). -file("src/totally.gleam", 172). ?DOC(" Verifies the given TOTP input with the given secret.\n"). -spec verify(bitstring(), binary()) -> boolean(). verify(Secret, Totp_input) -> totp(Secret) =:= {otp, Totp_input}. -file("src/totally.gleam", 192). ?DOC(" Verifies the given TOTP input with the given TOTP configuration.\n"). -spec verify_from_config(totp_config(), binary()) -> boolean(). verify_from_config(Config, Totp_input) -> Match = totp_from_config(Config) =:= {otp, Totp_input}, Reused = begin _pipe = gleam@int:floor_divide( erlang:element(3, Config), erlang:element(4, Config) ), gleam@result:unwrap(_pipe, 0) end =< begin _pipe@1 = gleam@int:floor_divide( erlang:element(5, Config), erlang:element(4, Config) ), gleam@result:unwrap(_pipe@1, 0) end, Match andalso not Reused. -file("src/totally.gleam", 178). ?DOC( " Verifies the given TOTP input with the given secret and last use time.\n" " The last use time is used to prevent replay attacks.\n" ). -spec verify_with_last_use(bitstring(), binary(), integer()) -> boolean(). verify_with_last_use(Secret, Totp_input, Last_use) -> Config = begin _pipe = default_config(), _pipe@1 = set_secret(_pipe, Secret), _pipe@2 = set_time_now(_pipe@1), set_last_use(_pipe@2, Last_use) end, verify_from_config(Config, Totp_input).