blob: f89f19b7ce573a8121cd266150c315ecf910e104 [file] [log] [blame]
%% @author Asier Azkuenaga Batiz <asier@zebixe.com>
%% @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 href="http://www.cse.msu.edu/~alexliu/publications/Cookie/cookie.pdf">
%% "A Secure Cookie Protocol"</a>.
%% 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 convining 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(
<<ExpirationTime:32/integer, Hmac/binary, EData/binary>>).
%% @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
<<ExpirationTime1:32/integer, BHmac:20/binary, EData/binary>> ->
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(<<A, As/binary>>, <<B, Bs/binary>>, 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).
-ifdef(crypto_compatibility).
-spec encrypt_data(binary(), binary()) -> binary().
encrypt_data(Data, Key) ->
IV = crypto:strong_rand_bytes(16),
Crypt = crypto:aes_cfb_128_encrypt(Key, IV, Data),
<<IV/binary, Crypt/binary>>.
-spec decrypt_data(binary(), binary()) -> binary().
decrypt_data(<<IV:16/binary, Crypt/binary>>, Key) ->
crypto:aes_cfb_128_decrypt(Key, IV, Crypt).
-spec gen_key(iolist(), iolist()) -> binary().
gen_key(ExpirationTime, ServerKey)->
crypto:md5_mac(ServerKey, [ExpirationTime]).
-spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary().
gen_hmac(ExpirationTime, Data, SessionKey, Key) ->
crypto:sha_mac(Key, [ExpirationTime, Data, SessionKey]).
-else.
-spec encrypt_data(binary(), binary()) -> binary().
encrypt_data(Data, Key) ->
IV = crypto:strong_rand_bytes(16),
Crypt = crypto:block_encrypt(aes_cfb128, Key, IV, Data),
<<IV/binary, Crypt/binary>>.
-spec decrypt_data(binary(), binary()) -> binary().
decrypt_data(<<IV:16/binary, Crypt/binary>>, Key) ->
crypto:block_decrypt(aes_cfb128, Key, IV, Crypt).
-spec gen_key(iolist(), iolist()) -> binary().
gen_key(ExpirationTime, ServerKey)->
crypto:hmac(md5, ServerKey, [ExpirationTime]).
-spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary().
gen_hmac(ExpirationTime, Data, SessionKey, Key) ->
crypto:hmac(sha, Key, [ExpirationTime, Data, SessionKey]).
-endif.
-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() ->
crypto:start(),
["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.