%% @author Asier Azkuenaga Batiz %% @copyright 2013 Mochi Media, Inc. %% %% 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. %% @doc HTTP Cookie session. Note that the expiration time travels unencrypted %% as far as this module is concerned. In order to achieve more security, %% it is advised to use https. %% Based on the paper %% %% "A Secure Cookie Protocol". %% This module is only supported on R15B02 and later, the AES CFB mode is not %% available in earlier releases of crypto. -module(mochiweb_session). -export([generate_session_data/4, generate_session_cookie/4, check_session_cookie/4]). -export_types([expiration_time/0]). -type expiration_time() :: integer(). -type key_fun() :: fun((string()) -> iolist()). %% TODO: Import this from elsewhere after attribute types refactor. -type header() :: {string(), string()}. %% @doc Generates a secure encrypted binary combining all the parameters. The %% expiration time must be a 32-bit integer. -spec generate_session_data( ExpirationTime :: expiration_time(), Data :: iolist(), FSessionKey :: key_fun(), ServerKey :: iolist()) -> binary(). generate_session_data(ExpirationTime, Data, FSessionKey, ServerKey) when is_integer(ExpirationTime), is_function(FSessionKey)-> BData = ensure_binary(Data), ExpTime = integer_to_list(ExpirationTime), Key = gen_key(ExpTime, ServerKey), Hmac = gen_hmac(ExpTime, BData, FSessionKey(ExpTime), Key), EData = encrypt_data(BData, Key), mochiweb_base64url:encode( <>). %% @doc Convenience wrapper for generate_session_data that returns a %% mochiweb cookie with "id" as the key, a max_age of 20000 seconds, %% and the current local time as local time. -spec generate_session_cookie( ExpirationTime :: expiration_time(), Data :: iolist(), FSessionKey :: key_fun(), ServerKey :: iolist()) -> header(). generate_session_cookie(ExpirationTime, Data, FSessionKey, ServerKey) when is_integer(ExpirationTime), is_function(FSessionKey)-> CookieData = generate_session_data(ExpirationTime, Data, FSessionKey, ServerKey), mochiweb_cookies:cookie("id", CookieData, [{max_age, 20000}, {local_time, calendar:universal_time_to_local_time( calendar:universal_time())}]). %% TODO: This return type is messy to express in the type system. -spec check_session_cookie( ECookie :: binary(), ExpirationTime :: string(), FSessionKey :: key_fun(), ServerKey :: iolist()) -> {Success :: boolean(), ExpTimeAndData :: [integer() | binary()]}. check_session_cookie(ECookie, ExpirationTime, FSessionKey, ServerKey) when is_binary(ECookie), is_integer(ExpirationTime), is_function(FSessionKey) -> case mochiweb_base64url:decode(ECookie) of <> -> ETString = integer_to_list(ExpirationTime1), Key = gen_key(ETString, ServerKey), Data = decrypt_data(EData, Key), Hmac2 = gen_hmac(ETString, Data, FSessionKey(ETString), Key), {ExpirationTime1 >= ExpirationTime andalso eq(Hmac2, BHmac), [ExpirationTime1, binary_to_list(Data)]}; _ -> {false, []} end; check_session_cookie(_ECookie, _ExpirationTime, _FSessionKey, _ServerKey) -> {false, []}. %% 'Constant' time =:= operator for binary, to mitigate timing attacks. -spec eq(binary(), binary()) -> boolean(). eq(A, B) when is_binary(A) andalso is_binary(B) -> eq(A, B, 0). eq(<>, <>, Acc) -> eq(As, Bs, Acc bor (A bxor B)); eq(<<>>, <<>>, 0) -> true; eq(_As, _Bs, _Acc) -> false. -spec ensure_binary(iolist()) -> binary(). ensure_binary(B) when is_binary(B) -> B; ensure_binary(L) when is_list(L) -> iolist_to_binary(L). -spec encrypt_data(binary(), binary()) -> binary(). encrypt_data(Data, Key) -> IV = crypto:strong_rand_bytes(16), Crypt = crypto:crypto_one_time(aes_128_cfb128, Key, IV, Data, true), <>. -spec decrypt_data(binary(), binary()) -> binary(). decrypt_data(<>, Key) -> crypto:crypto_one_time(aes_128_cfb128, Key, IV, Crypt, false). -spec gen_key(iolist(), iolist()) -> binary(). gen_key(ExpirationTime, ServerKey) -> crypto:mac(hmac, md5, ServerKey, [ExpirationTime]). -spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary(). gen_hmac(ExpirationTime, Data, SessionKey, Key) -> crypto:mac(hmac, sha, Key, [ExpirationTime, Data, SessionKey]). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). generate_check_session_cookie_test_() -> {setup, fun setup_server_key/0, fun generate_check_session_cookie/1}. setup_server_key() -> application:start(crypto), ["adfasdfasfs",30000]. generate_check_session_cookie([ServerKey, TS]) -> Id = fun (A) -> A end, TSFuture = TS + 1000, TSPast = TS - 1, [?_assertEqual( {true, [TSFuture, "alice"]}, check_session_cookie( generate_session_data(TSFuture, "alice", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice and"]}, check_session_cookie( generate_session_data(TSFuture, "alice and", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice and"]}, check_session_cookie( generate_session_data(TSFuture, "alice and", Id, ServerKey), TS, Id,ServerKey)), ?_assertEqual( {true, [TSFuture, "alice and bob"]}, check_session_cookie( generate_session_data(TSFuture, "alice and bob", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl"]}, check_session_cookie( generate_session_data(TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl", Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true, [TSFuture, "alice .'¡'ç+-$%/(&\""]}, check_session_cookie( generate_session_data(TSFuture, "alice .'¡'ç+-$%/(&\"" ,Id, ServerKey), TS, Id, ServerKey)), ?_assertEqual( {true,[TSFuture,"alice456689875"]}, check_session_cookie( generate_session_data(TSFuture, ["alice","456689875"], Id, ServerKey), TS, Id, ServerKey)), ?_assertError( function_clause, check_session_cookie( generate_session_data(TSFuture, {tuple,one}, Id, ServerKey), TS, Id,ServerKey)), ?_assertEqual( {false, [TSPast, "bob"]}, check_session_cookie( generate_session_data(TSPast, "bob", Id,ServerKey), TS, Id, ServerKey)) ]. -endif.