Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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.