Botan 3.13.0
Crypto and TLS for C&
tls_session_manager_sql.cpp
Go to the documentation of this file.
1/*
2* SQL TLS Session Manager
3* (C) 2012,2014 Jack Lloyd
4*
5* Botan is released under the Simplified BSD License (see license.txt)
6*/
7
8#include <botan/tls_session_manager_sql.h>
9
10#include <botan/database.h>
11#include <botan/hex.h>
12#include <botan/pwdhash.h>
13#include <botan/rng.h>
14#include <botan/tls_session.h>
15#include <botan/internal/loadstor.h>
16
17namespace Botan::TLS {
18
19Session_Manager_SQL::Session_Manager_SQL(std::shared_ptr<SQL_Database> db,
20 std::string_view passphrase,
21 const std::shared_ptr<RandomNumberGenerator>& rng,
22 size_t max_sessions) :
23 Session_Manager(rng), m_db(std::move(db)), m_max_sessions(max_sessions) {
24 create_or_migrate_and_open(passphrase);
25}
26
27void Session_Manager_SQL::create_or_migrate_and_open(std::string_view passphrase) {
28 switch(detect_schema_revision()) {
29 case CORRUPTED:
30 case PRE_BOTAN_3_0:
31 case EMPTY:
32 // Legacy sessions before Botan 3.0 are simply dropped, no actual
33 // migration is implemented. Same for apparently corrupt databases.
34 m_db->exec("DROP TABLE IF EXISTS tls_sessions");
35 m_db->exec("DROP TABLE IF EXISTS tls_sessions_metadata");
36 create_with_latest_schema(passphrase, BOTAN_3_0);
37 break;
38 case BOTAN_3_0:
39 initialize_existing_database(passphrase);
40 break;
41 default:
42 throw Internal_Error("TLS session db has unknown database schema");
43 }
44}
45
46Session_Manager_SQL::Schema_Revision Session_Manager_SQL::detect_schema_revision() {
47 try {
48 const auto meta_data_rows = m_db->row_count("tls_sessions_metadata");
49 if(meta_data_rows != 1) {
50 return CORRUPTED;
51 }
52 } catch(const SQL_Database::SQL_DB_Error&) {
53 return EMPTY; // `tls_sessions_metadata` probably didn't exist at all
54 }
55
56 try {
57 auto stmt = m_db->select("database_revision", "tls_sessions_metadata");
58 if(!stmt->step()) {
59 throw Internal_Error("Failed to read revision of TLS session database");
60 }
61 return Schema_Revision(stmt->get_size_t(0));
62 } catch(const SQL_Database::SQL_DB_Error&) {
63 return PRE_BOTAN_3_0; // `database_revision` did not exist yet -> preparing the statement failed
64 }
65}
66
67void Session_Manager_SQL::create_with_latest_schema(std::string_view passphrase, Schema_Revision rev) {
68 using DB = SQL_Database;
69 const auto blob = DB::Column_Type::Blob;
70 const auto str = DB::Column_Type::String;
71 const auto integer = DB::Column_Type::Integer;
72
73 m_db->create_table(DB::Table_Schema("tls_sessions",
74 {
75 DB::Column("session_id", str).primary_key(),
76 DB::Column("session_ticket", blob),
77 DB::Column("session_start", integer),
78 DB::Column("hostname", str),
79 DB::Column("hostport", integer),
80 DB::Column("session", blob).not_null(),
81 }));
82
83 m_db->create_table(DB::Table_Schema("tls_sessions_metadata",
84 {
85 DB::Column("passphrase_salt", blob).not_null(),
86 DB::Column("passphrase_iterations", integer).not_null(),
87 DB::Column("passphrase_check", integer).not_null(),
88 DB::Column("password_hash_family", str).not_null(),
89 DB::Column("database_revision", integer).not_null(),
90 }));
91
92 // speeds up lookups on session_tickets when deleting
93 m_db->exec("CREATE INDEX tls_tickets ON tls_sessions (session_ticket)");
94
95 auto salt = m_rng->random_vec<std::vector<uint8_t>>(16);
96
97 secure_vector<uint8_t> derived_key(32 + 2);
98
99 const std::string pbkdf_name = "PBKDF2(SHA-512)";
100 auto pbkdf_fam = PasswordHashFamily::create_or_throw(pbkdf_name);
101
102 constexpr uint32_t desired_runtime_msec = 100;
103 auto pbkdf = pbkdf_fam->tune_params(derived_key.size(), desired_runtime_msec);
104
105 pbkdf->derive_key(
106 derived_key.data(), derived_key.size(), passphrase.data(), passphrase.size(), salt.data(), salt.size());
107
108 const size_t iterations = pbkdf->iterations();
109 const size_t check_val = make_uint16(derived_key[0], derived_key[1]);
110 m_session_key = SymmetricKey(std::span(derived_key).subspan(2));
111
112 auto stmt = m_db->new_statement("INSERT INTO tls_sessions_metadata VALUES (?1, ?2, ?3, ?4, ?5)");
113
114 stmt->bind(1, salt);
115 stmt->bind(2, iterations);
116 stmt->bind(3, check_val);
117 stmt->bind(4, pbkdf_name);
118 stmt->bind(5, rev);
119
120 stmt->spin();
121}
122
123void Session_Manager_SQL::initialize_existing_database(std::string_view passphrase) {
124 auto stmt = m_db->select("*", "tls_sessions_metadata");
125 if(!stmt->step()) {
126 throw Internal_Error("Failed to initialize TLS session database");
127 }
128
129 const auto salt = stmt->get_blob(0);
130 const size_t iterations = stmt->get_size_t(1);
131 const size_t check_val_db = stmt->get_size_t(2);
132 const std::string pbkdf_name = stmt->get_str(3).value();
133
134 secure_vector<uint8_t> derived_key(32 + 2);
135
136 auto pbkdf_fam = PasswordHashFamily::create_or_throw(pbkdf_name);
137 auto pbkdf = pbkdf_fam->from_params(iterations);
138
139 pbkdf->derive_key(
140 derived_key.data(), derived_key.size(), passphrase.data(), passphrase.size(), salt.data(), salt.size());
141
142 const size_t check_val_created = make_uint16(derived_key[0], derived_key[1]);
143
144 if(check_val_created != check_val_db) {
145 throw Invalid_Argument("Session database password not valid");
146 }
147
148 m_session_key = SymmetricKey(std::span(derived_key).subspan(2));
149}
150
151void Session_Manager_SQL::store(const Session& session, const Session_Handle& handle) {
152 std::optional<lock_guard_type<recursive_mutex_type>> lk;
154 lk.emplace(mutex());
155 }
156
157 if(session.server_info().hostname().empty()) {
158 return;
159 }
160
161 auto stmt = m_db->upsert("tls_sessions",
162 {"session_id", "session_ticket", "session_start", "hostname", "hostport", "session"});
163
164 // Generate a random session ID if the peer did not provide one. Note that
165 // this ID will not be returned on ::find(), as the ticket is preferred.
166 const auto id = handle.id().value_or(m_rng->random_vec<Session_ID>(32));
167 const auto ticket = handle.ticket().value_or(Session_Ticket());
168
169 stmt->bind(1, hex_encode(id.get()));
170 stmt->bind(2, ticket.get());
171 stmt->bind(3, session.start_time());
172 stmt->bind(4, session.server_info().hostname());
173 stmt->bind(5, session.server_info().port());
174 stmt->bind(6, session.encrypt(m_session_key, *m_rng));
175
176 stmt->spin();
177
178 prune_session_cache();
179}
180
181std::optional<Session> Session_Manager_SQL::retrieve_one(const Session_Handle& handle) {
182 std::optional<lock_guard_type<recursive_mutex_type>> lk;
184 lk.emplace(mutex());
185 }
186
187 if(auto session_id = handle.id()) {
188 auto stmt = m_db->select("session", "tls_sessions", "session_id = ?1");
189
190 stmt->bind(1, hex_encode(session_id->get()));
191
192 while(stmt->step()) {
193 try {
194 return Session::decrypt(stmt->get_blob(0), m_session_key);
195 } catch(...) {}
196 }
197 }
198
199 return std::nullopt;
200}
201
202std::vector<Session_with_Handle> Session_Manager_SQL::find_some(const Server_Information& info,
203 const size_t max_sessions_hint) {
204 std::optional<lock_guard_type<recursive_mutex_type>> lk;
206 lk.emplace(mutex());
207 }
208
209 auto stmt = m_db->new_statement(
210 "SELECT session_id, session_ticket, session FROM tls_sessions"
211 " WHERE hostname = ?1 AND hostport = ?2"
212 " ORDER BY session_start DESC"
213 " LIMIT ?3");
214
215 stmt->bind(1, info.hostname());
216 stmt->bind(2, info.port());
217 stmt->bind(3, max_sessions_hint);
218
219 std::vector<Session_with_Handle> found_sessions;
220 while(stmt->step()) {
221 auto handle = [&]() -> Session_Handle {
222 auto ticket_blob = stmt->get_blob(1);
223 if(!ticket_blob.empty()) {
224 return Session_Handle(Session_Ticket(ticket_blob));
225 } else {
226 return Session_Handle(Session_ID(Botan::hex_decode(stmt->get_str(0).value())));
227 }
228 }();
229
230 try {
231 found_sessions.emplace_back(
232 Session_with_Handle{Session::decrypt(stmt->get_blob(2), m_session_key), std::move(handle)});
233 } catch(...) {}
234 }
235
236 return found_sessions;
237}
238
240 // The number of deleted rows is taken globally from the database connection,
241 // therefore we need to serialize this implementation.
243
244 if(const auto id = handle.id()) {
245 auto stmt = m_db->new_statement("DELETE FROM tls_sessions WHERE session_id = ?1");
246 stmt->bind(1, hex_encode(id->get()));
247 stmt->spin();
248 } else if(const auto ticket = handle.ticket()) {
249 auto stmt = m_db->new_statement("DELETE FROM tls_sessions WHERE session_ticket = ?1");
250 stmt->bind(1, ticket->get());
251 stmt->spin();
252 } else {
253 // should not happen, as session handles are exclusively either an ID or a ticket
254 throw Invalid_Argument("provided a session handle that is neither ID nor ticket");
255 }
256
257 return m_db->rows_changed_by_last_statement();
258}
259
261 // The number of deleted rows is taken globally from the database connection,
262 // therefore we need to serialize this implementation.
264
265 m_db->exec("DELETE FROM tls_sessions");
266 return m_db->rows_changed_by_last_statement();
267}
268
269void Session_Manager_SQL::prune_session_cache() {
270 // internal API: assuming that the lock is held already if needed
271
272 if(m_max_sessions == 0) {
273 return;
274 }
275
276 auto remove_oldest = m_db->new_statement(
277 "DELETE FROM tls_sessions WHERE session_id NOT IN "
278 "(SELECT session_id FROM tls_sessions ORDER BY session_start DESC LIMIT ?1)");
279 remove_oldest->bind(1, m_max_sessions);
280 remove_oldest->spin();
281}
282
283} // namespace Botan::TLS
static std::unique_ptr< PasswordHashFamily > create_or_throw(std::string_view algo_spec, std::string_view provider="")
Definition pwdhash.cpp:123
std::chrono::system_clock::time_point start_time() const
Definition tls_session.h:69
const Server_Information & server_info() const
Helper class to embody a session handle in all protocol versions.
std::optional< Session_Ticket > ticket() const
std::optional< Session_ID > id() const
Session_Manager_SQL(std::shared_ptr< SQL_Database > db, std::string_view passphrase, const std::shared_ptr< RandomNumberGenerator > &rng, size_t max_sessions=1000)
void store(const Session &session, const Session_Handle &handle) override
Save a Session under a Session_Handle (TLS Client).
size_t remove(const Session_Handle &handle) override
std::vector< Session_with_Handle > find_some(const Server_Information &info, size_t max_sessions_hint) override
Internal retrieval function to find sessions to resume.
std::optional< Session > retrieve_one(const Session_Handle &handle) override
Internal retrieval function for a single session.
recursive_mutex_type & mutex()
BOTAN_FUTURE_EXPLICIT Session_Manager(const std::shared_ptr< RandomNumberGenerator > &rng)
std::shared_ptr< RandomNumberGenerator > m_rng
std::vector< uint8_t > encrypt(const SymmetricKey &key, RandomNumberGenerator &rng) const
static Session decrypt(const uint8_t ctext[], size_t ctext_size, const SymmetricKey &key)
Strong< std::vector< uint8_t >, struct Session_ID_ > Session_ID
holds a TLS 1.2 session ID for stateful resumption
Strong< std::vector< uint8_t >, struct Session_Ticket_ > Session_Ticket
holds a TLS 1.2 session ticket for stateless resumption
OctetString SymmetricKey
Definition symkey.h:153
void hex_encode(char output[], const uint8_t input[], size_t input_length, bool uppercase)
Definition hex.cpp:34
size_t hex_decode(uint8_t output[], const char input[], size_t input_length, size_t &input_consumed, bool ignore_ws)
Definition hex.cpp:75
std::vector< T, secure_allocator< T > > secure_vector
Definition secmem.h:128
lock_guard< T > lock_guard_type
Definition mutex.h:58
constexpr uint16_t make_uint16(uint8_t i0, uint8_t i1)
Definition loadstor.h:92