%%%------------------------------------------------------------------- %%% @author Juan Jose Comellas %%% @copyright (C) 2015 Cielo24, Inc. %%% @doc Functions that decode and encode API tokens. %%% @end %%%------------------------------------------------------------------- -module(cet_token). -author('Juan Jose Comellas '). -export([encode/2, encode/3, encode/4]). -export([hash_secret/3]). -export([decode/1]). -export([verify/2, verify/4]). -export([rand_bytes/1]). -define(VERSION, "1"). -define(SALT_LENGTH, 20). %% @doc Generate the API token used between Cielo24 and the Media Data Proxy %% Server. The format of the resulting `Token` is: %% Version:Salt:Expiration:HashedSecret -spec encode(Secret :: binary(), Lifetime :: non_neg_integer()) -> Token :: binary(). encode(Secret, Lifetime) -> encode(Secret, rand_bytes(?SALT_LENGTH), Lifetime). -spec encode(Secret :: binary(), Salt :: binary(), Lifetime :: non_neg_integer()) -> Token :: binary(). encode(Secret, Salt, Lifetime) -> encode(Secret, Salt, Lifetime, os:system_time(seconds)). -spec encode(Secret :: binary(), Salt :: binary(), Lifetime :: non_neg_integer(), Epoch :: non_neg_integer()) -> Token :: binary(). encode(Secret, Salt, Lifetime, Epoch) when is_binary(Secret), is_binary(Salt), is_integer(Lifetime), is_integer(Epoch) -> Expiration = Epoch + Lifetime, Base64Salt = base64:encode(Salt), HexExpiration = bstr:lower(integer_to_binary(Expiration, 16)), Base64HashedSecret = base64:encode(hash_secret(Secret, Salt, Expiration)), <>. -spec hash_secret(Secret :: binary(), Salt :: binary(), Expiration :: non_neg_integer()) -> Token :: binary(). hash_secret(Secret, Salt, Expiration) -> %% Hash the salt together with the secret: SHA1(Salt + Secret) SaltedSecret = crypto:hash(sha, <>), %% Hash the least 32 significant bits (in network byte order) from the timestamp %% with the hashed salt and secret: SHA1(Timestamp + SHA1(Salt + Secret)). crypto:hash(sha, <>). %% @doc Decode a API Token. -spec decode(Token :: binary()) -> {ok, {Salt :: binary(), Epoch :: non_neg_integer(), HashedSecret :: binary()}} | {error, Reason :: term()}. decode(Token) -> case binary:split(Token, <<$:>>, [global]) of [Version, Salt, Expiration, HashedSecret] when Version =:= <> -> {ok, {base64:decode(Salt), binary_to_integer(Expiration, 16), base64:decode(HashedSecret)}}; _ -> {error, invalid_token_version} end. %% @doc Check that an API Token has not expired and was encoded %% properly with the given Secret. -spec verify(Token :: binary(), Secret :: binary()) -> ok | {error, Reason :: term()}. verify(Token, Secret) -> case binary:split(Token, <<$:>>, [global]) of [Version, Salt, Expiration, HashedSecret] when Version =:= <> -> verify(Secret, base64:decode(Salt), binary_to_integer(Expiration, 16), base64:decode(HashedSecret)); _ -> {error, invalid_token_version} end. %% @doc Check that an API Token has not expired and was encoded %% properly with the given Secret, Salt and %% Timestamp. -spec verify(Secret :: binary(), Salt :: binary(), Expiration :: non_neg_integer(), HashedSecret :: binary()) -> ok | {error, Reason :: term()}. verify(Secret, Salt, Expiration, HashedSecret) -> Now = os:system_time(seconds), if %% Make sure that the authentication token is not expired yet. Now =< Expiration -> case hash_secret(Secret, Salt, Expiration) of HashedSecret -> ok; _ -> {error, {invalid_token}} end; true -> {error, expired_token} end. -spec rand_bytes(Length :: non_neg_integer()) -> binary(). rand_bytes(Length) when is_integer(Length), Length > 0 -> try crypto:strong_rand_bytes(Length) of Bytes -> Bytes catch _:low_entropy -> crypto:rand_bytes(Length) end.