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
59 changes: 32 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ The functions below refer to the following common parameters:
| `Token` | string\* |

- `Interval` is an integer that represents the counter value, the "moving factor" referenced in [RFC 4226][rfc4226]. It is an 8 byte unsigned integer; if a negative and/or too large integer is passed, it will be 2's complemented and truncated appropriately.
- `Secret` is a base-32-encoded secret key. Generally, it should be at least 128 bits, preferably 160 bits.
- `Secret` is a base-32-encoded secret key by default. Generally, it should be at least 128 bits, preferably 160 bits. Use `{secret_encoding, raw}` when passing the raw binary key bytes directly.
- `Token` is a HOTP/TOTP value represented as a string\*. This is generally a 6-digit number, e.g., "123456", but its length may be modulated with the `token_length` option.

\*Note: for [Erlang][erlang] uses of `pot`, all strings should be in `binary()` format.
Expand All @@ -157,12 +157,14 @@ Elixir:

The following `Options` are allowed:

| Option | Type | Default |
|-----------------|-------------|---------|
| `digest_method` | atom | sha |
| `token_length` | integer > 0 | 6 |
| Option | Type | Default |
|-------------------|-------------------|---------|
| `digest_method` | atom | sha |
| `secret_encoding` | `base32` or `raw` | base32 |
| `token_length` | integer > 0 | 6 |

- `digest_method` controls the signing algorithm passed to the [Erlang][erlang] `crypto` module's [`hmac`][crypto_hmac] function. For [RFC 4226][rfc4226] compliant tokens, it must be set to `sha`. For [RFC 6238][rfc6238] compliant tokens, additional values such as `sha256` or `sha512` may be used.
- `secret_encoding` controls how `Secret` is interpreted, either as a base 32 binary (`base32`) or raw bytes binary (`raw`).
- `token_length` controls the number of digits in output `Token`.

#### `totp/1,2`
Expand All @@ -185,13 +187,14 @@ 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) |
| Option | Type | Default/Reference |
|-------------------|-------------------|---------------------------------------------|
| `addwindow` | integer | 0 |
| `digest_method` | atom | from [hotp/2,3](#hotp23) |
| `interval_length` | integer > 0 | 30 |
| `secret_encoding` | `base32` or `raw` | from [hotp/2,3](#hotp23) |
| `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].
- `interval_length` controls the number of seconds for the `Interval` computation.
Expand Down Expand Up @@ -243,13 +246,14 @@ Elixir:

The following `Options` are allowed:

| Option | Type | Default/Reference |
|-------------------|-------------|--------------------------|
| `digest_method` | atom | from [hotp/2,3](#hotp23) |
| `last` | integer | 1 |
| `return_interval` | boolean | false |
| `token_length` | integer > 0 | from [hotp/2,3](#hotp23) |
| `trials` | integer > 0 | 1000 |
| Option | Type | Default/Reference |
|-------------------|-------------------|--------------------------|
| `digest_method` | atom | from [hotp/2,3](#hotp23) |
| `last` | integer | 1 |
| `return_interval` | boolean | false |
| `secret_encoding` | `base32` or `raw` | from [hotp/2,3](#hotp23) |
| `token_length` | integer > 0 | from [hotp/2,3](#hotp23) |
| `trials` | integer > 0 | 1000 |

- `last` is the `Interval` value of the previous valid `Token`; the next `Interval` after `last` is used as the first candidate for validating the `Token`.
- `trials` controls the number of incremental `Interval` values after `last` to try when validating the `Token`. If a matching candidate is not found within `trials` attempts, the `Token` is considered invalid.
Expand All @@ -275,14 +279,15 @@ Elixir:

The following `Options` are allowed:

| Option | Type | Default/Reference |
|-------------------|-------------|--------------------------|
| `addwindow` | integer | from [totp/1,2](#totp12) |
| `digest_method` | atom | from [hotp/2,3](#hotp23) |
| `interval_length` | integer > 0 | 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 |
| Option | Type | Default/Reference |
|-------------------|-------------------|--------------------------|
| `addwindow` | integer | from [totp/1,2](#totp12) |
| `digest_method` | atom | from [hotp/2,3](#hotp23) |
| `interval_length` | integer > 0 | from [totp/1,2](#totp12) |
| `secret_encoding` | `base32` or `raw` | from [hotp/2,3](#hotp23) |
| `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.

Expand Down
12 changes: 11 additions & 1 deletion src/pot.erl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@
{interval_length, pos_integer()} |
{timestamp, erlang:timestamp()}.

-type secret_encoding() :: base32 | raw.

-type hotp_option() :: token_option() |
{secret_encoding, secret_encoding()} |
{digest_method, atom()}.

-type totp_option() :: hotp_option() | time_interval_option().
Expand Down Expand Up @@ -88,7 +91,7 @@ hotp(Secret, IntervalsNo) ->
hotp(Secret, IntervalsNo, Opts) ->
DigestMethod = proplists:get_value(digest_method, Opts, sha),
TokenLength = proplists:get_value(token_length, Opts, 6),
Key = pot_base32:decode(Secret),
Key = secret_key(Secret, Opts),
Msg = <<IntervalsNo:8/big-unsigned-integer-unit:8>>,
Digest = hmac(DigestMethod, Key, Msg),
<<Ob:8>> = binary:part(Digest, {byte_size(Digest), -1}),
Expand Down Expand Up @@ -176,6 +179,13 @@ valid_totp(Token, Secret, Opts) ->
%% Internal functions
%%==============================================================================

-spec secret_key(secret(), hotp_options()) -> binary().
secret_key(Secret, Opts) ->
case proplists:get_value(secret_encoding, Opts, base32) of
base32 -> pot_base32:decode(Secret);
raw -> Secret
end.

-spec time_interval(time_interval_options()) -> interval().
time_interval(Opts) ->
IntervalLength = proplists:get_value(interval_length, Opts, 30),
Expand Down
7 changes: 7 additions & 0 deletions test/hotp_generation_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ all_test_() ->
fun hotp_generation_for_different_intervals/1,
fun hotp_generation_with_padding/1,
fun hotp_generation_with_multiple_padding/1,
fun hotp_generation_with_raw_secret/1,
fun hotp_generation_unsigned_interval/1]}.


