1%%
2%% %CopyrightBegin%
3%%
4%% Copyright Ericsson AB 2020-2020. All Rights Reserved.
5%%
6%% Licensed under the Apache License, Version 2.0 (the "License");
7%% you may not use this file except in compliance with the License.
8%% You may obtain a copy of the License at
9%%
10%%     http://www.apache.org/licenses/LICENSE-2.0
11%%
12%% Unless required by applicable law or agreed to in writing, software
13%% distributed under the License is distributed on an "AS IS" BASIS,
14%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15%% See the License for the specific language governing permissions and
16%% limitations under the License.
17%%
18%% %CopyrightEnd%
19%%
20
21%%----------------------------------------------------------------------
22%% Purpose: Handle server side pre TLS-1.3 reuse session storage.
23%% This implements the RFC session reuse and not the session ticket extension
24%% that inspired the RFC session tickets of TLS-1.3.
25%%----------------------------------------------------------------------
26
27-module(ssl_server_session_cache).
28-behaviour(gen_server).
29
30-include_lib("kernel/include/logger.hrl").
31-include("ssl_handshake.hrl").
32-include("ssl_internal.hrl").
33
34%% API
35-export([start_link/2,
36         new_session_id/1,
37         register_session/2,
38         reuse_session/2
39        ]).
40
41%% gen_server callbacks
42-export([init/1,
43         handle_call/3,
44         handle_cast/2,
45         handle_info/2,
46         terminate/2
47         %%code_change/3,
48         %%format_status/2
49        ]).
50
51-record(state, {store_cb,
52                lifetime,
53                db,
54                max,
55                session_order,
56                id_generator,
57                listner
58               }).
59
60%%%===================================================================
61%%% API
62%%%===================================================================
63
64-spec start_link(pid(), map()) -> {ok, Pid :: pid()} |
65                      {error, Error :: {already_started, pid()}} |
66                      {error, Error :: term()} |
67                      ignore.
68start_link(ssl_unknown_listener = Listner, Map) ->
69    gen_server:start_link({local, Listner}, ?MODULE, [Listner, Map], []);
70start_link(Listner, Map) ->
71    gen_server:start_link(?MODULE, [Listner, Map], []).
72
73%%--------------------------------------------------------------------
74-spec new_session_id(Pid::pid()) -> ssl:session_id().
75%%
76%% Description: Creates a session id for the server.
77%%--------------------------------------------------------------------
78new_session_id(Pid) ->
79    case call(Pid, new_session_id) of
80        {no_server, _} ->
81            crypto:strong_rand_bytes(32);
82        Result ->
83            Result
84    end.
85
86%%--------------------------------------------------------------------
87-spec reuse_session(pid(), ssl:session_id()) ->  #session{} | not_reusable.
88%%
89%% Description: Returns session to reuse
90%%--------------------------------------------------------------------
91reuse_session(Pid, SessionId) ->
92    case call(Pid, {reuse_session, SessionId}) of
93        {no_server, _} ->
94            not_reusable;
95        Result ->
96            Result
97    end.
98%%--------------------------------------------------------------------
99-spec register_session(pid(), term()) -> ok.
100%%
101%% Description: Makes a session available for reuse
102%%--------------------------------------------------------------------
103register_session(Pid, Session) ->
104    gen_server:cast(Pid, {register_session, Session}).
105
106
107%%%===================================================================
108%%% gen_server callbacks
109%%%===================================================================
110-spec init(Args :: term()) -> {ok, State :: term()}.
111init([Listner, #{lifetime := Lifetime,
112                 session_cb := Cb,
113                 session_cb_init_args := InitArgs,
114                 max := Max
115                }]) ->
116    process_flag(trap_exit, true),
117    Monitor = monitor_listener(Listner),
118    DbRef = init(Cb, [{role, server} | InitArgs]),
119    State = #state{store_cb = Cb,
120                   lifetime = Lifetime,
121                   db = DbRef,
122                   max = Max,
123                   session_order = gb_trees:empty(),
124                   id_generator = crypto:strong_rand_bytes(16),
125                   listner = Monitor
126                  },
127    {ok, State}.
128
129-spec handle_call(Request :: term(), From :: {pid(), term()}, State :: term()) ->
130                         {reply, Reply :: term(), NewState :: term()} .
131handle_call(new_session_id, _From, #state{id_generator = IdGen} = State) ->
132    SessionId = session_id(IdGen),
133    {reply, SessionId, State};
134handle_call({reuse_session, SessionId}, _From,  #state{store_cb = Cb,
135                                                       db = Store0,
136                                                       lifetime = Lifetime,
137                                                       session_order = Order0} = State0) ->
138    case lookup(Cb, Store0, SessionId) of
139        undefined ->
140            {reply, not_reusable, State0};
141        #session{internal_id = InId} = Session ->
142            case ssl_session:valid_session(Session, Lifetime) of
143                true ->
144                    {reply, Session, State0};
145                false ->
146                    {Store, Order} = invalidate_session(Cb, Store0, Order0, SessionId, InId),
147                    {reply, not_reusable, State0#state{db = Store, session_order = Order}}
148            end
149    end.
150
151-spec handle_cast(Request :: term(), State :: term()) ->
152           {noreply, NewState :: term()}.
153handle_cast({register_session, #session{session_id = SessionId, time_stamp = TimeStamp} = Session0},
154            #state{store_cb = Cb,
155                   db = Store0,
156                   max = Max,
157                   lifetime = Lifetime,
158                   session_order = Order0}
159            = State0) ->
160    InternalId = {TimeStamp, erlang:unique_integer([monotonic])},
161    Session = Session0#session{internal_id = InternalId},
162    State = case size(Cb, Store0) of
163                Max ->
164                    %% Throw away oldest session table may not grow larger than max
165                    {_, OldSessId, Order1} = gb_trees:take_smallest(Order0),
166                    Store1 = delete(Cb, Store0, OldSessId),
167                    %% Insert new session
168                    Order = gb_trees:insert(InternalId, SessionId, Order1),
169                    Store = update(Cb, Store1, SessionId, Session),
170                    State0#state{db = Store, session_order = Order};
171                Size when Size > 0 ->
172                    {_, OldSessId, Order1} = gb_trees:take_smallest(Order0),
173                    OldestSession = lookup(Cb, Store0, OldSessId),
174                    case ssl_session:valid_session(OldestSession, Lifetime) of
175                        true ->
176                            Store = update(Cb, Store0, SessionId, Session#session{time_stamp = TimeStamp}),
177                            State0#state{db = Store,
178                                         session_order = gb_trees:insert(InternalId, SessionId, Order0)};
179                        false ->
180                            %% Throw away oldest session as it is not valid anymore
181                            Store1 = delete(Cb, Store0, OldSessId),
182                            Store = update(Cb, Store1, SessionId, Session#session{time_stamp = TimeStamp}),
183                            State0#state{db = Store,
184                                         session_order =  gb_trees:insert(InternalId, SessionId, Order1)}
185                    end;
186                0 ->
187                    Store = update(Cb, Store0, SessionId, Session#session{time_stamp = TimeStamp}),
188                    State0#state{db = Store,
189                                 session_order = gb_trees:insert(InternalId, SessionId, Order0)}
190            end,
191    {noreply, State}.
192
193-spec handle_info(Info :: timeout() | term(), State :: term()) ->
194          {noreply, NewState :: term()}.
195handle_info({'DOWN', Monitor, _, _, _}, #state{listner = Monitor} = State) ->
196     {stop, normal, State};
197handle_info(_, State) ->
198    {noreply, State}.
199
200terminate(_, _) ->
201    ok.
202
203%%%===================================================================
204%%% Internal functions
205%%%===================================================================
206call(Pid, Msg) ->
207    try gen_server:call(Pid, Msg, infinity)
208    catch
209	exit:Reason ->
210	    {no_server, Reason}
211    end.
212
213session_id(Key) ->
214    Unique1 = erlang:unique_integer(),
215    Unique2 = erlang:unique_integer(),
216    %% Obfuscate to avoid DoS attack possiblities
217    %% This id should be unpredictable an 32 bytes
218    %% and unique but have no other cryptographic requirements.
219    Bin1 = crypto:crypto_one_time(aes_128_ecb, Key, <<Unique1:128>>, true),
220    Bin2 = crypto:crypto_one_time(aes_128_ecb, Key, <<Unique2:128>>, true),
221    <<Bin1/binary, Bin2/binary>>.
222
223invalidate_session(Cb, Store0, Order, SessionId, InternalId) ->
224    Store = delete(Cb, Store0, SessionId),
225    {Store, gb_trees:delete(InternalId, Order)}.
226
227init(Cb, Options) ->
228    Cb:init(Options).
229
230lookup(Cb, Cache, Key) ->
231    Cb:lookup(Cache, Key).
232
233update(ssl_server_session_cache_db = Cb, Cache, Key, Session) ->
234    Cb:update(Cache, Key, Session);
235update(Cb, Cache, Key, Session) ->
236    Cb:update(Cache, Key, Session),
237    Cache.
238
239delete(ssl_server_session_cache_db = Cb, Cache, Key) ->
240    Cb:delete(Cache, Key);
241delete(Cb, Cache, Key) ->
242    Cb:delete(Cache, Key),
243    Cache.
244
245size(Cb,Cache) ->
246    try Cb:size(Cache) of
247        Size ->
248            Size
249    catch
250        error:undef ->
251            Cb:foldl(fun(_, Acc) -> Acc + 1 end, 0, Cache)
252    end.
253
254monitor_listener(ssl_unknown_listener) ->
255    %% Backwards compatible Erlang node
256    %% global process.
257    undefined;
258monitor_listener(Listen) ->
259    inet:monitor(Listen).
260