%% Copyright (c) 2014-2017 Yüce Tekol % %% Permission is hereby granted, free of charge, to any person obtaining a copy of this software %% and associated documentation files (the "Software"), to deal in the Software without %% restriction, including without limitation the rights to use, copy, modify, merge, publish, %% distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the %% Software is furnished to do so, subject to the following conditions: % %% The above copyright notice and this permission notice shall be included in all copies or %% substantial portions of the Software. % %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING %% BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, %% DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -module(pot). -export([valid_token/1, valid_token/2]). -export([hotp/2, hotp/3]). -export([totp/1, totp/2]). -export([valid_hotp/2, valid_hotp/3]). -export([valid_totp/2, valid_totp/3]). -export([time_interval/1]). -type token() :: binary(). -type secret() :: binary(). -type proplistitem() :: {atom(), term()}. -type proplist() :: [proplistitem()] | []. -type time() :: integer(). -spec valid_token(token()) -> boolean(). valid_token(Token) -> valid_token(Token, []). -spec valid_token(token(), proplist()) -> boolean(). valid_token(Token, Opts) when is_binary(Token) -> Length = proplists:get_value(token_length, Opts, 6), case byte_size(Token) == Length of true -> lists:all(fun(X) -> X >= $0 andalso X =< $9 end, binary_to_list(Token)); false -> false end. -spec hotp(secret(), pos_integer()) -> token(). hotp(Secret, IntervalsNo) -> hotp(Secret, IntervalsNo, []). -spec hotp(secret(), pos_integer(), proplist()) -> token(). hotp(Secret, IntervalsNo, Opts) -> DigestMethod = proplists:get_value(digest_method, Opts, sha), TokenLength = proplists:get_value(token_length, Opts, 6), IsLower = {lower, proplists:get_bool(casefold, Opts)}, Key = base32:decode(Secret, [{lower, IsLower}]), Msg = <>, Digest = crypto:hmac(DigestMethod, Key, Msg), <<_:19/binary, Ob:8>> = Digest, O = Ob band 15, <> = binary:part(Digest, O, 4), TokenBase = TokenBase0 band 16#7fffffff, Token0 = TokenBase rem trunc(math:pow(10, TokenLength)), Token1 = integer_to_binary(Token0), prepend_zeros(Token1, TokenLength - byte_size(Token1)). -spec totp(secret()) -> token(). totp(Secret) -> totp(Secret, []). -spec totp(secret(), proplist()) -> token(). totp(Secret, Opts) -> IntervalsNo = time_interval(Opts), hotp(Secret, IntervalsNo, Opts). -spec valid_hotp(token(), secret()) -> boolean(). valid_hotp(Token, Secret) -> valid_hotp(Token, Secret, []). -spec valid_hotp(token(), secret(), proplist()) -> boolean(). valid_hotp(Token, Secret, Opts) -> Last = proplists:get_value(last, Opts, 1), Trials = proplists:get_value(trials, Opts, 1000), TokenLength = proplists:get_value(token_length, Opts, 6), case valid_token(Token, [{token_length, TokenLength}]) of true -> check_candidate(Token, Secret, Last + 1, Last + Trials, Opts); _ -> false end. -spec valid_totp(token(), secret()) -> boolean(). valid_totp(Token, Secret) -> valid_totp(Token, Secret, []). -spec valid_totp(token(), secret(), proplist()) -> boolean(). valid_totp(Token, Secret, Opts) -> case valid_token(Token, Opts) of true -> IntervalsNo = time_interval(Opts), case totp(Secret, Opts) of Token -> true; _ -> Window = proplists:get_value(window, Opts, 0), case check_candidate(Token, Secret, IntervalsNo - Window, IntervalsNo + Window, Opts) of false -> false; _ -> true end end; _ -> false end. -spec time_interval(proplist()) -> time(). 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, _} = os:timestamp(), trunc((MegaSecs * 1000000 + (Secs + AddSeconds)) / IntervalLength). check_candidate(Token, Secret, Current, Last, Opts) when Current =< Last -> case Current of Last -> false; _ -> Candidate = hotp(Secret, Current, Opts), case Candidate of Token -> Current; _ -> check_candidate(Token, Secret, Current + 1, Last, Opts) end end. -spec prepend_zeros(token(), non_neg_integer()) -> token(). prepend_zeros(Token, N) -> Padding = << <<48:8>> || _ <- lists:seq(1, N) >>, <>.