Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -359,14 +362,22 @@ 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">>,
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
Expand Down Expand Up @@ -440,14 +451,22 @@ 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"
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
Expand Down Expand Up @@ -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
20 changes: 16 additions & 4 deletions src/pot.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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() |
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions test/totp_generation_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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)].
9 changes: 8 additions & 1 deletion test/totp_validity_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -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() ->
Expand Down Expand Up @@ -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}]))].