-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]). -opaque otp() :: {otp, binary()}. -type totp_algorithm() :: sha1 | sha256 | sha512. -type totp_config() :: {totp_config, bitstring(), integer(), integer(), integer(), integer(), totp_algorithm(), binary(), binary()}. -spec default_config() -> totp_config(). default_config() -> {totp_config, gleam_stdlib:identity(<<""/utf8>>), 0, 30, 0, 6, sha1, <<""/utf8>>, <<""/utf8>>}. -spec set_secret(totp_config(), bitstring()) -> totp_config(). set_secret(Config, Secret) -> erlang:setelement(2, Config, Secret). -spec set_issuer(totp_config(), binary()) -> totp_config(). set_issuer(Config, Issuer) -> erlang:setelement(8, Config, Issuer). -spec set_account(totp_config(), binary()) -> totp_config(). set_account(Config, Account) -> erlang:setelement(9, Config, Account). -spec set_time(totp_config(), integer()) -> totp_config(). set_time(Config, Time) -> erlang:setelement(3, Config, Time). -spec set_time_now(totp_config()) -> totp_config(). set_time_now(Config) -> erlang:setelement( 3, Config, begin _pipe = birl:utc_now(), birl:to_unix(_pipe) end ). -spec set_period(totp_config(), integer()) -> totp_config(). set_period(Config, Period) -> erlang:setelement(4, Config, Period). -spec set_last_use(totp_config(), integer()) -> totp_config(). set_last_use(Config, Last_use) -> erlang:setelement(5, Config, Last_use). -spec set_last_use_now(totp_config()) -> totp_config(). set_last_use_now(Config) -> erlang:setelement( 5, Config, begin _pipe = birl:utc_now(), birl:to_unix(_pipe) end ). -spec set_digits(totp_config(), integer()) -> totp_config(). set_digits(Config, Digits) -> erlang:setelement(6, Config, Digits). -spec set_algorithm(totp_config(), totp_algorithm()) -> totp_config(). set_algorithm(Config, Algorithm) -> erlang:setelement(7, Config, Algorithm). -spec secret() -> bitstring(). secret() -> crypto:strong_rand_bytes(20). -spec secret_with_size(integer()) -> bitstring(). secret_with_size(Size) -> crypto:strong_rand_bytes(Size). -spec otp_to_string(otp()) -> binary(). otp_to_string(Otp) -> {otp, Otp@1} = Otp, Otp@1. -spec valid_otp_code(binary()) -> boolean(). valid_otp_code(Otp) -> _assert_subject = gleam@regex: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 => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"totally"/utf8>>, function => <<"valid_otp_code"/utf8>>, line => 256}) end, gleam@regex:check(Re, Otp). -spec string_to_otp(binary()) -> {ok, otp()} | {error, binary()}. string_to_otp(Otp) -> case gleam@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. -spec otpauth_uri_from_config(totp_config()) -> binary(). otpauth_uri_from_config(Config) -> Issuer = gleam@uri: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@uri:percent_encode(erlang:element(9, Config)), <<"?secret="/utf8>>, totally_ffi:encode32(erlang:element(2, Config)), <<"&issuer="/utf8>>, Issuer, <<"&algorithm="/utf8>>, Algo, <<"&digits="/utf8>>, gleam@int:to_string(erlang:element(6, Config)), <<"&period="/utf8>>, gleam@int:to_string(erlang:element(4, Config))], <<""/utf8>> ). -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). -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 => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 269}) 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 => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@1, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 270}) end, <<_:1, Bits:31/integer>> = case Part of <<_:1, _:31/integer>> -> Part; _assert_fail@2 -> erlang:error(#{gleam_error => let_assert, message => <<"Assertion pattern match failed"/utf8>>, value => _assert_fail@2, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 271}) end, Bits. -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, gleam@int:to_float(erlang:element(6, Config)) ), _pipe@2 = gleam@result:unwrap(_pipe@1, +0.0), gleam@float:truncate(_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 = gleam@int:to_string(_pipe@6), _pipe@8 = gleam@string:pad_left( _pipe@7, erlang:element(6, Config), <<"0"/utf8>> ), {otp, _pipe@8}. -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). -spec verify(bitstring(), binary()) -> boolean(). verify(Secret, Totp_input) -> totp(Secret) =:= {otp, Totp_input}. -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. -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).