Expand Down Expand Up @@ -41,6 +42,12 @@ hotp_generation_with_multiple_padding(#{secret2 := Secret}) ->
[?_assertEqual(pot:hotp(Secret, 48930987), <<"000371">>)].


hotp_generation_with_raw_secret(#{secret1 := Secret}) ->
RawSecret = pot_base32:decode(Secret),
[?_assertEqual(pot:hotp(Secret, 1),
pot:hotp(RawSecret, 1, [{secret_encoding, raw}]))].


hotp_generation_unsigned_interval(#{secret1 := Secret}) ->
[{"negative intervals are equivalent to 64-bit unsigned representations (2^64 - 1)",
?_assertEqual(pot:hotp(Secret, 18446744073709551615),
Expand Down
9 changes: 8 additions & 1 deletion test/hotp_validity_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ checking_hotp_validity_without_range_test_() ->
fun validating_correct_totp_as_hotp/1,
fun retrieving_proper_interval_from_validator/1,
fun hotp_for_range_exact_match/1,
fun hotp_for_range_preceding_match/1]}.
fun hotp_for_range_preceding_match/1,
fun validating_hotp_with_raw_secret/1]}.


start() ->
Expand Down Expand Up @@ -89,3 +90,9 @@ hotp_for_range_preceding_match(_) ->
?_assertNot(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 2},
{return_interval, true}])),
?_assertEqual(pot:hotp(Secret, 4), Hotp)].


validating_hotp_with_raw_secret(Secret) ->
RawSecret = pot_base32:decode(Secret),
Token = pot:hotp(Secret, 123),
[?_assert(pot:valid_hotp(Token, RawSecret, [{secret_encoding, raw}]))].
13 changes: 13 additions & 0 deletions test/totp_generation_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ generating_totp_for_given_timestamp_and_compare_test_() ->
fun stop/1,
fun generating_totp_for_given_timestamp_and_compare/1}.

generating_totp_with_raw_secret_test_() ->
{setup, fun start/0,
fun stop/1,
fun generating_totp_with_raw_secret/1}.

start() ->
ok.

Expand All @@ -33,3 +38,11 @@ generating_totp_for_given_timestamp_and_compare(_) ->
Secret = <<"MFRGGZDFMZTWQ2LK">>,
Totp = pot:totp(Secret, [{timestamp, {1518, 179058, 919315}}]),
[?_assertEqual(Totp, <<"151469">>)].

generating_totp_with_raw_secret(_) ->
Secret = <<"MFRGGZDFMZTWQ2LK">>,
RawSecret = pot_base32:decode(Secret),
UnixTime = 1518179058,
[?_assertEqual(pot:totp(Secret, [{unix_time, UnixTime}]),
pot:totp(RawSecret, [{secret_encoding, raw},
{unix_time, UnixTime}]))].
11 changes: 10 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_with_raw_secret/1]}.


start() ->
Expand Down Expand Up @@ -55,3 +56,11 @@ 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_with_raw_secret(Secret) ->
RawSecret = pot_base32:decode(Secret),
UnixTime = 1518179058,
Token = pot:totp(Secret, [{unix_time, UnixTime}]),
[?_assert(pot:valid_totp(Token, RawSecret, [{secret_encoding, raw},
{unix_time, UnixTime}]))].