1 /*
2 ** Copyright (c) 2008 - present, Alexis Megas.
3 ** All rights reserved.
4 **
5 ** Redistribution and use in source and binary forms, with or without
6 ** modification, are permitted provided that the following conditions
7 ** are met:
8 ** 1. Redistributions of source code must retain the above copyright
9 **    notice, this list of conditions and the following disclaimer.
10 ** 2. Redistributions in binary form must reproduce the above copyright
11 **    notice, this list of conditions and the following disclaimer in the
12 **    documentation and/or other materials provided with the distribution.
13 ** 3. The name of the author may not be used to endorse or promote products
14 **    derived from Dooble without specific prior written permission.
15 **
16 ** DOOBLE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
17 ** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18 ** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19 ** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20 ** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21 ** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
25 ** DOOBLE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 */
27 
28 #include <QDir>
29 #include <QSqlQuery>
30 #include <QWebEngineCookieStore>
31 #include <QWebEngineProfile>
32 
33 #include "dooble.h"
34 #include "dooble_cookies.h"
35 #include "dooble_cryptography.h"
36 #include "dooble_database_utilities.h"
37 
dooble_cookies(bool is_private,QObject * parent)38 dooble_cookies::dooble_cookies(bool is_private, QObject *parent):QObject(parent)
39 {
40   m_is_private = is_private;
41 }
42 
identifier(const QNetworkCookie & cookie)43 QByteArray dooble_cookies::identifier(const QNetworkCookie &cookie)
44 {
45   QByteArray bytes;
46 
47   bytes.append(cookie.domain().toUtf8());
48   bytes.append(cookie.name());
49   bytes.append(cookie.path().toUtf8());
50   return bytes;
51 }
52 
create_tables(QSqlDatabase & db)53 void dooble_cookies::create_tables(QSqlDatabase &db)
54 {
55   db.open();
56 
57   QSqlQuery query(db);
58 
59   query.exec("CREATE TABLE IF NOT EXISTS dooble_cookies_domains ("
60 	     "domain TEXT NOT NULL, "
61 	     "domain_digest TEXT NOT NULL PRIMARY KEY, "
62 	     "favorite_digest TEXT NOT NULL)");
63   query.exec("CREATE TABLE IF NOT EXISTS dooble_cookies ("
64 	     "domain_digest TEXT NOT NULL, "
65 	     "identifier_digest TEXT NOT NULL, "
66 	     "raw_form BLOB NOT NULL, "
67 	     "PRIMARY KEY (domain_digest, identifier_digest), "
68 	     "FOREIGN KEY (domain_digest) REFERENCES "
69 	     "dooble_cookies_domains (domain_digest) ON DELETE CASCADE "
70 	     ")");
71 }
72 
purge(void)73 void dooble_cookies::purge(void)
74 {
75   auto database_name(dooble_database_utilities::database_name());
76 
77   {
78     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
79 
80     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
81 		       QDir::separator() +
82 		       "dooble_cookies.db");
83 
84     if(db.open())
85       {
86 	QSqlQuery query(db);
87 
88 	query.exec("PRAGMA foreign_keys = ON");
89 	query.exec("PRAGMA synchronous = OFF");
90 	query.exec("DELETE FROM dooble_cookies");
91 	query.exec("DELETE FROM dooble_cookies_domains");
92 	query.exec("VACUUM");
93       }
94 
95     db.close();
96   }
97 
98   QSqlDatabase::removeDatabase(database_name);
99 }
100 
slot_connect_cookie_added_signal(void)101 void dooble_cookies::slot_connect_cookie_added_signal(void)
102 {
103   connect(QWebEngineProfile::defaultProfile()->cookieStore(),
104 	  SIGNAL(cookieAdded(const QNetworkCookie &)),
105 	  dooble::s_cookies,
106 	  SLOT(slot_cookie_added(const QNetworkCookie &)),
107 	  Qt::UniqueConnection);
108   emit populated();
109 }
110 
slot_cookie_added(const QNetworkCookie & cookie)111 void dooble_cookies::slot_cookie_added(const QNetworkCookie &cookie)
112 {
113   emit cookies_added
114     (QList<QNetworkCookie> () << cookie, QList<bool> () << false);
115 
116   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
117     return;
118   else if(m_is_private)
119     return;
120 
121   QDateTime now;
122 
123   if(cookie.isSessionCookie())
124     {
125       if(dooble_settings::cookie_policy_string(dooble_settings::
126 					       setting("cookie_policy_index").
127 					       toInt()) == "save_all")
128 	/*
129 	** Allow a session cookie to be saved.
130 	*/
131 
132 	goto save_label;
133       else
134 	return;
135     }
136   else if(dooble_settings::
137 	  cookie_policy_string(dooble::s_settings->
138 			       setting("cookie_policy_index").
139 			       toInt()) == "do_not_save")
140     return;
141 
142   now = QDateTime::currentDateTime();
143 
144   if(cookie.expirationDate().toLocalTime() <= now)
145     return;
146 
147  save_label:
148 
149   auto database_name(dooble_database_utilities::database_name());
150 
151   {
152     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
153 
154     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
155 		       QDir::separator() +
156 		       "dooble_cookies.db");
157 
158     if(db.open())
159       {
160 	create_tables(db);
161 
162 	QSqlQuery query(db);
163 
164 	query.exec("PRAGMA synchronous = OFF");
165 	query.prepare
166 	  ("INSERT INTO dooble_cookies_domains "
167 	   "(domain, domain_digest, favorite_digest) VALUES (?, ?, ?)");
168 
169 	QByteArray bytes;
170 
171 	bytes = dooble::s_cryptography->encrypt_then_mac
172 	  (cookie.domain().toUtf8());
173 
174 	if(!bytes.isEmpty())
175 	  {
176 	    query.addBindValue(bytes.toBase64());
177 	    query.addBindValue
178 	      (dooble::s_cryptography->hmac(cookie.domain()).toBase64());
179 	    query.addBindValue
180 	      (dooble::s_cryptography->hmac(QByteArray("false")).toBase64());
181 	    query.exec();
182 	  }
183 
184 	query.prepare
185 	  ("INSERT OR REPLACE INTO dooble_cookies "
186 	   "(domain_digest, identifier_digest, raw_form) VALUES (?, ?, ?)");
187 	query.addBindValue
188 	  (dooble::s_cryptography->hmac(cookie.domain()).toBase64());
189 	query.addBindValue
190 	  (dooble::s_cryptography->hmac(identifier(cookie)).toBase64());
191 	bytes = dooble::s_cryptography->encrypt_then_mac(cookie.toRawForm());
192 
193 	if(!bytes.isEmpty())
194 	  query.addBindValue(bytes.toBase64());
195 	else
196 	  goto done_label;
197 
198 	query.exec();
199       }
200 
201   done_label:
202     db.close();
203   }
204 
205   QSqlDatabase::removeDatabase(database_name);
206 }
207 
slot_cookie_removed(const QNetworkCookie & cookie)208 void dooble_cookies::slot_cookie_removed(const QNetworkCookie &cookie)
209 {
210   emit cookie_removed(cookie);
211 
212   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
213     return;
214   else if(m_is_private)
215     return;
216 
217   auto database_name(dooble_database_utilities::database_name());
218 
219   {
220     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
221 
222     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
223 		       QDir::separator() +
224 		       "dooble_cookies.db");
225 
226     if(db.open())
227       {
228 	QSqlQuery query(db);
229 
230 	query.exec("PRAGMA synchronous = OFF");
231 	query.prepare("DELETE FROM dooble_cookies WHERE identifier_digest = ?");
232 	query.addBindValue
233 	  (dooble::s_cryptography->hmac(identifier(cookie)).toBase64());
234 	query.exec();
235       }
236 
237     db.close();
238   }
239 
240   QSqlDatabase::removeDatabase(database_name);
241 }
242 
slot_delete_cookie(const QNetworkCookie & cookie)243 void dooble_cookies::slot_delete_cookie(const QNetworkCookie &cookie)
244 {
245   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
246     return;
247   else if(m_is_private)
248     return;
249 
250   auto database_name(dooble_database_utilities::database_name());
251 
252   {
253     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
254 
255     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
256 		       QDir::separator() +
257 		       "dooble_cookies.db");
258 
259     if(db.open())
260       {
261 	QSqlQuery query(db);
262 
263 	query.exec("PRAGMA synchronous = OFF");
264 	query.prepare("DELETE FROM dooble_cookies WHERE identifier_digest = ?");
265 	query.addBindValue
266 	  (dooble::s_cryptography->hmac(identifier(cookie)).toBase64());
267 	query.exec();
268 	query.prepare("DELETE FROM dooble_cookies_domains WHERE "
269 		      "domain_digest NOT IN (SELECT domain_digest FROM "
270 		      "dooble_cookies) AND favorite_digest = ?");
271 	query.addBindValue
272 	  (dooble::s_cryptography->hmac(QByteArray("false")).toBase64());
273 	query.exec();
274       }
275 
276     db.close();
277   }
278 
279   QSqlDatabase::removeDatabase(database_name);
280 }
281 
slot_delete_domain(const QString & domain)282 void dooble_cookies::slot_delete_domain(const QString &domain)
283 {
284   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
285     return;
286   else if(m_is_private)
287     return;
288 
289   auto database_name(dooble_database_utilities::database_name());
290 
291   {
292     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
293 
294     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
295 		       QDir::separator() +
296 		       "dooble_cookies.db");
297 
298     if(db.open())
299       {
300 	QSqlQuery query(db);
301 
302 	query.exec("PRAGMA foreign_keys = ON");
303 	query.exec("PRAGMA synchronous = OFF");
304 	query.prepare("DELETE FROM dooble_cookies_domains WHERE "
305 		      "domain_digest = ?");
306 	query.addBindValue
307 	  (dooble::s_cryptography->hmac(domain.toUtf8()).toBase64());
308 	query.exec();
309       }
310 
311     db.close();
312   }
313 
314   QSqlDatabase::removeDatabase(database_name);
315 }
316 
slot_delete_items(const QList<QNetworkCookie> & cookies,const QStringList & domains)317 void dooble_cookies::slot_delete_items(const QList<QNetworkCookie> &cookies,
318 				       const QStringList &domains)
319 {
320   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
321     return;
322   else if(m_is_private)
323     return;
324 
325   auto database_name(dooble_database_utilities::database_name());
326 
327   {
328     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
329 
330     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
331 		       QDir::separator() +
332 		       "dooble_cookies.db");
333 
334     if(db.open())
335       {
336 	QSqlQuery query(db);
337 
338 	query.exec("PRAGMA synchronous = OFF");
339 
340 	for(const auto &cookie : cookies)
341 	  {
342 	    query.prepare
343 	      ("DELETE FROM dooble_cookies WHERE identifier_digest = ?");
344 	    query.addBindValue
345 	      (dooble::s_cryptography->
346 	       hmac(identifier(cookie)).toBase64());
347 	    query.exec();
348 	  }
349 
350 	query.prepare("DELETE FROM dooble_cookies_domains WHERE "
351 		      "domain_digest NOT IN (SELECT domain_digest FROM "
352 		      "dooble_cookies) AND favorite_digest = ?");
353 	query.addBindValue
354 	  (dooble::s_cryptography->hmac(QByteArray("false")).toBase64());
355 	query.exec();
356 	query.exec("PRAGMA foreign_keys = ON");
357 
358 	for(const auto &domain : domains)
359 	  {
360 	    query.prepare("DELETE FROM dooble_cookies_domains WHERE "
361 			  "domain_digest = ?");
362 	    query.addBindValue
363 	      (dooble::s_cryptography->hmac(domain.toUtf8()).toBase64());
364 	    query.exec();
365 	  }
366       }
367 
368     db.close();
369   }
370 
371   QSqlDatabase::removeDatabase(database_name);
372 }
373 
slot_populate(void)374 void dooble_cookies::slot_populate(void)
375 {
376   if(!dooble::s_cryptography || !dooble::s_cryptography->authenticated())
377     {
378       emit populated();
379       return;
380     }
381   else if(m_is_private)
382     {
383       emit populated();
384       return;
385     }
386 
387   QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
388 
389   auto database_name(dooble_database_utilities::database_name());
390   auto profile = QWebEngineProfile::defaultProfile();
391   int count = 0;
392 
393   disconnect(profile->cookieStore(),
394 	     SIGNAL(cookieAdded(const QNetworkCookie &)),
395 	     dooble::s_cookies,
396 	     SLOT(slot_cookie_added(const QNetworkCookie &)));
397 
398   {
399     auto db = QSqlDatabase::addDatabase("QSQLITE", database_name);
400 
401     db.setDatabaseName(dooble_settings::setting("home_path").toString() +
402 		       QDir::separator() +
403 		       "dooble_cookies.db");
404 
405     if(db.open())
406       {
407 	create_tables(db);
408 
409 	QList<QNetworkCookie> cookies;
410 	QList<bool> is_favorites;
411 	QSqlQuery query(db);
412 
413 	query.setForwardOnly(true);
414 
415 	if(query.exec("SELECT domain, favorite_digest FROM "
416 		      "dooble_cookies_domains"))
417 	  while(query.next())
418 	    {
419 	      auto bytes
420 		(QByteArray::fromBase64(query.value(0).toByteArray()));
421 
422 	      bytes = dooble::s_cryptography->mac_then_decrypt(bytes);
423 
424 	      if(bytes.isEmpty())
425 		{
426 		  QSqlQuery delete_query(db);
427 
428 		  delete_query.exec("PRAGMA foreign_keys = ON");
429 		  delete_query.prepare
430 		    ("DELETE FROM dooble_cookies_domains WHERE domain = ?");
431 		  delete_query.addBindValue(query.value(0));
432 		  delete_query.exec();
433 		  continue;
434 		}
435 
436 	      QNetworkCookie cookie;
437 	      auto is_favorite = dooble_cryptography::memcmp
438 		(dooble::s_cryptography->hmac(QByteArray("true")).toBase64(),
439 		 query.value(1).toByteArray());
440 
441 	      cookie.setDomain(bytes);
442 	      cookies << cookie;
443 	      is_favorites << is_favorite;
444 	    }
445 
446 	if(!cookies.isEmpty() && !is_favorites.isEmpty())
447 	  emit cookies_added(cookies, is_favorites);
448 
449 	cookies.clear();
450 	is_favorites.clear();
451 
452 	if(query.exec("SELECT "
453 		      "(SELECT favorite_digest FROM dooble_cookies_domains a "
454 		      "WHERE a.domain_digest = b.domain_digest) "
455 		      "AS favorite_digest, "
456 		      "raw_form FROM dooble_cookies b"))
457 	  while(query.next())
458 	    {
459 	      auto bytes
460 		(QByteArray::fromBase64(query.value(1).toByteArray()));
461 
462 	      bytes = dooble::s_cryptography->mac_then_decrypt(bytes);
463 
464 	      if(bytes.isEmpty())
465 		{
466 		  QSqlQuery delete_query(db);
467 
468 		  delete_query.prepare
469 		    ("DELETE FROM dooble_cookies WHERE raw_form = ?");
470 		  delete_query.addBindValue(query.value(1));
471 		  delete_query.exec();
472 		  delete_query.prepare
473 		    ("DELETE FROM dooble_cookies_domains WHERE "
474 		     "domain_digest NOT IN (SELECT domain_digest FROM "
475 		     "dooble_cookies) AND favorite_digest = ?");
476 		  delete_query.addBindValue
477 		    (dooble::s_cryptography->hmac(QByteArray("false")).
478 		     toBase64());
479 		  delete_query.exec();
480 		  continue;
481 		}
482 
483 	      auto cookie = QNetworkCookie::parseCookies(bytes);
484 
485 	      if(cookie.isEmpty())
486 		{
487 		  QSqlQuery delete_query(db);
488 
489 		  delete_query.prepare
490 		    ("DELETE FROM dooble_cookies WHERE raw_form = ?");
491 		  delete_query.addBindValue(query.value(1));
492 		  delete_query.exec();
493 		  delete_query.prepare
494 		    ("DELETE FROM dooble_cookies_domains WHERE "
495 		     "domain_digest NOT IN (SELECT domain_digest FROM "
496 		     "dooble_cookies) AND favorite_digest = ?");
497 		  delete_query.addBindValue
498 		    (dooble::s_cryptography->hmac(QByteArray("false")).
499 		     toBase64());
500 		  delete_query.exec();
501 		  continue;
502 		}
503 
504 	      auto allow_expired = false;
505 	      auto now(QDateTime::currentDateTime());
506 
507 	      if(cookie.at(0).isSessionCookie())
508 		{
509 		  if(dooble_settings::
510 		     cookie_policy_string(dooble_settings::
511 					  setting("cookie_policy_index").
512 					  toInt()) == "save_all")
513 		    /*
514 		    ** Ignore the session cookie's expiration date.
515 		    */
516 
517 		    allow_expired = true;
518 		}
519 	      else
520 		allow_expired = false;
521 
522 	      if(!allow_expired)
523 		if(cookie.at(0).expirationDate().toLocalTime() <= now)
524 		  {
525 		    QSqlQuery delete_query(db);
526 
527 		    delete_query.prepare
528 		      ("DELETE FROM dooble_cookies WHERE raw_form = ?");
529 		    delete_query.addBindValue(query.value(1));
530 		    delete_query.exec();
531 		    delete_query.prepare
532 		      ("DELETE FROM dooble_cookies_domains WHERE "
533 		       "domain_digest NOT IN (SELECT domain_digest FROM "
534 		       "dooble_cookies) AND favorite_digest = ?");
535 		    delete_query.addBindValue
536 		      (dooble::s_cryptography->hmac(QByteArray("false")).
537 		       toBase64());
538 		    delete_query.exec();
539 		    continue;
540 		  }
541 
542 	      auto c(cookie.at(0));
543 	      auto is_favorite = dooble_cryptography::memcmp
544 		(dooble::s_cryptography->hmac(QByteArray("true")).toBase64(),
545 		 query.value(0).toByteArray());
546 
547 #ifdef DOOBLE_COOKIES_REPLACE_HYPHEN_WITH_UNDERSCORE
548 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
549 	      c.setName(c.name().replace('-', '_'));
550 #endif
551 #endif
552 	      cookies << c;
553 
554 	      auto url(QUrl::fromUserInput(c.domain()));
555 
556 	      if(c.isSecure())
557 		url.setScheme("https");
558 
559 	      c.setDomain("");
560 	      count += 1;
561 	      is_favorites << is_favorite;
562 	      profile->cookieStore()->setCookie(c, url);
563 	    }
564 
565 	if(!cookies.isEmpty() && !is_favorites.isEmpty())
566 	  emit cookies_added(cookies, is_favorites);
567       }
568 
569     db.close();
570   }
571 
572   QSqlDatabase::removeDatabase(database_name);
573   QApplication::restoreOverrideCursor();
574 
575   /*
576   ** Re-connect the cookieAdded() signal.
577   */
578 
579   QTimer::singleShot(count, this, SLOT(slot_connect_cookie_added_signal(void)));
580   emit populated();
581 }
582