12 |
7u83 |
1 |
%% @author Asier Azkuenaga Batiz <asier@zebixe.com>
|
|
|
2 |
%% @copyright 2013 Mochi Media, Inc.
|
|
|
3 |
%%
|
|
|
4 |
%% Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
5 |
%% copy of this software and associated documentation files (the "Software"),
|
|
|
6 |
%% to deal in the Software without restriction, including without limitation
|
|
|
7 |
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
8 |
%% and/or sell copies of the Software, and to permit persons to whom the
|
|
|
9 |
%% Software is furnished to do so, subject to the following conditions:
|
|
|
10 |
%%
|
|
|
11 |
%% The above copyright notice and this permission notice shall be included in
|
|
|
12 |
%% all copies or substantial portions of the Software.
|
|
|
13 |
%%
|
|
|
14 |
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
15 |
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
16 |
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
|
17 |
%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
18 |
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
19 |
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
20 |
%% DEALINGS IN THE SOFTWARE.
|
|
|
21 |
|
|
|
22 |
%% @doc HTTP Cookie session. Note that the expiration time travels unencrypted
|
|
|
23 |
%% as far as this module is concerned. In order to achieve more security,
|
|
|
24 |
%% it is advised to use https.
|
|
|
25 |
%% Based on the paper
|
|
|
26 |
%% <a href="http://www.cse.msu.edu/~alexliu/publications/Cookie/cookie.pdf">
|
|
|
27 |
%% "A Secure Cookie Protocol"</a>.
|
|
|
28 |
%% This module is only supported on R15B02 and later, the AES CFB mode is not
|
|
|
29 |
%% available in earlier releases of crypto.
|
|
|
30 |
-module(mochiweb_session).
|
|
|
31 |
-export([generate_session_data/4, generate_session_cookie/4,
|
|
|
32 |
check_session_cookie/4]).
|
|
|
33 |
|
|
|
34 |
-export_types([expiration_time/0]).
|
|
|
35 |
-type expiration_time() :: integer().
|
|
|
36 |
-type key_fun() :: fun((string()) -> iolist()).
|
|
|
37 |
|
|
|
38 |
%% TODO: Import this from elsewhere after attribute types refactor.
|
|
|
39 |
-type header() :: {string(), string()}.
|
|
|
40 |
|
|
|
41 |
%% @doc Generates a secure encrypted binary convining all the parameters. The
|
|
|
42 |
%% expiration time must be a 32-bit integer.
|
|
|
43 |
-spec generate_session_data(
|
|
|
44 |
ExpirationTime :: expiration_time(),
|
|
|
45 |
Data :: iolist(),
|
|
|
46 |
FSessionKey :: key_fun(),
|
|
|
47 |
ServerKey :: iolist()) -> binary().
|
|
|
48 |
generate_session_data(ExpirationTime, Data, FSessionKey, ServerKey)
|
|
|
49 |
when is_integer(ExpirationTime), is_function(FSessionKey)->
|
|
|
50 |
BData = ensure_binary(Data),
|
|
|
51 |
ExpTime = integer_to_list(ExpirationTime),
|
|
|
52 |
Key = gen_key(ExpTime, ServerKey),
|
|
|
53 |
Hmac = gen_hmac(ExpTime, BData, FSessionKey(ExpTime), Key),
|
|
|
54 |
EData = encrypt_data(BData, Key),
|
|
|
55 |
mochiweb_base64url:encode(
|
|
|
56 |
<<ExpirationTime:32/integer, Hmac/binary, EData/binary>>).
|
|
|
57 |
|
|
|
58 |
%% @doc Convenience wrapper for generate_session_data that returns a
|
|
|
59 |
%% mochiweb cookie with "id" as the key, a max_age of 20000 seconds,
|
|
|
60 |
%% and the current local time as local time.
|
|
|
61 |
-spec generate_session_cookie(
|
|
|
62 |
ExpirationTime :: expiration_time(),
|
|
|
63 |
Data :: iolist(),
|
|
|
64 |
FSessionKey :: key_fun(),
|
|
|
65 |
ServerKey :: iolist()) -> header().
|
|
|
66 |
generate_session_cookie(ExpirationTime, Data, FSessionKey, ServerKey)
|
|
|
67 |
when is_integer(ExpirationTime), is_function(FSessionKey)->
|
|
|
68 |
CookieData = generate_session_data(ExpirationTime, Data,
|
|
|
69 |
FSessionKey, ServerKey),
|
|
|
70 |
mochiweb_cookies:cookie("id", CookieData,
|
|
|
71 |
[{max_age, 20000},
|
|
|
72 |
{local_time,
|
|
|
73 |
calendar:universal_time_to_local_time(
|
|
|
74 |
calendar:universal_time())}]).
|
|
|
75 |
|
|
|
76 |
%% TODO: This return type is messy to express in the type system.
|
|
|
77 |
-spec check_session_cookie(
|
|
|
78 |
ECookie :: binary(),
|
|
|
79 |
ExpirationTime :: string(),
|
|
|
80 |
FSessionKey :: key_fun(),
|
|
|
81 |
ServerKey :: iolist()) ->
|
|
|
82 |
{Success :: boolean(),
|
|
|
83 |
ExpTimeAndData :: [integer() | binary()]}.
|
|
|
84 |
check_session_cookie(ECookie, ExpirationTime, FSessionKey, ServerKey)
|
|
|
85 |
when is_binary(ECookie), is_integer(ExpirationTime),
|
|
|
86 |
is_function(FSessionKey) ->
|
|
|
87 |
case mochiweb_base64url:decode(ECookie) of
|
|
|
88 |
<<ExpirationTime1:32/integer, BHmac:20/binary, EData/binary>> ->
|
|
|
89 |
ETString = integer_to_list(ExpirationTime1),
|
|
|
90 |
Key = gen_key(ETString, ServerKey),
|
|
|
91 |
Data = decrypt_data(EData, Key),
|
|
|
92 |
Hmac2 = gen_hmac(ETString,
|
|
|
93 |
Data,
|
|
|
94 |
FSessionKey(ETString),
|
|
|
95 |
Key),
|
|
|
96 |
{ExpirationTime1 >= ExpirationTime andalso eq(Hmac2, BHmac),
|
|
|
97 |
[ExpirationTime1, binary_to_list(Data)]};
|
|
|
98 |
_ ->
|
|
|
99 |
{false, []}
|
|
|
100 |
end;
|
|
|
101 |
check_session_cookie(_ECookie, _ExpirationTime, _FSessionKey, _ServerKey) ->
|
|
|
102 |
{false, []}.
|
|
|
103 |
|
|
|
104 |
%% 'Constant' time =:= operator for binary, to mitigate timing attacks.
|
|
|
105 |
-spec eq(binary(), binary()) -> boolean().
|
|
|
106 |
eq(A, B) when is_binary(A) andalso is_binary(B) ->
|
|
|
107 |
eq(A, B, 0).
|
|
|
108 |
|
|
|
109 |
eq(<<A, As/binary>>, <<B, Bs/binary>>, Acc) ->
|
|
|
110 |
eq(As, Bs, Acc bor (A bxor B));
|
|
|
111 |
eq(<<>>, <<>>, 0) ->
|
|
|
112 |
true;
|
|
|
113 |
eq(_As, _Bs, _Acc) ->
|
|
|
114 |
false.
|
|
|
115 |
|
|
|
116 |
-spec ensure_binary(iolist()) -> binary().
|
|
|
117 |
ensure_binary(B) when is_binary(B) ->
|
|
|
118 |
B;
|
|
|
119 |
ensure_binary(L) when is_list(L) ->
|
|
|
120 |
iolist_to_binary(L).
|
|
|
121 |
|
|
|
122 |
-ifdef(crypto_compatibility).
|
|
|
123 |
-spec encrypt_data(binary(), binary()) -> binary().
|
|
|
124 |
encrypt_data(Data, Key) ->
|
|
|
125 |
IV = crypto:strong_rand_bytes(16),
|
|
|
126 |
Crypt = crypto:aes_cfb_128_encrypt(Key, IV, Data),
|
|
|
127 |
<<IV/binary, Crypt/binary>>.
|
|
|
128 |
|
|
|
129 |
-spec decrypt_data(binary(), binary()) -> binary().
|
|
|
130 |
decrypt_data(<<IV:16/binary, Crypt/binary>>, Key) ->
|
|
|
131 |
crypto:aes_cfb_128_decrypt(Key, IV, Crypt).
|
|
|
132 |
|
|
|
133 |
-spec gen_key(iolist(), iolist()) -> binary().
|
|
|
134 |
gen_key(ExpirationTime, ServerKey)->
|
|
|
135 |
crypto:md5_mac(ServerKey, [ExpirationTime]).
|
|
|
136 |
|
|
|
137 |
-spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary().
|
|
|
138 |
gen_hmac(ExpirationTime, Data, SessionKey, Key) ->
|
|
|
139 |
crypto:sha_mac(Key, [ExpirationTime, Data, SessionKey]).
|
|
|
140 |
|
|
|
141 |
-else.
|
|
|
142 |
-spec encrypt_data(binary(), binary()) -> binary().
|
|
|
143 |
encrypt_data(Data, Key) ->
|
|
|
144 |
IV = crypto:strong_rand_bytes(16),
|
|
|
145 |
Crypt = crypto:block_encrypt(aes_cfb128, Key, IV, Data),
|
|
|
146 |
<<IV/binary, Crypt/binary>>.
|
|
|
147 |
|
|
|
148 |
-spec decrypt_data(binary(), binary()) -> binary().
|
|
|
149 |
decrypt_data(<<IV:16/binary, Crypt/binary>>, Key) ->
|
|
|
150 |
crypto:block_decrypt(aes_cfb128, Key, IV, Crypt).
|
|
|
151 |
|
|
|
152 |
-spec gen_key(iolist(), iolist()) -> binary().
|
|
|
153 |
gen_key(ExpirationTime, ServerKey)->
|
|
|
154 |
crypto:hmac(md5, ServerKey, [ExpirationTime]).
|
|
|
155 |
|
|
|
156 |
-spec gen_hmac(iolist(), binary(), iolist(), binary()) -> binary().
|
|
|
157 |
gen_hmac(ExpirationTime, Data, SessionKey, Key) ->
|
|
|
158 |
crypto:hmac(sha, Key, [ExpirationTime, Data, SessionKey]).
|
|
|
159 |
|
|
|
160 |
-endif.
|
|
|
161 |
|
|
|
162 |
-ifdef(TEST).
|
|
|
163 |
-include_lib("eunit/include/eunit.hrl").
|
|
|
164 |
|
|
|
165 |
generate_check_session_cookie_test_() ->
|
|
|
166 |
{setup,
|
|
|
167 |
fun setup_server_key/0,
|
|
|
168 |
fun generate_check_session_cookie/1}.
|
|
|
169 |
|
|
|
170 |
setup_server_key() ->
|
|
|
171 |
crypto:start(),
|
|
|
172 |
["adfasdfasfs",30000].
|
|
|
173 |
|
|
|
174 |
generate_check_session_cookie([ServerKey, TS]) ->
|
|
|
175 |
Id = fun (A) -> A end,
|
|
|
176 |
TSFuture = TS + 1000,
|
|
|
177 |
TSPast = TS - 1,
|
|
|
178 |
[?_assertEqual(
|
|
|
179 |
{true, [TSFuture, "alice"]},
|
|
|
180 |
check_session_cookie(
|
|
|
181 |
generate_session_data(TSFuture, "alice", Id, ServerKey),
|
|
|
182 |
TS, Id, ServerKey)),
|
|
|
183 |
?_assertEqual(
|
|
|
184 |
{true, [TSFuture, "alice and"]},
|
|
|
185 |
check_session_cookie(
|
|
|
186 |
generate_session_data(TSFuture, "alice and", Id, ServerKey),
|
|
|
187 |
TS, Id, ServerKey)),
|
|
|
188 |
?_assertEqual(
|
|
|
189 |
{true, [TSFuture, "alice and"]},
|
|
|
190 |
check_session_cookie(
|
|
|
191 |
generate_session_data(TSFuture, "alice and", Id, ServerKey),
|
|
|
192 |
TS, Id,ServerKey)),
|
|
|
193 |
?_assertEqual(
|
|
|
194 |
{true, [TSFuture, "alice and bob"]},
|
|
|
195 |
check_session_cookie(
|
|
|
196 |
generate_session_data(TSFuture, "alice and bob",
|
|
|
197 |
Id, ServerKey),
|
|
|
198 |
TS, Id, ServerKey)),
|
|
|
199 |
?_assertEqual(
|
|
|
200 |
{true, [TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl"]},
|
|
|
201 |
check_session_cookie(
|
|
|
202 |
generate_session_data(TSFuture, "alice jlkjfkjsdfg sdkfjgldsjgl",
|
|
|
203 |
Id, ServerKey),
|
|
|
204 |
TS, Id, ServerKey)),
|
|
|
205 |
?_assertEqual(
|
|
|
206 |
{true, [TSFuture, "alice .'¡'ç+-$%/(&\""]},
|
|
|
207 |
check_session_cookie(
|
|
|
208 |
generate_session_data(TSFuture, "alice .'¡'ç+-$%/(&\""
|
|
|
209 |
,Id, ServerKey),
|
|
|
210 |
TS, Id, ServerKey)),
|
|
|
211 |
?_assertEqual(
|
|
|
212 |
{true,[TSFuture,"alice456689875"]},
|
|
|
213 |
check_session_cookie(
|
|
|
214 |
generate_session_data(TSFuture, ["alice","456689875"],
|
|
|
215 |
Id, ServerKey),
|
|
|
216 |
TS, Id, ServerKey)),
|
|
|
217 |
?_assertError(
|
|
|
218 |
function_clause,
|
|
|
219 |
check_session_cookie(
|
|
|
220 |
generate_session_data(TSFuture, {tuple,one},
|
|
|
221 |
Id, ServerKey),
|
|
|
222 |
TS, Id,ServerKey)),
|
|
|
223 |
?_assertEqual(
|
|
|
224 |
{false, [TSPast, "bob"]},
|
|
|
225 |
check_session_cookie(
|
|
|
226 |
generate_session_data(TSPast, "bob", Id,ServerKey),
|
|
|
227 |
TS, Id, ServerKey))
|
|
|
228 |
].
|
|
|
229 |
-endif.
|