Packages

TOTP (Time-based One-Time Password) for Gleam

Current section

Files

Jump to
totally src totally.erl
Raw

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]).
-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(
<<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 = 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).