1 // Copyright 2024 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "net/device_bound_sessions/session_store_impl.h"
6
7 #include <algorithm>
8
9 #include "base/sequence_checker.h"
10 #include "base/task/sequenced_task_runner.h"
11 #include "base/task/thread_pool.h"
12 #include "base/time/time.h"
13 #include "components/unexportable_keys/background_task_priority.h"
14 #include "components/unexportable_keys/service_error.h"
15 #include "components/unexportable_keys/unexportable_key_id.h"
16 #include "components/unexportable_keys/unexportable_key_service.h"
17 #include "net/base/schemeful_site.h"
18 #include "net/device_bound_sessions/proto/storage.pb.h"
19
20 namespace net::device_bound_sessions {
21
22 namespace {
23
24 using unexportable_keys::BackgroundTaskPriority;
25 using unexportable_keys::ServiceError;
26 using unexportable_keys::ServiceErrorOr;
27 using unexportable_keys::UnexportableKeyId;
28 using unexportable_keys::UnexportableKeyService;
29
30 // Priority is set to `USER_VISIBLE` because the initial load of
31 // sessions from disk is required to complete before URL requests
32 // can be checked to see if they are associated with bound sessions.
33 constexpr base::TaskTraits kDBTaskTraits = {
34 base::MayBlock(), base::TaskPriority::USER_VISIBLE,
35 base::TaskShutdownBehavior::BLOCK_SHUTDOWN};
36
37 const int kCurrentSchemaVersion = 1;
38 const char kSessionTableName[] = "dbsc_session_tbl";
39 const base::TimeDelta kFlushDelay = base::Seconds(2);
40
InitializeOnDbSequence(sql::Database * db,base::FilePath db_storage_path,sqlite_proto::ProtoTableManager * table_manager,sqlite_proto::KeyValueData<proto::SiteSessions> * session_data)41 SessionStoreImpl::DBStatus InitializeOnDbSequence(
42 sql::Database* db,
43 base::FilePath db_storage_path,
44 sqlite_proto::ProtoTableManager* table_manager,
45 sqlite_proto::KeyValueData<proto::SiteSessions>* session_data) {
46 if (db->Open(db_storage_path) == false) {
47 return SessionStoreImpl::DBStatus::kFailure;
48 }
49
50 db->Preload();
51
52 table_manager->InitializeOnDbSequence(
53 db, std::vector<std::string>{kSessionTableName}, kCurrentSchemaVersion);
54 session_data->InitializeOnDBSequence();
55
56 return SessionStoreImpl::DBStatus::kSuccess;
57 }
58
59 } // namespace
60
SessionStoreImpl(base::FilePath db_storage_path,UnexportableKeyService & key_service)61 SessionStoreImpl::SessionStoreImpl(base::FilePath db_storage_path,
62 UnexportableKeyService& key_service)
63 : key_service_(key_service),
64 db_task_runner_(
65 base::ThreadPool::CreateSequencedTaskRunner(kDBTaskTraits)),
66 db_storage_path_(std::move(db_storage_path)),
67 db_(std::make_unique<sql::Database>(
68 sql::DatabaseOptions{.page_size = 4096, .cache_size = 500})),
69 table_manager_(base::MakeRefCounted<sqlite_proto::ProtoTableManager>(
70 db_task_runner_)),
71 session_table_(
72 std::make_unique<sqlite_proto::KeyValueTable<proto::SiteSessions>>(
73 kSessionTableName)),
74 session_data_(
75 std::make_unique<sqlite_proto::KeyValueData<proto::SiteSessions>>(
76 table_manager_,
77 session_table_.get(),
78 /*max_num_entries=*/std::nullopt,
79 kFlushDelay)) {
80 db_->set_histogram_tag("DBSCSessions");
81 }
82
~SessionStoreImpl()83 SessionStoreImpl::~SessionStoreImpl() {
84 DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
85
86 if (db_status_ == DBStatus::kSuccess) {
87 session_data_->FlushDataToDisk();
88 }
89
90 // Shutdown `table_manager_`, and delete it together with `db_`
91 // and KeyValueTable on DB sequence, then delete the KeyValueData
92 // and call `shutdown_callback_` on main sequence.
93 // This ensures that DB objects outlive any other task posted to DB
94 // sequence, since their deletion is the very last posted task.
95 db_task_runner_->PostTaskAndReply(
96 FROM_HERE,
97 base::BindOnce(
98 [](scoped_refptr<sqlite_proto::ProtoTableManager> table_manager,
99 std::unique_ptr<sql::Database> db,
100 auto session_table) { table_manager->WillShutdown(); },
101 std::move(table_manager_), std::move(db_), std::move(session_table_)),
102 base::BindOnce(
103 [](auto session_data, base::OnceClosure shutdown_callback) {
104 if (shutdown_callback) {
105 std::move(shutdown_callback).Run();
106 }
107 },
108 std::move(session_data_), std::move(shutdown_callback_)));
109 }
110
LoadSessions(LoadSessionsCallback callback)111 void SessionStoreImpl::LoadSessions(LoadSessionsCallback callback) {
112 CHECK_EQ(db_status_, DBStatus::kNotLoaded);
113
114 // This is safe because tasks are serialized on the db_task_runner sequence
115 // and the `table_manager_` and `session_data_` are only freed after a
116 // response from a task (triggered by the destructor) runs on the
117 // `db_task_runner_`.
118 // Similarly, the `db_` is not actually destroyed until the task
119 // triggered by the destructor runs on the `db_task_runner_`.
120 db_task_runner_->PostTaskAndReplyWithResult(
121 FROM_HERE,
122 base::BindOnce(&InitializeOnDbSequence, base::Unretained(db_.get()),
123 db_storage_path_, base::Unretained(table_manager_.get()),
124 base::Unretained(session_data_.get())),
125 base::BindOnce(&SessionStoreImpl::OnDatabaseLoaded,
126 weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
127 }
128
OnDatabaseLoaded(LoadSessionsCallback callback,DBStatus db_status)129 void SessionStoreImpl::OnDatabaseLoaded(LoadSessionsCallback callback,
130 DBStatus db_status) {
131 db_status_ = db_status;
132 SessionsMap sessions;
133 if (db_status == DBStatus::kSuccess) {
134 std::vector<std::string> keys_to_delete;
135 sessions = CreateSessionsFromLoadedData(session_data_->GetAllCached(),
136 keys_to_delete);
137 if (keys_to_delete.size() > 0) {
138 session_data_->DeleteData(keys_to_delete);
139 }
140 }
141 std::move(callback).Run(std::move(sessions));
142 }
143
144 // static
CreateSessionsFromLoadedData(const std::map<std::string,proto::SiteSessions> & loaded_data,std::vector<std::string> & keys_to_delete)145 SessionStore::SessionsMap SessionStoreImpl::CreateSessionsFromLoadedData(
146 const std::map<std::string, proto::SiteSessions>& loaded_data,
147 std::vector<std::string>& keys_to_delete) {
148 SessionsMap all_sessions;
149 for (const auto& [site_str, site_proto] : loaded_data) {
150 SchemefulSite site = net::SchemefulSite::Deserialize(site_str);
151 if (site.opaque()) {
152 keys_to_delete.push_back(site_str);
153 continue;
154 }
155
156 bool invalid_session_found = false;
157 SessionsMap site_sessions;
158 for (const auto& [session_id, session_proto] : site_proto.sessions()) {
159 if (!session_proto.has_wrapped_key() ||
160 session_proto.wrapped_key().empty()) {
161 invalid_session_found = true;
162 break;
163 }
164
165 std::unique_ptr<Session> session =
166 Session::CreateFromProto(session_proto);
167 if (!session) {
168 invalid_session_found = true;
169 break;
170 }
171
172 // Restored session entry has passed basic validation checks. Save it.
173 site_sessions.emplace(site, std::move(session));
174 }
175
176 // Remove the entire site entry from the DB if a single invalid session is
177 // found as it could be a sign of data corruption or external manipulation.
178 // Note: A session could also cease to be valid because the criteria for
179 // validity changed after a Chrome update. In this scenario, however, we
180 // would migrate that session rather than deleting the site sessions.
181 if (invalid_session_found) {
182 keys_to_delete.push_back(site_str);
183 } else {
184 all_sessions.merge(site_sessions);
185 }
186 }
187
188 return all_sessions;
189 }
190
SetShutdownCallbackForTesting(base::OnceClosure shutdown_callback)191 void SessionStoreImpl::SetShutdownCallbackForTesting(
192 base::OnceClosure shutdown_callback) {
193 shutdown_callback_ = std::move(shutdown_callback);
194 }
195
SaveSession(const SchemefulSite & site,const Session & session)196 void SessionStoreImpl::SaveSession(const SchemefulSite& site,
197 const Session& session) {
198 if (db_status_ != DBStatus::kSuccess) {
199 return;
200 }
201
202 CHECK(session.unexportable_key_id().has_value());
203
204 // Wrap the unexportable key into a persistable form.
205 ServiceErrorOr<std::vector<uint8_t>> wrapped_key =
206 key_service_->GetWrappedKey(*session.unexportable_key_id());
207 // Don't bother persisting the session if wrapping fails because we will throw
208 // away all persisted data if the wrapped key is missing for any session.
209 if (!wrapped_key.has_value()) {
210 return;
211 }
212
213 proto::Session session_proto = session.ToProto();
214 session_proto.set_wrapped_key(
215 std::string(wrapped_key->begin(), wrapped_key->end()));
216 proto::SiteSessions site_proto;
217 std::string site_str = site.Serialize();
218 session_data_->TryGetData(site_str, &site_proto);
219 (*site_proto.mutable_sessions())[session_proto.id()] =
220 std::move(session_proto);
221
222 session_data_->UpdateData(site_str, site_proto);
223 }
224
DeleteSession(const SchemefulSite & site,const Session::Id & session_id)225 void SessionStoreImpl::DeleteSession(const SchemefulSite& site,
226 const Session::Id& session_id) {
227 if (db_status_ != DBStatus::kSuccess) {
228 return;
229 }
230
231 proto::SiteSessions site_proto;
232 std::string site_str = site.Serialize();
233 if (!session_data_->TryGetData(site_str, &site_proto)) {
234 return;
235 }
236
237 if (site_proto.sessions().count(*session_id) == 0) {
238 return;
239 }
240
241 // If this is the only session associated with the site,
242 // delete the site entry.
243 if (site_proto.mutable_sessions()->size() == 1) {
244 session_data_->DeleteData({site_str});
245 return;
246 }
247
248 site_proto.mutable_sessions()->erase(*session_id);
249
250 // Schedule a DB update for the site entry.
251 session_data_->UpdateData(site.Serialize(), site_proto);
252 }
253
GetAllSessions() const254 SessionStore::SessionsMap SessionStoreImpl::GetAllSessions() const {
255 if (db_status_ != DBStatus::kSuccess) {
256 return SessionsMap();
257 }
258
259 std::vector<std::string> keys_to_delete;
260 SessionsMap all_sessions = CreateSessionsFromLoadedData(
261 session_data_->GetAllCached(), keys_to_delete);
262 // We shouldn't find invalid keys at this point, they should have all been
263 // filtered out in the `LoadSessions` operations.
264 CHECK(keys_to_delete.empty());
265
266 return all_sessions;
267 }
268
RestoreSessionBindingKey(const SchemefulSite & site,const Session::Id & session_id,RestoreSessionBindingKeyCallback callback)269 void SessionStoreImpl::RestoreSessionBindingKey(
270 const SchemefulSite& site,
271 const Session::Id& session_id,
272 RestoreSessionBindingKeyCallback callback) {
273 auto key_id_or_error = base::unexpected(ServiceError::kKeyNotFound);
274 if (db_status_ != DBStatus::kSuccess) {
275 std::move(callback).Run(key_id_or_error);
276 return;
277 }
278
279 // Retrieve the session's persisted binding key and unwrap it.
280 proto::SiteSessions site_proto;
281 if (session_data_->TryGetData(site.Serialize(), &site_proto)) {
282 auto it = site_proto.sessions().find(*session_id);
283 if (it != site_proto.sessions().end()) {
284 // Unwrap the binding key asynchronously.
285 std::vector<uint8_t> wrapped_key(it->second.wrapped_key().begin(),
286 it->second.wrapped_key().end());
287 key_service_->FromWrappedSigningKeySlowlyAsync(
288 wrapped_key, BackgroundTaskPriority::kUserVisible,
289 std::move(callback));
290 return;
291 }
292 }
293
294 // The session is not present in the store,
295 // invoke the callback immediately.
296 std::move(callback).Run(key_id_or_error);
297 }
298
299 } // namespace net::device_bound_sessions
300