Skip to content

Commit 21dbd07

Browse files
committed
Add return_interval option to valid_hotp
There is a flaw in the `valid_hotp` logic introduced in #15 that prevents it from being effectively used: because it returns a raw `boolean`, there is no way to know *which* interval matched: the next one, the 1000th one, or somewhere in between? In order to effectively implement a proper HOTP scheme, you need to track the interval number of the last valid HOTP to prevent its reuse, OR the reuse of a HOTP from any previous interval. For backwards compatibility, I have retained the default `boolean()` response, but if the `return_interval` option is set, the interval will be returned when a hotp is valid, like `{true, interval()}`.
1 parent c9f5e18 commit 21dbd07

File tree

3 files changed

+73
-14
lines changed

3 files changed

+73
-14
lines changed

README.md

+34-2
Original file line numberDiff line numberDiff line change
@@ -204,14 +204,14 @@ Erlang:
204204

205205
```
206206
pot:valid_hotp(Token, Secret) -> Boolean
207-
pot:valid_hotp(Token, Secret, Options) -> Boolean
207+
pot:valid_hotp(Token, Secret, Options) -> Boolean | {true, interval()}
208208
```
209209

210210
Elixir:
211211

212212
```
213213
:pot.valid_hotp(Token, Secret) -> Boolean
214-
:pot.valid_hotp(Token, Secret, Options) -> Boolean
214+
:pot.valid_hotp(Token, Secret, Options) -> Boolean | {true, interval()}
215215
```
216216

