1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/password_manager/login_database.h"
10 #include "base/files/file_path.h"
11 #include "base/logging.h"
12 #include "base/metrics/histogram.h"
13 #include "base/pickle.h"
14 #include "base/strings/string_util.h"
15 #include "base/time/time.h"
16 #include "components/autofill/core/common/password_form.h"
17 #include "sql/connection.h"
18 #include "sql/statement.h"
19 #include "sql/transaction.h"
21 using autofill::PasswordForm
;
23 static const int kCurrentVersionNumber
= 4;
24 static const int kCompatibleVersionNumber
= 1;
28 // Convenience enum for interacting with SQL queries that use all the columns.
29 enum LoginTableColumns
{
30 COLUMN_ORIGIN_URL
= 0,
32 COLUMN_USERNAME_ELEMENT
,
33 COLUMN_USERNAME_VALUE
,
34 COLUMN_PASSWORD_ELEMENT
,
35 COLUMN_PASSWORD_VALUE
,
36 COLUMN_SUBMIT_ELEMENT
,
41 COLUMN_BLACKLISTED_BY_USER
,
44 COLUMN_POSSIBLE_USERNAMES
,
51 LoginDatabase::LoginDatabase() {
54 LoginDatabase::~LoginDatabase() {
57 bool LoginDatabase::Init(const base::FilePath
& db_path
) {
58 // Set pragmas for a small, private database (based on WebDatabase).
59 db_
.set_page_size(2048);
60 db_
.set_cache_size(32);
61 db_
.set_exclusive_locking();
62 db_
.set_restrict_to_user();
64 if (!db_
.Open(db_path
)) {
65 LOG(WARNING
) << "Unable to open the password store database.";
69 sql::Transaction
transaction(&db_
);
72 // Check the database version.
73 if (!meta_table_
.Init(&db_
, kCurrentVersionNumber
,
74 kCompatibleVersionNumber
)) {
78 if (meta_table_
.GetCompatibleVersionNumber() > kCurrentVersionNumber
) {
79 LOG(WARNING
) << "Password store database is too new.";
84 // Initialize the tables.
85 if (!InitLoginsTable()) {
86 LOG(WARNING
) << "Unable to initialize the password store database.";
91 // Save the path for DeleteDatabaseFile().
94 // If the file on disk is an older database version, bring it up to date.
95 if (!MigrateOldVersionsAsNeeded()) {
96 LOG(WARNING
) << "Unable to migrate database";
101 if (!transaction
.Commit()) {
109 bool LoginDatabase::MigrateOldVersionsAsNeeded() {
110 switch (meta_table_
.GetVersionNumber()) {
112 if (!db_
.Execute("ALTER TABLE logins "
113 "ADD COLUMN password_type INTEGER") ||
114 !db_
.Execute("ALTER TABLE logins "
115 "ADD COLUMN possible_usernames BLOB")) {
118 meta_table_
.SetVersionNumber(2);
121 if (!db_
.Execute("ALTER TABLE logins ADD COLUMN times_used INTEGER")) {
124 meta_table_
.SetVersionNumber(3);
127 // We need to check if the column exists because of
128 // https://crbug.com/295851
129 if (!db_
.DoesColumnExist("logins", "form_data") &&
130 !db_
.Execute("ALTER TABLE logins ADD COLUMN form_data BLOB")) {
133 meta_table_
.SetVersionNumber(4);
135 case kCurrentVersionNumber
:
136 // Already up to date
144 bool LoginDatabase::InitLoginsTable() {
145 if (!db_
.DoesTableExist("logins")) {
146 if (!db_
.Execute("CREATE TABLE logins ("
147 "origin_url VARCHAR NOT NULL, "
148 "action_url VARCHAR, "
149 "username_element VARCHAR, "
150 "username_value VARCHAR, "
151 "password_element VARCHAR, "
152 "password_value BLOB, "
153 "submit_element VARCHAR, "
154 "signon_realm VARCHAR NOT NULL,"
155 "ssl_valid INTEGER NOT NULL,"
156 "preferred INTEGER NOT NULL,"
157 "date_created INTEGER NOT NULL,"
158 "blacklisted_by_user INTEGER NOT NULL,"
159 "scheme INTEGER NOT NULL,"
160 "password_type INTEGER,"
161 "possible_usernames BLOB,"
162 "times_used INTEGER,"
165 "(origin_url, username_element, "
166 "username_value, password_element, "
167 "submit_element, signon_realm))")) {
171 if (!db_
.Execute("CREATE INDEX logins_signon ON "
172 "logins (signon_realm)")) {
180 void LoginDatabase::ReportMetrics() {
181 sql::Statement
s(db_
.GetCachedStatement(
183 "SELECT signon_realm, blacklisted_by_user, COUNT(username_value) "
184 "FROM logins GROUP BY signon_realm, blacklisted_by_user"));
189 int total_accounts
= 0;
190 int blacklisted_sites
= 0;
192 int blacklisted
= s
.ColumnInt(1);
193 int accounts_per_site
= s
.ColumnInt(2);
197 total_accounts
+= accounts_per_site
;
198 UMA_HISTOGRAM_CUSTOM_COUNTS("PasswordManager.AccountsPerSite",
199 accounts_per_site
, 0, 32, 6);
202 UMA_HISTOGRAM_CUSTOM_COUNTS("PasswordManager.TotalAccounts",
203 total_accounts
, 0, 32, 6);
204 UMA_HISTOGRAM_CUSTOM_COUNTS("PasswordManager.BlacklistedSites",
205 blacklisted_sites
, 0, 32, 6);
207 sql::Statement
usage_statement(db_
.GetCachedStatement(
209 "SELECT password_type, times_used FROM logins"));
211 if (!usage_statement
.is_valid())
214 while (usage_statement
.Step()) {
215 PasswordForm::Type type
= static_cast<PasswordForm::Type
>(
216 usage_statement
.ColumnInt(0));
218 if (type
== PasswordForm::TYPE_GENERATED
) {
219 UMA_HISTOGRAM_CUSTOM_COUNTS(
220 "PasswordManager.TimesGeneratedPasswordUsed",
221 usage_statement
.ColumnInt(1), 0, 100, 10);
223 UMA_HISTOGRAM_CUSTOM_COUNTS(
224 "PasswordManager.TimesPasswordUsed",
225 usage_statement
.ColumnInt(1), 0, 100, 10);
230 bool LoginDatabase::AddLogin(const PasswordForm
& form
) {
231 std::string encrypted_password
;
232 if (EncryptedString(form
.password_value
, &encrypted_password
) !=
233 ENCRYPTION_RESULT_SUCCESS
)
236 // You *must* change LoginTableColumns if this query changes.
237 sql::Statement
s(db_
.GetCachedStatement(SQL_FROM_HERE
,
238 "INSERT OR REPLACE INTO logins "
239 "(origin_url, action_url, username_element, username_value, "
240 " password_element, password_value, submit_element, "
241 " signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
242 " scheme, password_type, possible_usernames, times_used, form_data) "
244 "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
245 s
.BindString(COLUMN_ORIGIN_URL
, form
.origin
.spec());
246 s
.BindString(COLUMN_ACTION_URL
, form
.action
.spec());
247 s
.BindString16(COLUMN_USERNAME_ELEMENT
, form
.username_element
);
248 s
.BindString16(COLUMN_USERNAME_VALUE
, form
.username_value
);
249 s
.BindString16(COLUMN_PASSWORD_ELEMENT
, form
.password_element
);
250 s
.BindBlob(COLUMN_PASSWORD_VALUE
, encrypted_password
.data(),
251 static_cast<int>(encrypted_password
.length()));
252 s
.BindString16(COLUMN_SUBMIT_ELEMENT
, form
.submit_element
);
253 s
.BindString(COLUMN_SIGNON_REALM
, form
.signon_realm
);
254 s
.BindInt(COLUMN_SSL_VALID
, form
.ssl_valid
);
255 s
.BindInt(COLUMN_PREFERRED
, form
.preferred
);
256 s
.BindInt64(COLUMN_DATE_CREATED
, form
.date_created
.ToTimeT());
257 s
.BindInt(COLUMN_BLACKLISTED_BY_USER
, form
.blacklisted_by_user
);
258 s
.BindInt(COLUMN_SCHEME
, form
.scheme
);
259 s
.BindInt(COLUMN_PASSWORD_TYPE
, form
.type
);
260 Pickle usernames_pickle
= SerializeVector(form
.other_possible_usernames
);
261 s
.BindBlob(COLUMN_POSSIBLE_USERNAMES
,
262 usernames_pickle
.data(),
263 usernames_pickle
.size());
264 s
.BindInt(COLUMN_TIMES_USED
, form
.times_used
);
265 Pickle form_data_pickle
;
266 autofill::SerializeFormData(form
.form_data
, &form_data_pickle
);
267 s
.BindBlob(COLUMN_FORM_DATA
,
268 form_data_pickle
.data(),
269 form_data_pickle
.size());
274 bool LoginDatabase::UpdateLogin(const PasswordForm
& form
, int* items_changed
) {
275 std::string encrypted_password
;
276 if (EncryptedString(form
.password_value
, &encrypted_password
) !=
277 ENCRYPTION_RESULT_SUCCESS
)
280 sql::Statement
s(db_
.GetCachedStatement(SQL_FROM_HERE
,
283 "password_value = ?, "
286 "possible_usernames = ?, "
288 "WHERE origin_url = ? AND "
289 "username_element = ? AND "
290 "username_value = ? AND "
291 "password_element = ? AND "
292 "signon_realm = ?"));
293 s
.BindString(0, form
.action
.spec());
294 s
.BindBlob(1, encrypted_password
.data(),
295 static_cast<int>(encrypted_password
.length()));
296 s
.BindInt(2, form
.ssl_valid
);
297 s
.BindInt(3, form
.preferred
);
298 Pickle pickle
= SerializeVector(form
.other_possible_usernames
);
299 s
.BindBlob(4, pickle
.data(), pickle
.size());
300 s
.BindInt(5, form
.times_used
);
301 s
.BindString(6, form
.origin
.spec());
302 s
.BindString16(7, form
.username_element
);
303 s
.BindString16(8, form
.username_value
);
304 s
.BindString16(9, form
.password_element
);
305 s
.BindString(10, form
.signon_realm
);
311 *items_changed
= db_
.GetLastChangeCount();
316 bool LoginDatabase::RemoveLogin(const PasswordForm
& form
) {
317 // Remove a login by UNIQUE-constrained fields.
318 sql::Statement
s(db_
.GetCachedStatement(SQL_FROM_HERE
,
319 "DELETE FROM logins WHERE "
320 "origin_url = ? AND "
321 "username_element = ? AND "
322 "username_value = ? AND "
323 "password_element = ? AND "
324 "submit_element = ? AND "
325 "signon_realm = ? "));
326 s
.BindString(0, form
.origin
.spec());
327 s
.BindString16(1, form
.username_element
);
328 s
.BindString16(2, form
.username_value
);
329 s
.BindString16(3, form
.password_element
);
330 s
.BindString16(4, form
.submit_element
);
331 s
.BindString(5, form
.signon_realm
);
336 bool LoginDatabase::RemoveLoginsCreatedBetween(const base::Time delete_begin
,
337 const base::Time delete_end
) {
338 sql::Statement
s(db_
.GetCachedStatement(SQL_FROM_HERE
,
339 "DELETE FROM logins WHERE "
340 "date_created >= ? AND date_created < ?"));
341 s
.BindInt64(0, delete_begin
.ToTimeT());
342 s
.BindInt64(1, delete_end
.is_null() ? std::numeric_limits
<int64
>::max()
343 : delete_end
.ToTimeT());
348 LoginDatabase::EncryptionResult
LoginDatabase::InitPasswordFormFromStatement(
350 sql::Statement
& s
) const {
351 std::string encrypted_password
;
352 s
.ColumnBlobAsString(COLUMN_PASSWORD_VALUE
, &encrypted_password
);
353 base::string16 decrypted_password
;
354 EncryptionResult encryption_result
=
355 DecryptedString(encrypted_password
, &decrypted_password
);
356 if (encryption_result
!= ENCRYPTION_RESULT_SUCCESS
)
357 return encryption_result
;
359 std::string tmp
= s
.ColumnString(COLUMN_ORIGIN_URL
);
360 form
->origin
= GURL(tmp
);
361 tmp
= s
.ColumnString(COLUMN_ACTION_URL
);
362 form
->action
= GURL(tmp
);
363 form
->username_element
= s
.ColumnString16(COLUMN_USERNAME_ELEMENT
);
364 form
->username_value
= s
.ColumnString16(COLUMN_USERNAME_VALUE
);
365 form
->password_element
= s
.ColumnString16(COLUMN_PASSWORD_ELEMENT
);
366 form
->password_value
= decrypted_password
;
367 form
->submit_element
= s
.ColumnString16(COLUMN_SUBMIT_ELEMENT
);
368 tmp
= s
.ColumnString(COLUMN_SIGNON_REALM
);
369 form
->signon_realm
= tmp
;
370 form
->ssl_valid
= (s
.ColumnInt(COLUMN_SSL_VALID
) > 0);
371 form
->preferred
= (s
.ColumnInt(COLUMN_PREFERRED
) > 0);
372 form
->date_created
= base::Time::FromTimeT(
373 s
.ColumnInt64(COLUMN_DATE_CREATED
));
374 form
->blacklisted_by_user
= (s
.ColumnInt(COLUMN_BLACKLISTED_BY_USER
) > 0);
375 int scheme_int
= s
.ColumnInt(COLUMN_SCHEME
);
376 DCHECK((scheme_int
>= 0) && (scheme_int
<= PasswordForm::SCHEME_OTHER
));
377 form
->scheme
= static_cast<PasswordForm::Scheme
>(scheme_int
);
378 int type_int
= s
.ColumnInt(COLUMN_PASSWORD_TYPE
);
379 DCHECK(type_int
>= 0 && type_int
<= PasswordForm::TYPE_GENERATED
);
380 form
->type
= static_cast<PasswordForm::Type
>(type_int
);
382 static_cast<const char*>(s
.ColumnBlob(COLUMN_POSSIBLE_USERNAMES
)),
383 s
.ColumnByteLength(COLUMN_POSSIBLE_USERNAMES
));
384 form
->other_possible_usernames
= DeserializeVector(pickle
);
385 form
->times_used
= s
.ColumnInt(COLUMN_TIMES_USED
);
386 Pickle
form_data_pickle(
387 static_cast<const char*>(s
.ColumnBlob(COLUMN_FORM_DATA
)),
388 s
.ColumnByteLength(COLUMN_FORM_DATA
));
389 PickleIterator
form_data_iter(form_data_pickle
);
390 autofill::DeserializeFormData(&form_data_iter
, &form
->form_data
);
391 return ENCRYPTION_RESULT_SUCCESS
;
394 bool LoginDatabase::GetLogins(const PasswordForm
& form
,
395 std::vector
<PasswordForm
*>* forms
) const {
397 // You *must* change LoginTableColumns if this query changes.
398 const std::string sql_query
= "SELECT origin_url, action_url, "
399 "username_element, username_value, "
400 "password_element, password_value, submit_element, "
401 "signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
402 "scheme, password_type, possible_usernames, times_used, form_data "
403 "FROM logins WHERE signon_realm == ? ";
405 const GURL
signon_realm(form
.signon_realm
);
406 std::string registered_domain
=
407 PSLMatchingHelper::GetRegistryControlledDomain(signon_realm
);
408 PSLMatchingHelper::PSLDomainMatchMetric psl_domain_match_metric
=
409 PSLMatchingHelper::PSL_DOMAIN_MATCH_NONE
;
410 if (psl_helper_
.ShouldPSLDomainMatchingApply(registered_domain
)) {
411 // We are extending the original SQL query with one that includes more
412 // possible matches based on public suffix domain matching. Using a regexp
413 // here is just an optimization to not have to parse all the stored entries
414 // in the |logins| table. The result (scheme, domain and port) is verified
415 // further down using GURL. See the functions SchemeMatches,
416 // RegistryControlledDomainMatches and PortMatches.
417 const std::string extended_sql_query
=
418 sql_query
+ "OR signon_realm REGEXP ? ";
419 // TODO(nyquist) Re-enable usage of GetCachedStatement when
420 // http://crbug.com/248608 is fixed.
421 s
.Assign(db_
.GetUniqueStatement(extended_sql_query
.c_str()));
422 // We need to escape . in the domain. Since the domain has already been
423 // sanitized using GURL, we do not need to escape any other characters.
424 base::ReplaceChars(registered_domain
, ".", "\\.", ®istered_domain
);
425 std::string scheme
= signon_realm
.scheme();
426 // We need to escape . in the scheme. Since the scheme has already been
427 // sanitized using GURL, we do not need to escape any other characters.
428 // The scheme soap.beep is an example with '.'.
429 base::ReplaceChars(scheme
, ".", "\\.", &scheme
);
430 const std::string port
= signon_realm
.port();
431 // For a signon realm such as http://foo.bar/, this regexp will match
432 // domains on the form http://foo.bar/, http://www.foo.bar/,
433 // http://www.mobile.foo.bar/. It will not match http://notfoo.bar/.
434 // The scheme and port has to be the same as the observed form.
435 std::string regexp
= "^(" + scheme
+ ":\\/\\/)([\\w-]+\\.)*" +
436 registered_domain
+ "(:" + port
+ ")?\\/$";
437 s
.BindString(0, form
.signon_realm
);
438 s
.BindString(1, regexp
);
440 psl_domain_match_metric
= PSLMatchingHelper::PSL_DOMAIN_MATCH_DISABLED
;
441 s
.Assign(db_
.GetCachedStatement(SQL_FROM_HERE
, sql_query
.c_str()));
442 s
.BindString(0, form
.signon_realm
);
446 scoped_ptr
<PasswordForm
> new_form(new PasswordForm());
447 EncryptionResult result
= InitPasswordFormFromStatement(new_form
.get(), s
);
448 if (result
== ENCRYPTION_RESULT_SERVICE_FAILURE
)
450 if (result
== ENCRYPTION_RESULT_ITEM_FAILURE
)
452 DCHECK(result
== ENCRYPTION_RESULT_SUCCESS
);
453 if (psl_helper_
.IsMatchingEnabled()) {
454 if (!PSLMatchingHelper::IsPublicSuffixDomainMatch(new_form
->signon_realm
,
455 form
.signon_realm
)) {
456 // The database returned results that should not match. Skipping result.
459 if (form
.signon_realm
!= new_form
->signon_realm
) {
460 psl_domain_match_metric
= PSLMatchingHelper::PSL_DOMAIN_MATCH_FOUND
;
461 // This is not a perfect match, so we need to create a new valid result.
462 // We do this by copying over origin, signon realm and action from the
463 // observed form and setting the original signon realm to what we found
464 // in the database. We use the fact that |original_signon_realm| is
465 // non-empty to communicate that this match was found using public
467 new_form
->original_signon_realm
= new_form
->signon_realm
;
468 new_form
->origin
= form
.origin
;
469 new_form
->signon_realm
= form
.signon_realm
;
470 new_form
->action
= form
.action
;
473 forms
->push_back(new_form
.release());
475 UMA_HISTOGRAM_ENUMERATION("PasswordManager.PslDomainMatchTriggering",
476 psl_domain_match_metric
,
477 PSLMatchingHelper::PSL_DOMAIN_MATCH_COUNT
);
478 return s
.Succeeded();
481 bool LoginDatabase::GetLoginsCreatedBetween(
482 const base::Time begin
,
483 const base::Time end
,
484 std::vector
<autofill::PasswordForm
*>* forms
) const {
486 sql::Statement
s(db_
.GetCachedStatement(SQL_FROM_HERE
,
487 "SELECT origin_url, action_url, "
488 "username_element, username_value, "
489 "password_element, password_value, submit_element, "
490 "signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
491 "scheme, password_type, possible_usernames, times_used, form_data "
492 "FROM logins WHERE date_created >= ? AND date_created < ?"
493 "ORDER BY origin_url"));
494 s
.BindInt64(0, begin
.ToTimeT());
495 s
.BindInt64(1, end
.is_null() ? std::numeric_limits
<int64
>::max()
499 scoped_ptr
<PasswordForm
> new_form(new PasswordForm());
500 EncryptionResult result
= InitPasswordFormFromStatement(new_form
.get(), s
);
501 if (result
== ENCRYPTION_RESULT_SERVICE_FAILURE
)
503 if (result
== ENCRYPTION_RESULT_ITEM_FAILURE
)
505 DCHECK(result
== ENCRYPTION_RESULT_SUCCESS
);
506 forms
->push_back(new_form
.release());
508 return s
.Succeeded();
511 bool LoginDatabase::GetAutofillableLogins(
512 std::vector
<PasswordForm
*>* forms
) const {
513 return GetAllLoginsWithBlacklistSetting(false, forms
);
516 bool LoginDatabase::GetBlacklistLogins(
517 std::vector
<PasswordForm
*>* forms
) const {
518 return GetAllLoginsWithBlacklistSetting(true, forms
);
521 bool LoginDatabase::GetAllLoginsWithBlacklistSetting(
522 bool blacklisted
, std::vector
<PasswordForm
*>* forms
) const {
524 // You *must* change LoginTableColumns if this query changes.
525 sql::Statement
s(db_
.GetCachedStatement(SQL_FROM_HERE
,
526 "SELECT origin_url, action_url, "
527 "username_element, username_value, "
528 "password_element, password_value, submit_element, "
529 "signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
530 "scheme, password_type, possible_usernames, times_used, form_data "
531 "FROM logins WHERE blacklisted_by_user == ? "
532 "ORDER BY origin_url"));
533 s
.BindInt(0, blacklisted
? 1 : 0);
536 scoped_ptr
<PasswordForm
> new_form(new PasswordForm());
537 EncryptionResult result
= InitPasswordFormFromStatement(new_form
.get(), s
);
538 if (result
== ENCRYPTION_RESULT_SERVICE_FAILURE
)
540 if (result
== ENCRYPTION_RESULT_ITEM_FAILURE
)
542 DCHECK(result
== ENCRYPTION_RESULT_SUCCESS
);
543 forms
->push_back(new_form
.release());
545 return s
.Succeeded();
548 bool LoginDatabase::DeleteAndRecreateDatabaseFile() {
549 DCHECK(db_
.is_open());
552 sql::Connection::Delete(db_path_
);
553 return Init(db_path_
);
556 Pickle
LoginDatabase::SerializeVector(
557 const std::vector
<base::string16
>& vec
) const {
559 for (size_t i
= 0; i
< vec
.size(); ++i
) {
560 p
.WriteString16(vec
[i
]);
565 std::vector
<base::string16
> LoginDatabase::DeserializeVector(
566 const Pickle
& p
) const {
567 std::vector
<base::string16
> ret
;
570 PickleIterator
iterator(p
);
571 while (iterator
.ReadString16(&str
)) {