1 /**
2 * SQLite3 TLS Session Manager
3 * 
4 * Copyright:
5 * (C) 2012 Jack Lloyd
6 * (C) 2014-2015 Etienne Cimon
7 *
8 * License:
9 * Botan is released under the Simplified BSD License (see LICENSE.md)
10 */
11 module botan.tls.session_manager_sqlite;
12 
13 import botan.constants;
14 static if (BOTAN_HAS_TLS && BOTAN_HAS_SQLITE):
15 
16 import botan.tls.session_manager;
17 import botan.utils.sqlite3.sqlite3;
18 import botan.libstate.lookup;
19 import botan.codec.hex;
20 import botan.utils.loadstor;
21 import botan.utils.mem_ops;
22 import std.datetime;
23 
24 /// The higher the safer, but will takes longer to load
25 size_t g_passphraseIterations = 128*1024;
26 
27 /**
28 * An implementation of TLSSessionManager that saves values in a SQLite3
29 * database file, with the session data encrypted using a passphrase.
30 *
31 * Notes:
32 * For clients, the hostnames associated with the saved
33 * sessions are stored in the database in plaintext. This may be a
34 * serious privacy risk in some situations.
35 */
36 final class TLSSessionManagerSQLite : TLSSessionManager
37 {
38 public:
39     /**
40     * Params:
41     *  passphrase = used to encrypt the session data
42     *  rng = a random number generator
43     *  db_filename = filename of the SQLite database file.
44                 The table names tls_sessions and tls_sessions_metadata
45                 will be used
46     *  max_sessions = a hint on the maximum number of sessions
47     *          to keep in memory at any one time. (If zero, don't cap)
48     *  session_lifetime = sessions are expired after this duration has elapsed from initial handshake.
49     */
50     this(in string passphrase,
51            RandomNumberGenerator rng,
52            in string db_filename,
53            size_t max_sessions = 1000,
54            Duration session_lifetime = 7200.seconds) 
55     {
56         m_rng = rng;
57         m_max_sessions = max_sessions;
58         m_session_lifetime = session_lifetime;
59         m_db = new sqlite3_database(db_filename);
60 
61 		if (m_db.rowCount("sqlite_master WHERE type='table' AND (name='tls_sessions' OR name='tls_sessions_metadata')") == 0) {
62 
63 	        m_db.createTable(
64 	            "create table if not exists tls_sessions "
65 	            ~ "("
66 	            ~ "session_id TEXT PRIMARY KEY, "
67 	            ~ "session_start INTEGER, "
68 	            ~ "hostname TEXT, "
69 	            ~ "hostport INTEGER, "
70 	            ~ "session BLOB"
71 	            ~ ")");
72 	        
73 	        m_db.createTable(
74 	            "create table if not exists tls_sessions_metadata "
75 	            ~ "("
76 	            ~ "passphrase_salt BLOB, "
77 	            ~ "passphrase_iterations INTEGER, "
78 	            ~ "passphrase_check INTEGER "
79 	            ~ ")");
80 		}
81         const size_t salts = m_db.rowCount("tls_sessions_metadata");
82         
83         if (salts == 1)
84         {
85             // existing db
86             sqlite3_statement stmt = sqlite3_statement(m_db, "select * from tls_sessions_metadata");
87             
88             if (stmt.step())
89             {
90                 Pair!(const(ubyte)*, size_t) salt = stmt.getBlob(0);
91                 const size_t iterations = stmt.getSizeT(1);
92                 const size_t check_val_db = stmt.getSizeT(2);
93                 
94                 size_t check_val_created;
95                 m_session_key = deriveKey(passphrase,
96                                            salt.first,
97                                            salt.second,
98                                            iterations,
99                                            check_val_created);
100                 
101                 if (check_val_created != check_val_db)
102                     throw new Exception("TLSSession database password not valid");
103             }
104         }
105         else
106         {
107             // maybe just zap the salts + sessions tables in this case?
108             if (salts != 0)
109                 throw new Exception("Seemingly corrupted database, multiple salts found");
110             
111             // new database case
112             
113             Vector!ubyte salt = unlock(rng.randomVec(16));
114 			const size_t iterations = g_passphraseIterations;
115             size_t check_val = 0;
116             
117             m_session_key = deriveKey(passphrase, salt.ptr, salt.length, iterations, check_val);
118             
119             sqlite3_statement stmt = sqlite3_statement(m_db, "insert into tls_sessions_metadata"
120                                                                 ~ " values(?1, ?2, ?3)");
121             
122             stmt.bind(1, salt);
123             stmt.bind(2, cast(int) iterations);
124             stmt.bind(3, cast(int) check_val);
125             
126             stmt.spin();
127         }
128     }
129 
130     ~this()
131     {
132         destroy(m_db);
133     }
134 
135     override bool loadFromSessionId(const ref Vector!ubyte session_id, ref TLSSession session)
136     {
137         sqlite3_statement stmt = sqlite3_statement(m_db, "select session from tls_sessions where session_id = ?1");
138         
139         stmt.bind(1, hexEncode(session_id));
140         
141         while (stmt.step())
142         {
143             Pair!(const(ubyte)*, size_t) blob = stmt.getBlob(0);
144             
145             try
146             {
147                 session = TLSSession.decrypt(blob.first, blob.second, m_session_key);
148                 return true;
149             }
150             catch (Exception)
151             {
152             }
153         }
154         
155         return false;
156     }
157 
158     override bool loadFromServerInfo(in TLSServerInformation server, ref TLSSession session)
159     {
160         sqlite3_statement stmt = sqlite3_statement(m_db, "select session from tls_sessions"
161                                                        ~ " where hostname = ?1 and hostport = ?2"
162                                                        ~ " order by session_start desc");
163         
164         stmt.bind(1, server.hostname());
165         stmt.bind(2, server.port());
166         
167         while (stmt.step())
168         {
169             Pair!(const(ubyte)*, size_t) blob = stmt.getBlob(0);
170             
171             try
172             {
173                 session = TLSSession.decrypt(blob.first, blob.second, m_session_key);
174                 return true;
175             }
176             catch (Exception)
177             {
178             }
179         }
180         
181         return false;
182     }
183 
184     override void removeEntry(const ref Vector!ubyte session_id)
185     {
186         sqlite3_statement stmt = sqlite3_statement(m_db, "delete from tls_sessions where session_id = ?1");
187         
188         stmt.bind(1, hexEncode(session_id));
189         
190         stmt.spin();
191     }
192 
193     override void save(in TLSSession session)
194     {
195 
196 		sqlite3_statement stmt = sqlite3_statement(m_db, "insert or replace into tls_sessions"
197 			~ " values(?1, ?2, ?3, ?4, ?5)");
198 		
199 		stmt.bind(1, hexEncode(session.sessionId()));
200 		stmt.bind(2, session.startTime());
201 		stmt.bind(3, session.serverInfo().hostname());
202 		stmt.bind(4, session.serverInfo().port());
203 		stmt.bind(5, session.encrypt(m_session_key, m_rng));
204 		
205 		stmt.spin();
206 		try pruneSessionCache();
207 		catch (Exception e) {
208 		}
209     }
210 
211     override Duration sessionLifetime() const
212     { return m_session_lifetime; }
213 
214 private:
215     @disable this(const ref TLSSessionManagerSQLite);
216     @disable TLSSessionManagerSQLite opAssign(const ref TLSSessionManagerSQLite);
217 
218     void pruneSessionCache()
219     {
220         sqlite3_statement remove_expired = sqlite3_statement(m_db, "delete from tls_sessions where session_start <= ?1");
221         
222         remove_expired.bind(1, Clock.currTime(UTC()) - m_session_lifetime);
223         
224         remove_expired.spin();
225         
226         const size_t sessions = m_db.rowCount("tls_sessions");
227         
228         if (sessions > m_max_sessions)
229         {
230             sqlite3_statement remove_some = sqlite3_statement(m_db, "delete from tls_sessions where session_id in "
231                                           ~ "(select session_id from tls_sessions limit ?1)");
232             
233             remove_some.bind(1, cast(int)(sessions - m_max_sessions));
234             remove_some.spin();
235         }
236     }
237 
238     SymmetricKey m_session_key;
239     RandomNumberGenerator m_rng;
240     size_t m_max_sessions;
241     Duration m_session_lifetime;
242     sqlite3_database m_db;
243 }
244 
245 SymmetricKey deriveKey(in string passphrase,
246                         const(ubyte)* salt,
247                         size_t salt_len,
248                         size_t iterations,
249                         ref size_t check_val)
250 {
251     Unique!PBKDF pbkdf = getPbkdf("PBKDF2(SHA-512)");
252     
253 	auto key = pbkdf.deriveKey(32 + 2, passphrase, salt, salt_len, iterations);
254     SecureVector!ubyte x = key.bitsOf();
255     
256     check_val = make_ushort(x[0], x[1]);
257     return SymmetricKey(&x[2], x.length - 2);
258 }