217217
The following `Options` are allowed:
@@ -220,11 +220,13 @@ The following `Options` are allowed:
220220
|-------------------|-------------|--------------------------|
221221
| `digest_method` | atom | from [hotp/2,3](#hotp23) |
222222
| `last` | integer | 1 |
223+
| `return_interval` | boolean | false |
223224
| `token_length` | integer > 0 | from [hotp/2,3](#hotp23) |
224225
| `trials` | integer > 0 | 1000 |
225226

226227
- `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`.
227228
- `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.
229+
- `return_interval` controls whether the matching `Interval` of a valid `Token` is returned with the result. if set to `true`, then `valid_hotp/2` will return `{true, Interval}` (e.g., `{true, 123}`) when a valid `Token` is provided.
228230

229231
#### `valid_totp/2,3`
230232

@@ -297,6 +299,20 @@ IsValid = pot:valid_hotp(Token, Secret, [{last, LastUsed}]),
297299
% Do something
298300
```
299301

302+
Alternately, to get the last interval from a validated token:
303+
304+
```erlang
305+
Secret = <<"MFRGGZDFMZTWQ2LK">>,
306+
Token = <<"123456">>,
307+
LastUsed = 5, % last successful trial
308+
Options = [{last, LastUsed}, {return_interval, true}],
309+
NewLastUsed = case pot:valid_hotp(Token, Secret, Options) of
310+
{true, LastInterval} -> LastInterval;
311+
false -> LastUsed
312+
end,
313+
% Do something
314+
```
315+
300316
### Create a time based token with 30 seconds ahead
301317

302318
```erlang
@@ -362,6 +378,22 @@ is_valid = :pot.valid_hotp(token, secret, [{:last, last_used}])
362378
# Do something
363379
```
364380

381+
Alternately, to get the last interval from a validated token:
382+
383+
```elixir
384+
secret = "MFRGGZDFMZTWQ2LK"
385+
token = "123456"
386+
last_used = 5 # last successful trial
387+
options = [{:last, last_used}, {:return_token, true}]
388+
new_last_used =
389+
case :pot.valid_hotp(token, secret, options) do
390+
{true, last_interval} -> last_interval
391+
false -> last_used
392+
end
393+
# Do something
394+
```
395+
396+
365397
### Create a time based token with 30 seconds ahead
366398

367399
```elixir

src/pot.erl

+12-3
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959

6060
-type valid_hotp_option() :: hotp_option() |
6161
{last, interval()} |
62+
return_interval | {return_interval, boolean()} |
6263
{trials, pos_integer()}.
6364

6465
-type valid_totp_option() :: totp_option() |
@@ -73,6 +74,8 @@
7374

7475
-type time_interval_options() :: [time_interval_option()].
7576

77+
-type valid_hotp_return() :: boolean() | {true, LastInterval :: interval()}.
78+
7679
%%==============================================================================
7780
%% Token generation
7881
%%==============================================================================
@@ -127,7 +130,7 @@ valid_token(Token, Opts) when is_binary(Token) ->
127130
valid_hotp(Token, Secret) ->
128131
valid_hotp(Token, Secret, []).
129132

130-
-spec valid_hotp(token(), secret(), valid_hotp_options()) -> boolean().
133+
-spec valid_hotp(token(), secret(), valid_hotp_options()) -> valid_hotp_return().
131134
valid_hotp(Token, Secret, Opts) ->
132135
Last = proplists:get_value(last, Opts, 1),
133136
Trials = proplists:get_value(trials, Opts, 1000),
@@ -137,8 +140,8 @@ valid_hotp(Token, Secret, Opts) ->
137140
case check_candidate(Token, Secret, Last + 1, Last + Trials, Opts) of
138141
false ->
139142
false;
140-
_ ->
141-
true
143+
LastInterval ->
144+
valid_hotp_return(LastInterval, proplists:get_value(return_interval, Opts, false))
142145
end;
143146
_ ->
144147
false
@@ -196,3 +199,9 @@ check_candidate(_Token, _Secret, _Current, _Last, _Opts) ->
196199
prepend_zeros(Token, N) ->
197200
Padding = << <<48:8>> || _ <- lists:seq(1, N) >>,
198201
<<Padding/binary, Token/binary>>.
202+
203+
-spec valid_hotp_return(interval(), boolean()) -> valid_hotp_return().
204+
valid_hotp_return(LastInterval, true = _ReturnInterval) ->
205+
{true, LastInterval};
206+
valid_hotp_return(_LastInterval, _ReturnInterval) ->
207+
true.

test/hotp_validity_tests.erl

+27-9
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ checking_hotp_validity_without_range_test_() ->
99
fun start/0,
1010
fun stop/1,
1111
[fun checking_hotp_validity_without_range/1,
12+
fun checking_hotp_validity_without_range_return_interval/1,
1213
fun checking_hotp_validity_max_default_range/1,
1314
fun checking_hotp_validity_past_max_default_range/1,
1415
fun validating_invalid_token_hotp/1,
@@ -31,6 +32,11 @@ checking_hotp_validity_without_range(Secret) ->
3132
[?_assert(pot:valid_hotp(pot:hotp(Secret, 123), Secret))].
3233

3334

35+
checking_hotp_validity_without_range_return_interval(Secret) ->
36+
[?_assertEqual({true, 123},
37+
pot:valid_hotp(pot:hotp(Secret, 123), Secret, [return_interval]))].
38+
39+
3440
checking_hotp_validity_max_default_range(Secret) ->
3541
[{"hotp at the max # of trials (1000) past the start (1) is valid",
3642
?_assert(pot:valid_hotp(pot:hotp(Secret, 1001), Secret))}].
@@ -54,20 +60,32 @@ validating_correct_totp_as_hotp(Secret) ->
5460

5561

5662
retrieving_proper_interval_from_validator(Secret) ->
57-
Totp = <<"713385">>,
58-
Result = pot:valid_hotp(Totp, Secret, [{last, 1}, {trials, 5}]),
59-
[?_assert(Result),
60-
?_assertEqual(pot:hotp(Secret, 4), Totp)].
63+
Hotp = <<"713385">>,
64+
[?_assert(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 5}])),
65+
?_assert(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 5},
66+
{return_interval, false}])),
67+
?_assertEqual({true, 4},
68+
pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 5},
69+
{return_interval, true}])),
70+
?_assertEqual(pot:hotp(Secret, 4), Hotp)].
6171

6272

6373
hotp_for_range_exact_match(_) ->
6474
Secret = <<"MFRGGZDFMZTWQ2LK">>,
65-
Totp = <<"816065">>,
66-
Result = pot:valid_hotp(Totp, Secret, [{last, 1}, {trials, 1}]),
67-
[?_assert(Result),
68-
?_assertEqual(pot:hotp(Secret, 2), Totp)].
75+
Hotp = <<"816065">>,
76+
[?_assert(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 1}])),
77+
?_assert(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 1},
78+
{return_interval, false}])),
79+
?_assertEqual({true, 2},
80+
pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 1},
81+
{return_interval, true}])),
82+
?_assertEqual(pot:hotp(Secret, 2), Hotp)].
6983

7084

7185
hotp_for_range_preceding_match(_) ->
7286
Secret = <<"MFRGGZDFMZTWQ2LK">>,
73-
[?_assertNot(pot:valid_hotp(<<"713385">>, Secret, [{last, 1}, {trials, 2}]))].
87+
Hotp = <<"713385">>,
88+
[?_assertNot(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 2}])),
89+
?_assertNot(pot:valid_hotp(Hotp, Secret, [{last, 1}, {trials, 2},
90+
{return_interval, true}])),
91+
?_assertEqual(pot:hotp(Secret, 4), Hotp)].

0 commit comments

Comments
 (0)