Current section
Files
Jump to
Current section
Files
src/totally.erl
-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(
<<Payload:64/integer>>,
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).