diff --git a/README.md b/README.md index 4cba061..f727099 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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` @@ -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. @@ -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. @@ -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. diff --git a/src/pot.erl b/src/pot.erl index 754bf15..d256d67 100644 --- a/src/pot.erl +++ b/src/pot.erl @@ -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(). @@ -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 = <>, Digest = hmac(DigestMethod, Key, Msg), <> = binary:part(Digest, {byte_size(Digest), -1}), @@ -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), diff --git a/test/hotp_generation_tests.erl b/test/hotp_generation_tests.erl index 4c659ea..c10befa 100644 --- a/test/hotp_generation_tests.erl +++ b/test/hotp_generation_tests.erl @@ -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]}. @@ -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), diff --git a/test/hotp_validity_tests.erl b/test/hotp_validity_tests.erl index 51c670c..9f9641e 100644 --- a/test/hotp_validity_tests.erl +++ b/test/hotp_validity_tests.erl @@ -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() -> @@ -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}]))]. diff --git a/test/totp_generation_tests.erl b/test/totp_generation_tests.erl index c1d27eb..95ae2bc 100644 --- a/test/totp_generation_tests.erl +++ b/test/totp_generation_tests.erl @@ -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. @@ -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}]))]. diff --git a/test/totp_validity_tests.erl b/test/totp_validity_tests.erl index 9c50ff2..a60bbea 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_with_raw_secret/1]}. start() -> @@ -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}]))].