-module(totally). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/totally.gleam"). -export([new/1, 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, is_valid/2, is_valid_from_config/2, is_valid_with_last_use/3]). -export_type([otp/0, totp_algorithm/0, digits/0, totp_config/0, totp_error/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 digits() :: six | seven | eight. -opaque totp_config() :: {totp_config, bitstring(), gleam@time@timestamp:timestamp(), integer(), gleam@time@timestamp:timestamp(), digits(), totp_algorithm(), binary(), binary()}. -type totp_error() :: insecure_secret | invalid_period | invalid_otp | invalid_otp_length. -file("src/totally.gleam", 60). ?DOC( " Creates a TOTP configuration with the given secret and default values:\n" " algorithm: Sha1, period: 30, digits: 6. These are the most commonly used TOTP settings.\n" " The secret must be at least 16 bytes (128 bits).\n" ). -spec new(bitstring()) -> {ok, totp_config()} | {error, totp_error()}. new(Secret) -> gleam@bool:guard( erlang:byte_size(Secret) < 16, {error, insecure_secret}, fun() -> {ok, {totp_config, Secret, gleam@time@timestamp:from_unix_seconds(0), 30, gleam@time@timestamp:from_unix_seconds(0), six, sha1, <<""/utf8>>, <<""/utf8>>}} end ). -file("src/totally.gleam", 79). ?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) -> {totp_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config), Issuer, erlang:element(9, Config)}. -file("src/totally.gleam", 84). ?DOC(" Sets the account for the TOTP configuration.\n"). -spec set_account(totp_config(), binary()) -> totp_config(). set_account(Config, Account) -> {totp_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config), erlang:element(8, Config), Account}. -file("src/totally.gleam", 91). ?DOC( " Sets the time for OTP generation.\n" " This is only used by `totp_from_config`. Verification functions\n" " use the current time automatically.\n" ). -spec set_time(totp_config(), gleam@time@timestamp:timestamp()) -> totp_config(). set_time(Config, Time) -> {totp_config, erlang:element(2, Config), Time, erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config), erlang:element(8, Config), erlang:element(9, Config)}. -file("src/totally.gleam", 98). ?DOC( " Sets the time for OTP generation to the current time.\n" " This is only used by `totp_from_config`. Verification functions\n" " use the current time automatically.\n" ). -spec set_time_now(totp_config()) -> totp_config(). set_time_now(Config) -> {totp_config, erlang:element(2, Config), gleam@time@timestamp:system_time(), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config), erlang:element(8, Config), erlang:element(9, Config)}. -file("src/totally.gleam", 104). ?DOC( " Sets the refresh period in seconds for the TOTP configuration.\n" " Must be greater than 0.\n" ). -spec set_period(totp_config(), integer()) -> {ok, totp_config()} | {error, totp_error()}. set_period(Config, Period) -> gleam@bool:guard( Period =< 0, {error, invalid_period}, fun() -> {ok, {totp_config, erlang:element(2, Config), erlang:element(3, Config), Period, erlang:element(5, Config), erlang:element(6, Config), erlang:element(7, Config), erlang:element(8, Config), erlang:element(9, Config)}} end ). -file("src/totally.gleam", 114). ?DOC( " Sets the last use time for the TOTP configuration.\n" " Used to prevent replay attacks.\n" ). -spec set_last_use(totp_config(), gleam@time@timestamp:timestamp()) -> totp_config(). set_last_use(Config, Last_use) -> {totp_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), Last_use, erlang:element(6, Config), erlang:element(7, Config), erlang:element(8, Config), erlang:element(9, Config)}. -file("src/totally.gleam", 122). ?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) -> {totp_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), gleam@time@timestamp:system_time(), erlang:element(6, Config), erlang:element(7, Config), erlang:element(8, Config), erlang:element(9, Config)}. -file("src/totally.gleam", 127). ?DOC(" Sets the digits for the TOTP configuration.\n"). -spec set_digits(totp_config(), digits()) -> totp_config(). set_digits(Config, Digits) -> {totp_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), Digits, erlang:element(7, Config), erlang:element(8, Config), erlang:element(9, Config)}. -file("src/totally.gleam", 133). ?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) -> {totp_config, erlang:element(2, Config), erlang:element(3, Config), erlang:element(4, Config), erlang:element(5, Config), erlang:element(6, Config), Algorithm, erlang:element(8, Config), erlang:element(9, Config)}. -file("src/totally.gleam", 139). ?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", 145). ?DOC( " Generates a random secret with the given size.\n" " Must be at least 16 bytes.\n" ). -spec secret_with_size(integer()) -> {ok, bitstring()} | {error, totp_error()}. secret_with_size(Size) -> case Size < 16 of false -> {ok, crypto:strong_rand_bytes(Size)}; true -> {error, insecure_secret} end. -file("src/totally.gleam", 224). ?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", 288). -spec digits_to_int(digits()) -> integer(). digits_to_int(Digits) -> case Digits of six -> 6; seven -> 7; eight -> 8 end. -file("src/totally.gleam", 296). -spec digits_to_power(digits()) -> integer(). digits_to_power(Digits) -> case Digits of six -> 1000000; seven -> 10000000; eight -> 100000000 end. -file("src/totally.gleam", 304). -spec timestep(gleam@time@timestamp:timestamp(), integer()) -> integer(). timestep(Time, Period) -> _pipe@1 = gleam@int:floor_divide( begin _pipe = gleam@time@timestamp:to_unix_seconds(Time), erlang:trunc(_pipe) end, Period ), gleam@result:unwrap(_pipe@1, 0). -file("src/totally.gleam", 310). ?DOC(" Checks if the string fits the otp format.\n"). -spec all_digits(binary()) -> boolean(). all_digits(Otp) -> _pipe = gleam@string:to_graphemes(Otp), gleam@list:all(_pipe, fun(C) -> case C of <<"0"/utf8>> -> true; <<"1"/utf8>> -> true; <<"2"/utf8>> -> true; <<"3"/utf8>> -> true; <<"4"/utf8>> -> true; <<"5"/utf8>> -> true; <<"6"/utf8>> -> true; <<"7"/utf8>> -> true; <<"8"/utf8>> -> true; <<"9"/utf8>> -> true; _ -> false end end). -file("src/totally.gleam", 230). ?DOC(" Converts a valid OTP string to an OTP type.\n"). -spec string_to_otp(binary()) -> {ok, otp()} | {error, totp_error()}. string_to_otp(Otp) -> case string:length(Otp) of 6 -> case all_digits(Otp) of true -> {ok, {otp, Otp}}; false -> {error, invalid_otp} end; 7 -> case all_digits(Otp) of true -> {ok, {otp, Otp}}; false -> {error, invalid_otp} end; 8 -> case all_digits(Otp) of true -> {ok, {otp, Otp}}; false -> {error, invalid_otp} end; _ -> {error, invalid_otp_length} end. -file("src/totally.gleam", 336). -spec base32_char(integer()) -> binary(). base32_char(I) -> case I of 0 -> <<"A"/utf8>>; 1 -> <<"B"/utf8>>; 2 -> <<"C"/utf8>>; 3 -> <<"D"/utf8>>; 4 -> <<"E"/utf8>>; 5 -> <<"F"/utf8>>; 6 -> <<"G"/utf8>>; 7 -> <<"H"/utf8>>; 8 -> <<"I"/utf8>>; 9 -> <<"J"/utf8>>; 10 -> <<"K"/utf8>>; 11 -> <<"L"/utf8>>; 12 -> <<"M"/utf8>>; 13 -> <<"N"/utf8>>; 14 -> <<"O"/utf8>>; 15 -> <<"P"/utf8>>; 16 -> <<"Q"/utf8>>; 17 -> <<"R"/utf8>>; 18 -> <<"S"/utf8>>; 19 -> <<"T"/utf8>>; 20 -> <<"U"/utf8>>; 21 -> <<"V"/utf8>>; 22 -> <<"W"/utf8>>; 23 -> <<"X"/utf8>>; 24 -> <<"Y"/utf8>>; 25 -> <<"Z"/utf8>>; 26 -> <<"2"/utf8>>; 27 -> <<"3"/utf8>>; 28 -> <<"4"/utf8>>; 29 -> <<"5"/utf8>>; 30 -> <<"6"/utf8>>; 31 -> <<"7"/utf8>>; _ -> <<""/utf8>> end. -file("src/totally.gleam", 325). -spec do_encode32(bitstring(), binary()) -> binary(). do_encode32(Input, Acc) -> case Input of <> -> do_encode32(Rest, <>); <> -> <>; <> -> <>; <> -> <>; <> -> <>; _ -> Acc end. -file("src/totally.gleam", 321). ?DOC(" Encodes the given BitArray to a base32 string.\n"). -spec encode32(bitstring()) -> binary(). encode32(Input) -> do_encode32(Input, <<""/utf8>>). -file("src/totally.gleam", 258). ?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>>, encode32(erlang:element(2, Config)), <<"&issuer="/utf8>>, Issuer, <<"&algorithm="/utf8>>, Algo, <<"&digits="/utf8>>, erlang:integer_to_binary(digits_to_int(erlang:element(6, Config))), <<"&period="/utf8>>, erlang:integer_to_binary(erlang:element(4, Config))], <<""/utf8>> ). -file("src/totally.gleam", 244). ?DOC( " Generates an otpauth URI for the given secret, issuer and account name.\n" " The secret must be at least 16 bytes (128 bits).\n" " The otpauth URI is used to generate QR codes for TOTP.\n" ). -spec otpauth_uri(bitstring(), binary(), binary()) -> {ok, binary()} | {error, totp_error()}. otpauth_uri(Secret, Issuer, Account_name) -> gleam@result:'try'(new(Secret), fun(Config) -> _pipe = Config, _pipe@1 = set_issuer(_pipe, Issuer), _pipe@2 = set_account(_pipe@1, Account_name), _pipe@3 = otpauth_uri_from_config(_pipe@2), {ok, _pipe@3} end). -file("src/totally.gleam", 375). ?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, Offset@1 = case Hmac of <<_:Off_offset, Offset:4/integer>> -> Offset; _assert_fail -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 377, value => _assert_fail, start => 9643, 'end' => 9703, pattern_start => 9654, pattern_end => 9696}) end, Part@1 = case Hmac of <<_:Offset@1/binary, Part:4/binary, _/binary>> -> Part; _assert_fail@1 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 378, value => _assert_fail@1, start => 9706, 'end' => 9777, pattern_start => 9717, pattern_end => 9770}) end, Bits@1 = case Part@1 of <<_:1, Bits:31/integer>> -> Bits; _assert_fail@2 -> erlang:error(#{gleam_error => let_assert, message => <<"Pattern match failed, no pattern matched the value."/utf8>>, file => <>, module => <<"totally"/utf8>>, function => <<"extract_otp_bits"/utf8>>, line => 379, value => _assert_fail@2, start => 9780, 'end' => 9830, pattern_start => 9791, pattern_end => 9823}) end, Bits@1. -file("src/totally.gleam", 164). ?DOC( " Generates a TOTP using the given TOTP configuration.\n" " Make sure to set the time with `set_time` or `set_time_now` first.\n" ). -spec totp_from_config(totp_config()) -> otp(). totp_from_config(Config) -> Payload = timestep(erlang:element(3, Config), erlang:element(4, Config)), Num_digits = digits_to_int(erlang:element(6, Config)), Rem_digits = digits_to_power(erlang:element(6, Config)), Algo = case erlang:element(7, Config) of sha1 -> sha1; sha256 -> sha256; sha512 -> sha512 end, _pipe = gleam_crypto_ffi:hmac( <>, Algo, erlang:element(2, Config) ), _pipe@1 = extract_otp_bits(_pipe), _pipe@2 = gleam@int:remainder(_pipe@1, Rem_digits), _pipe@3 = gleam@result:unwrap(_pipe@2, 0), _pipe@4 = erlang:integer_to_binary(_pipe@3), _pipe@5 = gleam@string:pad_start(_pipe@4, Num_digits, <<"0"/utf8>>), {otp, _pipe@5}. -file("src/totally.gleam", 154). ?DOC( " Generates a TOTP using the given secret and default configuration.\n" " The secret must be at least 16 bytes (128 bits).\n" ). -spec totp(bitstring()) -> {ok, otp()} | {error, totp_error()}. totp(Secret) -> gleam@result:'try'(new(Secret), fun(Config) -> _pipe = Config, _pipe@1 = set_time_now(_pipe), _pipe@2 = totp_from_config(_pipe@1), {ok, _pipe@2} end). -file("src/totally.gleam", 188). ?DOC( " Checks if the given TOTP input matches the current code for the secret.\n" " Does not check for replay attacks. Use `is_valid_with_last_use` or\n" " `is_valid_from_config` with `set_last_use` for replay protection.\n" ). -spec is_valid(bitstring(), binary()) -> {ok, boolean()} | {error, totp_error()}. is_valid(Secret, Totp_input) -> gleam@result:'try'( totp(Secret), fun(Otp) -> {ok, Otp =:= {otp, Totp_input}} end ). -file("src/totally.gleam", 212). ?DOC( " Checks if the given TOTP input matches the current code for the config.\n" " Automatically uses the current time for verification.\n" ). -spec is_valid_from_config(totp_config(), binary()) -> boolean(). is_valid_from_config(Config, Totp_input) -> Now = gleam@time@timestamp:system_time(), Match = begin _pipe = set_time(Config, Now), totp_from_config(_pipe) end =:= {otp, Totp_input}, Reused = timestep(Now, erlang:element(4, Config)) =< timestep( erlang:element(5, Config), erlang:element(4, Config) ), Match andalso not Reused. -file("src/totally.gleam", 198). ?DOC( " Checks if the given TOTP input matches the current code for the secret,\n" " rejecting codes that were already used in the same time window as `last_use`.\n" ). -spec is_valid_with_last_use( bitstring(), binary(), gleam@time@timestamp:timestamp() ) -> {ok, boolean()} | {error, totp_error()}. is_valid_with_last_use(Secret, Totp_input, Last_use) -> gleam@result:'try'(new(Secret), fun(Config) -> _pipe = Config, _pipe@1 = set_last_use(_pipe, Last_use), _pipe@2 = is_valid_from_config(_pipe@1, Totp_input), {ok, _pipe@2} end).