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, 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
<<I:5, Rest/bitstring>> ->
do_encode32(Rest, <<Acc/binary, (base32_char(I))/binary>>);
<<I@1:4>> ->
<<Acc/binary, (base32_char(erlang:'bsl'(I@1, 1)))/binary>>;
<<I@2:3>> ->
<<Acc/binary, (base32_char(erlang:'bsl'(I@2, 2)))/binary>>;
<<I@3:2>> ->
<<Acc/binary, (base32_char(erlang:'bsl'(I@3, 3)))/binary>>;
<<I@4:1>> ->
<<Acc/binary, (base32_char(erlang:'bsl'(I@4, 4)))/binary>>;
_ ->
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 => <<?FILEPATH/utf8>>,
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 => <<?FILEPATH/utf8>>,
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 => <<?FILEPATH/utf8>>,
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(
<<Payload:64/integer>>,
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).