From 8871a9b2dd06065061ed3b810f38c096db876041 Mon Sep 17 00:00:00 2001 From: Sebastien Merle Date: Fri, 7 Aug 2026 18:34:06 +0200 Subject: [PATCH] Add Unix time option for TOTP generation and validation --- README.md | 44 ++++++++++++++++++++++++---------- src/pot.erl | 20 ++++++++++++---- test/totp_generation_tests.erl | 23 ++++++++++++++++++ test/totp_validity_tests.erl | 9 ++++++- 4 files changed, 79 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 4cba061..8069965 100644 --- a/README.md +++ b/README.md @@ -185,16 +185,18 @@ Elixir: The following `Options` are allowed: -| Option | Type | Default/Reference | -|-------------------|-------------|--------------------------| -| `addwindow` | integer | 0 | -| `digest_method` | atom | from [hotp/2,3](#hotp23) | -| `interval_length` | integer > 0 | 30 | -| `timestamp` | timestamp | [`os:timestamp()`][ts] | -| `token_length` | integer > 0 | from [hotp/2,3](#hotp23) | - -- `addwindow` acts as an offset to the `Interval` extrapolated from dividing the `timestamp` by the `interval_length` per the algorithm described in [RFC 6238][rfc6238]. +| Option | Type | Default/Reference | +|-------------------|-------------|---------------------------------------------| +| `addwindow` | integer | 0 | +| `digest_method` | atom | from [hotp/2,3](#hotp23) | +| `interval_length` | integer > 0 | 30 | +| `unix_time` | integer | [`erlang:system_time(second)`][system_time] | +| `timestamp` | timestamp | [`os:timestamp()`][ts] | +| `token_length` | integer > 0 | from [hotp/2,3](#hotp23) | + +- `addwindow` acts as an offset to the `Interval` extrapolated from dividing the time by the `interval_length` per the algorithm described in [RFC 6238][rfc6238]. - `interval_length` controls the number of seconds for the `Interval` computation. +- `unix_time` may be passed to specify custom Unix seconds to use for computing the `Interval` used to generate a `Token`. - `timestamp` may be passed to specify a custom timestamp (in Erlang [timestamp][ts] format) to use for computing the `Interval` used to generate a `Token`. ### Token Validation Functions @@ -280,11 +282,12 @@ The following `Options` are allowed: | `addwindow` | integer | from [totp/1,2](#totp12) | | `digest_method` | atom | from [hotp/2,3](#hotp23) | | `interval_length` | integer > 0 | from [totp/1,2](#totp12) | +| `unix_time` | integer | from [totp/1,2](#totp12) | | `timestamp` | timestamp | from [totp/1,2](#totp12) | | `token_length` | integer > 0 | from [hotp/2,3](#hotp23) | | `window` | integer > 0 | 0 | -- `window` is a range used for expanding `Interval` value derived from the `timestamp`. This is done by considering the `window` `Interval`s before *and* after the one derived from the `timestamp`. This allows validation to be relaxed to allow for successful validation of TOTP `Token`s generated by clients with some degree of unknown clock drift from the server, as well as some client entry delay. +- `window` is a range used for expanding `Interval` value derived from the time. This is done by considering the `window` `Interval`s before *and* after the one derived from the time. This allows validation to be relaxed to allow for successful validation of TOTP `Token`s generated by clients with some degree of unknown clock drift from the server, as well as some client entry delay. ## Examples (Erlang) @@ -359,7 +362,7 @@ IsValid = pot:valid_totp(Token, Secret, [{window, 1}, {addwindow, 1}]), ### Create a time based token for given time -Time format is `{MegaSecs, Secs, MicroSecs}` received by os:timestamp() +Using the time format `{MegaSecs, Secs, MicroSecs}` received by `os:timestamp()`: ```erlang Secret = <<"MFRGGZDFMZTWQ2LK">>, @@ -367,6 +370,14 @@ Token = pot:totp(Secret, [{timestamp, {1518, 179058, 919315}}]), % Token will be <<"151469">> ``` +Using Unix seconds: + +```erlang +Secret = <<"MFRGGZDFMZTWQ2LK">>, +Token = pot:totp(Secret, [{unix_time, 1518179058}]), +% Token will be <<"151469">> +``` + ## Examples (Elixir) ### Create a time based token @@ -440,7 +451,7 @@ is_valid = :pot.valid_totp(token, secret, [window: 1, addwindow: 1]) ### Create a time based token for given time -Time format is `{MegaSecs, Secs, MicroSecs}` received by :os.timestamp() +Using the time format `{MegaSecs, Secs, MicroSecs}` received by `:os.timestamp()`: ```elixir secret = "MFRGGZDFMZTWQ2LK" @@ -448,6 +459,14 @@ token = :pot.totp(secret, [timestamp: {1518, 179058, 919315}]) # Token will be <<"151469">> ``` +Using Unix seconds: + +```elixir +secret = "MFRGGZDFMZTWQ2LK" +token = :pot.totp(secret, [unix_time: 1518179058]) +# Token will be <<"151469">> +``` + ## Credits - Yüce Tekol @@ -491,3 +510,4 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SO [totp_wiki]: http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm [crypto_hmac]: http://erlang.org/doc/man/crypto.html#hmac-3 [ts]: http://erlang.org/doc/man/os.html#timestamp-0 +[system_time]: https://www.erlang.org/doc/apps/erts/erlang.html#system_time/1 diff --git a/src/pot.erl b/src/pot.erl index 754bf15..4ca859b 100644 --- a/src/pot.erl +++ b/src/pot.erl @@ -50,6 +50,7 @@ -type time_interval_option() :: {addwindow, integer()} | {interval_length, pos_integer()} | + {unix_time, integer()} | {timestamp, erlang:timestamp()}. -type hotp_option() :: token_option() | @@ -178,10 +179,21 @@ valid_totp(Token, Secret, Opts) -> -spec time_interval(time_interval_options()) -> interval(). time_interval(Opts) -> - IntervalLength = proplists:get_value(interval_length, Opts, 30), - AddSeconds = proplists:get_value(addwindow, Opts, 0) * proplists:get_value(interval_length, Opts, 30), - {MegaSecs, Secs, _} = proplists:get_value(timestamp, Opts, os:timestamp()), - trunc((MegaSecs * 1000000 + (Secs + AddSeconds)) / IntervalLength). + IntervalLength = proplists:get_value(interval_length, Opts, 30), + AddSeconds = proplists:get_value(addwindow, Opts, 0) * IntervalLength, + trunc((unix_time(Opts) + AddSeconds) / IntervalLength). + +-spec unix_time(time_interval_options()) -> integer(). +unix_time(Opts) -> + case proplists:get_value(unix_time, Opts) of + undefined -> + case proplists:get_value(timestamp, Opts) of + undefined -> erlang:system_time(second); + {MegaSecs, Secs, _} -> MegaSecs * 1000000 + Secs + end; + UnixTime -> + UnixTime + end. -spec check_candidate(token(), secret(), interval(), interval(), hotp_options()) -> interval() | false. diff --git a/test/totp_generation_tests.erl b/test/totp_generation_tests.erl index c1d27eb..589b539 100644 --- a/test/totp_generation_tests.erl +++ b/test/totp_generation_tests.erl @@ -13,6 +13,16 @@ generating_totp_for_given_timestamp_and_compare_test_() -> fun stop/1, fun generating_totp_for_given_timestamp_and_compare/1}. +generating_totp_for_given_unix_time_and_compare_test_() -> + {setup, fun start/0, + fun stop/1, + fun generating_totp_for_given_unix_time_and_compare/1}. + +generating_post_2038_totp_for_given_unix_time_test_() -> + {setup, fun start/0, + fun stop/1, + fun generating_post_2038_totp_for_given_unix_time/1}. + start() -> ok. @@ -33,3 +43,16 @@ generating_totp_for_given_timestamp_and_compare(_) -> Secret = <<"MFRGGZDFMZTWQ2LK">>, Totp = pot:totp(Secret, [{timestamp, {1518, 179058, 919315}}]), [?_assertEqual(Totp, <<"151469">>)]. + +generating_totp_for_given_unix_time_and_compare(_) -> + Secret = <<"MFRGGZDFMZTWQ2LK">>, + UnixTime = 1518179058, + Totp = pot:totp(Secret, [{unix_time, UnixTime}]), + [?_assertEqual(Totp, <<"151469">>)]. + +generating_post_2038_totp_for_given_unix_time(_) -> + Secret = <<"MFRGGZDFMZTWQ2LK">>, + UnixTime = 20000000000, + Totp = pot:totp(Secret, [{unix_time, UnixTime}]), + Hotp = pot:hotp(Secret, UnixTime div 30), + [?_assertEqual(Hotp, Totp)]. diff --git a/test/totp_validity_tests.erl b/test/totp_validity_tests.erl index 9c50ff2..c49376b 100644 --- a/test/totp_validity_tests.erl +++ b/test/totp_validity_tests.erl @@ -12,7 +12,8 @@ totp_validity_test_() -> fun validating_invalid_token_hotp/1, fun validating_correct_hotp_as_totp/1, fun validating_past_future_totp_too_small_window/1, - fun validating_past_future_totp_with_window/1]}. + fun validating_past_future_totp_with_window/1, + fun validating_totp_for_given_unix_time/1]}. start() -> @@ -55,3 +56,9 @@ validating_past_future_totp_with_window(Secret) -> Secret, [{window, W} | IntervalOpts])) || W <- lists:seq(0, N), AW <- lists:seq(-N, N), W >= abs(AW)]. + + +validating_totp_for_given_unix_time(Secret) -> + UnixTime = 1518179058, + Token = pot:totp(Secret, [{unix_time, UnixTime}]), + [?_assert(pot:valid_totp(Token, Secret, [{unix_time, UnixTime}]))].