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 SecureVector!ubyte x = pbkdf.deriveKey(32 + 2, passphrase, salt, salt_len, iterations).bitsOf(); 254 255 check_val = make_ushort(x[0], x[1]); 256 return SymmetricKey(&x[2], x.length - 2); 257 }