| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/password_manager/core/browser/login_database.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| #include <algorithm> |
| #include <limits> |
| #include <map> |
| #include <memory> |
| #include <set> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/check_op.h" |
| #include "base/feature_list.h" |
| #include "base/files/file_path.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/pickle.h" |
| #include "base/stl_util.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/trace_event/trace_event.h" |
| #include "build/build_config.h" |
| #include "components/os_crypt/os_crypt.h" |
| #include "components/password_manager/core/browser/android_affiliation/affiliation_utils.h" |
| #include "components/password_manager/core/browser/password_bubble_experiment.h" |
| #include "components/password_manager/core/browser/password_form.h" |
| #include "components/password_manager/core/browser/password_manager_client.h" |
| #include "components/password_manager/core/browser/password_manager_metrics_util.h" |
| #include "components/password_manager/core/browser/password_manager_util.h" |
| #include "components/password_manager/core/browser/psl_matching_helper.h" |
| #include "components/password_manager/core/browser/sql_table_builder.h" |
| #include "components/password_manager/core/common/password_manager_features.h" |
| #include "components/safe_browsing/core/features.h" |
| #include "components/sync/protocol/entity_metadata.pb.h" |
| #include "components/sync/protocol/model_type_state.pb.h" |
| #include "google_apis/gaia/gaia_auth_util.h" |
| #include "google_apis/gaia/gaia_urls.h" |
| #include "sql/database.h" |
| #include "sql/statement.h" |
| #include "sql/transaction.h" |
| #include "third_party/re2/src/re2/re2.h" |
| #include "url/origin.h" |
| #include "url/url_constants.h" |
| |
| using autofill::GaiaIdHash; |
| |
| namespace password_manager { |
| |
| // The current version number of the login database schema. |
| const int kCurrentVersionNumber = 28; |
| // The oldest version of the schema such that a legacy Chrome client using that |
| // version can still read/write the current database. |
| const int kCompatibleVersionNumber = 28; |
| |
| base::Pickle SerializeValueElementPairs(const ValueElementVector& vec) { |
| base::Pickle p; |
| for (const auto& pair : vec) { |
| p.WriteString16(pair.first); |
| p.WriteString16(pair.second); |
| } |
| return p; |
| } |
| |
| ValueElementVector DeserializeValueElementPairs(const base::Pickle& p) { |
| ValueElementVector ret; |
| base::string16 value; |
| base::string16 field_name; |
| |
| base::PickleIterator iterator(p); |
| while (iterator.ReadString16(&value)) { |
| bool name_success = iterator.ReadString16(&field_name); |
| DCHECK(name_success); |
| ret.push_back(ValueElementPair(value, field_name)); |
| } |
| return ret; |
| } |
| |
| base::Pickle SerializeGaiaIdHashVector(const std::vector<GaiaIdHash>& hashes) { |
| base::Pickle p; |
| for (const auto& hash : hashes) |
| p.WriteString(hash.ToBinary()); |
| return p; |
| } |
| |
| std::vector<GaiaIdHash> DeserializeGaiaIdHashVector(const base::Pickle& p) { |
| std::vector<GaiaIdHash> hashes; |
| std::string hash; |
| |
| base::PickleIterator iterator(p); |
| while (iterator.ReadString(&hash)) |
| hashes.push_back(GaiaIdHash::FromBinary(hash)); |
| return hashes; |
| } |
| |
| namespace { |
| |
| // Common prefix for all histograms. |
| constexpr char kPasswordManager[] = "PasswordManager"; |
| |
| // Need to stay in sync with the PasswordGenerated suffix in histograms.xml. |
| constexpr char kAutoGeneratedSuffix[] = ".AutoGenerated"; |
| constexpr char kUserCreatedSuffix[] = ".UserCreated"; |
| constexpr char kOverallSuffix[] = ".Overall"; |
| |
| // Need to stay in sync with the PasswordCustomPassphrase suffix in |
| // histograms.xml. |
| constexpr char kWithCustomPassphraseSuffix[] = ".WithCustomPassphrase"; |
| constexpr char kWithoutCustomPassphraseSuffix[] = ".WithoutCustomPassphrase"; |
| |
| base::StringPiece GetCustomPassphraseSuffix( |
| bool custom_passphrase_sync_enabled) { |
| return custom_passphrase_sync_enabled ? kWithCustomPassphraseSuffix |
| : kWithoutCustomPassphraseSuffix; |
| } |
| |
| // A simple class for scoping a login database transaction. This does not |
| // support rollback since the login database doesn't either. |
| class ScopedTransaction { |
| public: |
| explicit ScopedTransaction(LoginDatabase* db) : db_(db) { |
| db_->BeginTransaction(); |
| } |
| ~ScopedTransaction() { db_->CommitTransaction(); } |
| |
| private: |
| LoginDatabase* db_; |
| DISALLOW_COPY_AND_ASSIGN(ScopedTransaction); |
| }; |
| |
| // Convenience enum for interacting with SQL queries that use all the columns. |
| enum LoginDatabaseTableColumns { |
| COLUMN_ORIGIN_URL = 0, |
| COLUMN_ACTION_URL, |
| COLUMN_USERNAME_ELEMENT, |
| COLUMN_USERNAME_VALUE, |
| COLUMN_PASSWORD_ELEMENT, |
| COLUMN_PASSWORD_VALUE, |
| COLUMN_SUBMIT_ELEMENT, |
| COLUMN_SIGNON_REALM, |
| COLUMN_DATE_CREATED, |
| COLUMN_BLACKLISTED_BY_USER, |
| COLUMN_SCHEME, |
| COLUMN_PASSWORD_TYPE, |
| COLUMN_TIMES_USED, |
| COLUMN_FORM_DATA, |
| COLUMN_DATE_SYNCED, |
| COLUMN_DISPLAY_NAME, |
| COLUMN_ICON_URL, |
| COLUMN_FEDERATION_URL, |
| COLUMN_SKIP_ZERO_CLICK, |
| COLUMN_GENERATION_UPLOAD_STATUS, |
| COLUMN_POSSIBLE_USERNAME_PAIRS, |
| COLUMN_ID, |
| COLUMN_DATE_LAST_USED, |
| COLUMN_MOVING_BLOCKED_FOR, |
| COLUMN_NUM // Keep this last. |
| }; |
| |
| enum class HistogramSize { SMALL, LARGE }; |
| |
| // An enum for UMA reporting. Add values to the end only. |
| enum DatabaseInitError { |
| INIT_OK = 0, |
| OPEN_FILE_ERROR = 1, |
| START_TRANSACTION_ERROR = 2, |
| META_TABLE_INIT_ERROR = 3, |
| INCOMPATIBLE_VERSION = 4, |
| INIT_LOGINS_ERROR = 5, |
| INIT_STATS_ERROR = 6, |
| MIGRATION_ERROR = 7, |
| COMMIT_TRANSACTION_ERROR = 8, |
| INIT_COMPROMISED_CREDENTIALS_ERROR = 9, |
| INIT_FIELD_INFO_ERROR = 10, |
| FOREIGN_KEY_ERROR = 11, |
| |
| DATABASE_INIT_ERROR_COUNT, |
| }; |
| |
| // Struct to hold table builder for "logins", "sync_entities_metadata", and |
| // "sync_model_metadata" tables. |
| struct SQLTableBuilders { |
| SQLTableBuilder* logins; |
| SQLTableBuilder* sync_entities_metadata; |
| SQLTableBuilder* sync_model_metadata; |
| }; |
| |
| void BindAddStatement(const PasswordForm& form, sql::Statement* s) { |
| s->BindString(COLUMN_ORIGIN_URL, form.url.spec()); |
| s->BindString(COLUMN_ACTION_URL, form.action.spec()); |
| s->BindString16(COLUMN_USERNAME_ELEMENT, form.username_element); |
| s->BindString16(COLUMN_USERNAME_VALUE, form.username_value); |
| s->BindString16(COLUMN_PASSWORD_ELEMENT, form.password_element); |
| s->BindBlob(COLUMN_PASSWORD_VALUE, form.encrypted_password.data(), |
| static_cast<int>(form.encrypted_password.length())); |
| s->BindString16(COLUMN_SUBMIT_ELEMENT, form.submit_element); |
| s->BindString(COLUMN_SIGNON_REALM, form.signon_realm); |
| s->BindInt64(COLUMN_DATE_CREATED, form.date_created.ToInternalValue()); |
| s->BindInt(COLUMN_BLACKLISTED_BY_USER, form.blocked_by_user); |
| s->BindInt(COLUMN_SCHEME, static_cast<int>(form.scheme)); |
| s->BindInt(COLUMN_PASSWORD_TYPE, static_cast<int>(form.type)); |
| s->BindInt(COLUMN_TIMES_USED, form.times_used); |
| base::Pickle form_data_pickle; |
| autofill::SerializeFormData(form.form_data, &form_data_pickle); |
| s->BindBlob(COLUMN_FORM_DATA, form_data_pickle.data(), |
| form_data_pickle.size()); |
| s->BindInt64(COLUMN_DATE_SYNCED, form.date_synced.ToInternalValue()); |
| s->BindString16(COLUMN_DISPLAY_NAME, form.display_name); |
| s->BindString(COLUMN_ICON_URL, form.icon_url.spec()); |
| // An empty Origin serializes as "null" which would be strange to store here. |
| s->BindString(COLUMN_FEDERATION_URL, |
| form.federation_origin.opaque() |
| ? std::string() |
| : form.federation_origin.Serialize()); |
| s->BindInt(COLUMN_SKIP_ZERO_CLICK, form.skip_zero_click); |
| s->BindInt(COLUMN_GENERATION_UPLOAD_STATUS, |
| static_cast<int>(form.generation_upload_status)); |
| base::Pickle usernames_pickle = |
| SerializeValueElementPairs(form.all_possible_usernames); |
| s->BindBlob(COLUMN_POSSIBLE_USERNAME_PAIRS, usernames_pickle.data(), |
| usernames_pickle.size()); |
| s->BindInt64(COLUMN_DATE_LAST_USED, |
| form.date_last_used.ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| base::Pickle moving_blocked_for_pickle = |
| SerializeGaiaIdHashVector(form.moving_blocked_for_list); |
| s->BindBlob(COLUMN_MOVING_BLOCKED_FOR, moving_blocked_for_pickle.data(), |
| moving_blocked_for_pickle.size()); |
| } |
| |
| // Output parameter is the first one because of binding order. |
| void AddCallback(int* output_err, int err, sql::Statement* /*stmt*/) { |
| DCHECK(output_err); |
| *output_err = err; |
| if (err == 19 /*SQLITE_CONSTRAINT*/) |
| DLOG(WARNING) << "LoginDatabase::AddLogin updated an existing form"; |
| } |
| |
| bool DoesMatchConstraints(const PasswordForm& form) { |
| if (!IsValidAndroidFacetURI(form.signon_realm) && form.url.is_empty()) { |
| DLOG(ERROR) << "Constraint violation: form.origin is empty"; |
| return false; |
| } |
| if (form.signon_realm.empty()) { |
| DLOG(ERROR) << "Constraint violation: form.signon_realm is empty"; |
| return false; |
| } |
| return true; |
| } |
| |
| void LogDatabaseInitError(DatabaseInitError error) { |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.LoginDatabaseInit", error, |
| DATABASE_INIT_ERROR_COUNT); |
| } |
| |
| void LogAccountStat(const std::string& name, int sample) { |
| base::UmaHistogramCustomCounts(name, sample, 0, 32, 6); |
| } |
| |
| void LogAccountStatHiRes(const std::string& name, int sample) { |
| base::UmaHistogramCustomCounts(name, sample, 0, 1000, 100); |
| } |
| |
| void LogTimesUsedStat(const std::string& name, int sample) { |
| base::UmaHistogramCustomCounts(name, sample, 0, 100, 10); |
| } |
| |
| void LogNumberOfAccountsForScheme(base::StringPiece suffix_for_store, |
| const std::string& scheme, |
| int sample) { |
| base::UmaHistogramCustomCounts( |
| base::StrCat({kPasswordManager, suffix_for_store, |
| ".TotalAccountsHiRes.WithScheme.", scheme}), |
| sample, 1, 1000, 100); |
| } |
| |
| bool ClearAllSyncMetadata(sql::Database* db) { |
| sql::Statement s1( |
| db->GetCachedStatement(SQL_FROM_HERE, "DELETE FROM sync_model_metadata")); |
| |
| sql::Statement s2(db->GetCachedStatement( |
| SQL_FROM_HERE, "DELETE FROM sync_entities_metadata")); |
| |
| return s1.Run() && s2.Run(); |
| } |
| |
| // Seals the version of the given builders. This is method should be always used |
| // to seal versions of all builder to make sure all builders are at the same |
| // version. |
| void SealVersion(SQLTableBuilders builders, unsigned expected_version) { |
| unsigned logins_version = builders.logins->SealVersion(); |
| DCHECK_EQ(expected_version, logins_version); |
| |
| unsigned sync_entities_metadata_version = |
| builders.sync_entities_metadata->SealVersion(); |
| DCHECK_EQ(expected_version, sync_entities_metadata_version); |
| |
| unsigned sync_model_metadata_version = |
| builders.sync_model_metadata->SealVersion(); |
| DCHECK_EQ(expected_version, sync_model_metadata_version); |
| } |
| |
| // Teaches |builders| about the different DB schemes in different versions. |
| void InitializeBuilders(SQLTableBuilders builders) { |
| // Versions 0 and 1, which are the same. |
| builders.logins->AddColumnToUniqueKey("origin_url", "VARCHAR NOT NULL"); |
| builders.logins->AddColumn("action_url", "VARCHAR"); |
| builders.logins->AddColumnToUniqueKey("username_element", "VARCHAR"); |
| builders.logins->AddColumnToUniqueKey("username_value", "VARCHAR"); |
| builders.logins->AddColumnToUniqueKey("password_element", "VARCHAR"); |
| builders.logins->AddColumn("password_value", "BLOB"); |
| builders.logins->AddColumn("submit_element", "VARCHAR"); |
| builders.logins->AddColumnToUniqueKey("signon_realm", "VARCHAR NOT NULL"); |
| builders.logins->AddColumn("ssl_valid", "INTEGER NOT NULL"); |
| builders.logins->AddColumn("preferred", "INTEGER NOT NULL"); |
| builders.logins->AddColumn("date_created", "INTEGER NOT NULL"); |
| builders.logins->AddColumn("blacklisted_by_user", "INTEGER NOT NULL"); |
| builders.logins->AddColumn("scheme", "INTEGER NOT NULL"); |
| builders.logins->AddIndex("logins_signon", {"signon_realm"}); |
| SealVersion(builders, /*expected_version=*/0u); |
| SealVersion(builders, /*expected_version=*/1u); |
| |
| // Version 2. |
| builders.logins->AddColumn("password_type", "INTEGER"); |
| builders.logins->AddColumn("possible_usernames", "BLOB"); |
| SealVersion(builders, /*expected_version=*/2u); |
| |
| // Version 3. |
| builders.logins->AddColumn("times_used", "INTEGER"); |
| SealVersion(builders, /*expected_version=*/3u); |
| |
| // Version 4. |
| builders.logins->AddColumn("form_data", "BLOB"); |
| SealVersion(builders, /*expected_version=*/4u); |
| |
| // Version 5. |
| builders.logins->AddColumn("use_additional_auth", "INTEGER"); |
| SealVersion(builders, /*expected_version=*/5u); |
| |
| // Version 6. |
| builders.logins->AddColumn("date_synced", "INTEGER"); |
| SealVersion(builders, /*expected_version=*/6u); |
| |
| // Version 7. |
| builders.logins->AddColumn("display_name", "VARCHAR"); |
| builders.logins->AddColumn("avatar_url", "VARCHAR"); |
| builders.logins->AddColumn("federation_url", "VARCHAR"); |
| builders.logins->AddColumn("is_zero_click", "INTEGER"); |
| SealVersion(builders, /*expected_version=*/7u); |
| |
| // Version 8. |
| SealVersion(builders, /*expected_version=*/8u); |
| // Version 9. |
| SealVersion(builders, /*expected_version=*/9u); |
| // Version 10. |
| builders.logins->DropColumn("use_additional_auth"); |
| SealVersion(builders, /*expected_version=*/10u); |
| |
| // Version 11. |
| builders.logins->RenameColumn("is_zero_click", "skip_zero_click"); |
| SealVersion(builders, /*expected_version=*/11u); |
| |
| // Version 12. |
| builders.logins->AddColumn("generation_upload_status", "INTEGER"); |
| SealVersion(builders, /*expected_version=*/12u); |
| |
| // Version 13. |
| SealVersion(builders, /*expected_version=*/13u); |
| // Version 14. |
| builders.logins->RenameColumn("avatar_url", "icon_url"); |
| SealVersion(builders, /*expected_version=*/14u); |
| |
| // Version 15. |
| SealVersion(builders, /*expected_version=*/15u); |
| // Version 16. |
| SealVersion(builders, /*expected_version=*/16u); |
| // Version 17. |
| SealVersion(builders, /*expected_version=*/17u); |
| |
| // Version 18. |
| builders.logins->DropColumn("ssl_valid"); |
| SealVersion(builders, /*expected_version=*/18u); |
| |
| // Version 19. |
| builders.logins->DropColumn("possible_usernames"); |
| builders.logins->AddColumn("possible_username_pairs", "BLOB"); |
| SealVersion(builders, /*expected_version=*/19u); |
| |
| // Version 20. |
| builders.logins->AddPrimaryKeyColumn("id"); |
| SealVersion(builders, /*expected_version=*/20u); |
| |
| // Version 21. |
| builders.sync_entities_metadata->AddPrimaryKeyColumn("storage_key"); |
| builders.sync_entities_metadata->AddColumn("metadata", "VARCHAR NOT NULL"); |
| builders.sync_model_metadata->AddPrimaryKeyColumn("id"); |
| builders.sync_model_metadata->AddColumn("model_metadata", "VARCHAR NOT NULL"); |
| SealVersion(builders, /*expected_version=*/21u); |
| |
| // Version 22. Changes in Sync metadata encryption. |
| SealVersion(builders, /*expected_version=*/22u); |
| |
| // Version 23. Version 22 could have some corruption in Sync metadata and |
| // hence we are migrating users on it by clearing their metadata to make Sync |
| // start clean from scratch. |
| SealVersion(builders, /*expected_version=*/23u); |
| |
| // Version 24. Version 23 could have some corruption in Sync metadata and |
| // hence we are migrating users on it by clearing their metadata to make Sync |
| // start clean from scratch. |
| SealVersion(builders, /*expected_version=*/24u); |
| |
| // Version 25. Introduce date_last_used column to replace the preferred |
| // column. MigrateLogins() will take care of migrating the data. |
| builders.logins->AddColumn("date_last_used", "INTEGER NOT NULL DEFAULT 0"); |
| SealVersion(builders, /*expected_version=*/25u); |
| |
| // Version 26 is the first version where the id is AUTOINCREMENT. |
| SealVersion(builders, /*expected_version=*/26u); |
| |
| // Version 27. Add the moving_blocked_for column to contain serialized list of |
| // gaia id hashes for users that prefer not to move this credential to their |
| // account store. |
| builders.logins->AddColumn("moving_blocked_for", "BLOB"); |
| SealVersion(builders, /*expected_version=*/27u); |
| |
| // Version 28. |
| builders.logins->DropColumn("preferred"); |
| SealVersion(builders, /*expected_version=*/28u); |
| |
| DCHECK_EQ(static_cast<size_t>(COLUMN_NUM), builders.logins->NumberOfColumns()) |
| << "Adjust LoginDatabaseTableColumns if you change column definitions " |
| "here."; |
| } |
| |
| // Callback called upon each migration step of the logins table. It's used to |
| // inject custom schema migration logic not covered by the generic |
| // SQLTableBuilder migration. |new_version| indicates how far |
| // SQLTableBuilder is in the migration process. |
| bool LoginsTablePostMigrationStepCallback(sql::Database* db, |
| unsigned new_version) { |
| // In version 26, the primary key of the logins table became an |
| // AUTOINCREMENT field. Since SQLite doesn't allow changing the column type, |
| // the only way is to actually create a temp table with the primary key |
| // properly set as an AUTOINCREMENT field, and move the data there. The code |
| // has been adjusted such that newly created tables have the primary key |
| // properly set as AUTOINCREMENT. |
| if (new_version == 26) { |
| // This statement creates the logins database similar to version 26 with |
| // the primary key column set to AUTOINCREMENT. |
| const char temp_table_create_statement_version_26[] = |
| "CREATE TABLE logins_temp (origin_url VARCHAR NOT NULL,action_url " |
| "VARCHAR,username_element VARCHAR,username_value " |
| "VARCHAR,password_element VARCHAR,password_value BLOB,submit_element " |
| "VARCHAR,signon_realm VARCHAR NOT NULL,preferred INTEGER NOT " |
| "NULL,date_created INTEGER NOT NULL,blacklisted_by_user INTEGER NOT " |
| "NULL,scheme INTEGER NOT NULL,password_type INTEGER,times_used " |
| "INTEGER,form_data BLOB,date_synced INTEGER,display_name " |
| "VARCHAR,icon_url VARCHAR,federation_url VARCHAR,skip_zero_click " |
| "INTEGER,generation_upload_status INTEGER,possible_username_pairs " |
| "BLOB,id INTEGER PRIMARY KEY AUTOINCREMENT,date_last_used " |
| "INTEGER,UNIQUE (origin_url, username_element, username_value, " |
| "password_element, signon_realm))"; |
| const char move_data_statement[] = |
| "INSERT INTO logins_temp SELECT * from logins"; |
| const char drop_table_statement[] = "DROP TABLE logins"; |
| const char rename_table_statement[] = |
| "ALTER TABLE logins_temp RENAME TO logins"; |
| |
| sql::Transaction transaction(db); |
| if (!(transaction.Begin() && |
| db->Execute(temp_table_create_statement_version_26) && |
| db->Execute(move_data_statement) && |
| db->Execute(drop_table_statement) && |
| db->Execute(rename_table_statement) && transaction.Commit())) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Call this after having called InitializeBuilders(), to migrate the database |
| // from the current version to kCurrentVersionNumber. |
| bool MigrateLogins(unsigned current_version, |
| SQLTableBuilders builders, |
| sql::Database* db) { |
| if (!builders.logins->MigrateFrom( |
| current_version, db, |
| base::BindRepeating(&LoginsTablePostMigrationStepCallback))) |
| return false; |
| |
| if (!builders.sync_entities_metadata->MigrateFrom(current_version, db)) |
| return false; |
| |
| if (!builders.sync_model_metadata->MigrateFrom(current_version, db)) |
| return false; |
| |
| // Data changes, not covered by the schema migration above. |
| if (current_version <= 8) { |
| sql::Statement fix_time_format; |
| fix_time_format.Assign(db->GetCachedStatement( |
| SQL_FROM_HERE, |
| "UPDATE logins SET date_created = (date_created * ?) + ?")); |
| fix_time_format.BindInt64(0, base::Time::kMicrosecondsPerSecond); |
| fix_time_format.BindInt64(1, base::Time::kTimeTToMicrosecondsOffset); |
| if (!fix_time_format.Run()) |
| return false; |
| } |
| |
| if (current_version <= 16) { |
| sql::Statement reset_zero_click; |
| reset_zero_click.Assign(db->GetCachedStatement( |
| SQL_FROM_HERE, "UPDATE logins SET skip_zero_click = 1")); |
| if (!reset_zero_click.Run()) |
| return false; |
| } |
| |
| // Sync Metadata tables have been introduced in version 21. It is enough to |
| // drop all data because Sync would populate the tables properly at startup. |
| if (current_version >= 21 && current_version < 26) { |
| if (!ClearAllSyncMetadata(db)) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Because of https://crbug.com/295851, some early version numbers might be |
| // wrong. This function detects that and fixes the version. |
| bool FixVersionIfNeeded(sql::Database* db, int* current_version) { |
| if (*current_version == 1) { |
| int extra_columns = 0; |
| if (db->DoesColumnExist("logins", "password_type")) |
| ++extra_columns; |
| if (db->DoesColumnExist("logins", "possible_usernames")) |
| ++extra_columns; |
| if (extra_columns == 2) { |
| *current_version = 2; |
| } else if (extra_columns == 1) { |
| // If this is https://crbug.com/295851 then either both columns exist |
| // or none. |
| return false; |
| } |
| } |
| if (*current_version == 2) { |
| if (db->DoesColumnExist("logins", "times_used")) |
| *current_version = 3; |
| } |
| if (*current_version == 3) { |
| if (db->DoesColumnExist("logins", "form_data")) |
| *current_version = 4; |
| } |
| // "date_last_used" columns has been introduced in version 25. if it exists, |
| // the version should be at least 25. This has been added to address this bug |
| // (crbug.com/1020320). |
| if (*current_version < 25) { |
| if (db->DoesColumnExist("logins", "date_last_used")) |
| *current_version = 25; |
| } |
| return true; |
| } |
| |
| // Generates the string "(?,?,...,?)" with |count| repetitions of "?". |
| std::string GeneratePlaceholders(size_t count) { |
| std::string result(2 * count + 1, ','); |
| result.front() = '('; |
| result.back() = ')'; |
| for (size_t i = 1; i < 2 * count + 1; i += 2) { |
| result[i] = '?'; |
| } |
| return result; |
| } |
| |
| #if defined(OS_MAC) |
| // Fills |form| with necessary data required to be removed from the database |
| // and returns it. |
| PasswordForm GetFormForRemoval(const sql::Statement& statement) { |
| PasswordForm form; |
| form.url = GURL(statement.ColumnString(COLUMN_ORIGIN_URL)); |
| form.username_element = statement.ColumnString16(COLUMN_USERNAME_ELEMENT); |
| form.username_value = statement.ColumnString16(COLUMN_USERNAME_VALUE); |
| form.password_element = statement.ColumnString16(COLUMN_PASSWORD_ELEMENT); |
| form.signon_realm = statement.ColumnString(COLUMN_SIGNON_REALM); |
| return form; |
| } |
| #endif |
| |
| } // namespace |
| |
| struct LoginDatabase::PrimaryKeyAndPassword { |
| int primary_key; |
| std::string encrypted_password; |
| base::string16 decrypted_password; |
| }; |
| |
| LoginDatabase::LoginDatabase(const base::FilePath& db_path, |
| IsAccountStore is_account_store) |
| : db_path_(db_path), |
| is_account_store_(is_account_store), |
| // Set options for a small, private database (based on WebDatabase). |
| db_({.exclusive_locking = true, .page_size = 2048, .cache_size = 32}) {} |
| |
| LoginDatabase::~LoginDatabase() = default; |
| |
| bool LoginDatabase::Init() { |
| TRACE_EVENT0("passwords", "LoginDatabase::Init"); |
| db_.set_histogram_tag("Passwords"); |
| |
| if (!db_.Open(db_path_)) { |
| LogDatabaseInitError(OPEN_FILE_ERROR); |
| LOG(ERROR) << "Unable to open the password store database."; |
| return false; |
| } |
| |
| if (!db_.Execute("PRAGMA foreign_keys = ON")) { |
| LogDatabaseInitError(FOREIGN_KEY_ERROR); |
| LOG(ERROR) << "Unable to activate foreign keys."; |
| return false; |
| } |
| |
| sql::Transaction transaction(&db_); |
| if (!transaction.Begin()) { |
| LogDatabaseInitError(START_TRANSACTION_ERROR); |
| LOG(ERROR) << "Unable to start a transaction."; |
| db_.Close(); |
| return false; |
| } |
| |
| // Check the database version. |
| if (!meta_table_.Init(&db_, kCurrentVersionNumber, |
| kCompatibleVersionNumber)) { |
| LogDatabaseInitError(META_TABLE_INIT_ERROR); |
| LOG(ERROR) << "Unable to create the meta table."; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) { |
| LogDatabaseInitError(INCOMPATIBLE_VERSION); |
| LOG(ERROR) << "Password store database is too new, kCurrentVersionNumber=" |
| << kCurrentVersionNumber << ", GetCompatibleVersionNumber=" |
| << meta_table_.GetCompatibleVersionNumber(); |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| |
| SQLTableBuilder logins_builder("logins"); |
| SQLTableBuilder sync_entities_metadata_builder("sync_entities_metadata"); |
| SQLTableBuilder sync_model_metadata_builder("sync_model_metadata"); |
| SQLTableBuilders builders = {&logins_builder, &sync_entities_metadata_builder, |
| &sync_model_metadata_builder}; |
| InitializeBuilders(builders); |
| InitializeStatementStrings(logins_builder); |
| |
| if (!db_.DoesTableExist("logins")) { |
| if (!logins_builder.CreateTable(&db_)) { |
| VLOG(0) << "Failed to create the 'logins' table"; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| } |
| |
| if (!db_.DoesTableExist("sync_entities_metadata")) { |
| if (!sync_entities_metadata_builder.CreateTable(&db_)) { |
| VLOG(0) << "Failed to create the 'sync_entities_metadata' table"; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| } |
| |
| if (!db_.DoesTableExist("sync_model_metadata")) { |
| if (!sync_model_metadata_builder.CreateTable(&db_)) { |
| VLOG(0) << "Failed to create the 'sync_model_metadata' table"; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| } |
| |
| stats_table_.Init(&db_); |
| compromised_credentials_table_.Init(&db_); |
| field_info_table_.Init(&db_); |
| |
| int current_version = meta_table_.GetVersionNumber(); |
| bool migration_success = FixVersionIfNeeded(&db_, ¤t_version); |
| |
| // If the file on disk is an older database version, bring it up to date. |
| if (migration_success && current_version < kCurrentVersionNumber) { |
| migration_success = MigrateLogins( |
| base::checked_cast<unsigned>(current_version), builders, &db_); |
| } |
| if (migration_success && current_version <= 15) { |
| migration_success = stats_table_.MigrateToVersion(16); |
| } |
| if (migration_success) { |
| // |migration_success| could be true when no logins have been actually |
| // migrated. We should protect against downgrading the database version in |
| // such case. Update the database version only if a migration took place. |
| if (current_version < kCurrentVersionNumber) { |
| meta_table_.SetCompatibleVersionNumber(kCompatibleVersionNumber); |
| meta_table_.SetVersionNumber(kCurrentVersionNumber); |
| } |
| } else { |
| LogDatabaseInitError(MIGRATION_ERROR); |
| LOG(ERROR) << "Unable to migrate database from " |
| << meta_table_.GetVersionNumber() << " to " |
| << kCurrentVersionNumber; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| |
| if (!stats_table_.CreateTableIfNecessary()) { |
| LogDatabaseInitError(INIT_STATS_ERROR); |
| LOG(ERROR) << "Unable to create the stats table."; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| |
| // The table "leaked_credentials" was previously created without a flag. |
| // The table is now renamed to "compromised_credentials" and also includes |
| // a new column so the old table needs to be deleted. |
| if (db_.DoesTableExist("leaked_credentials")) { |
| if (!db_.Execute("DROP TABLE leaked_credentials")) { |
| LOG(ERROR) << "Unable to create the stats table."; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| } |
| |
| if (base::FeatureList::IsEnabled( |
| password_manager::features::kPasswordCheck) || |
| base::FeatureList::IsEnabled( |
| safe_browsing::kPasswordProtectionShowDomainsForSavedPasswords)) { |
| if (!compromised_credentials_table_.CreateTableIfNecessary()) { |
| LogDatabaseInitError(INIT_COMPROMISED_CREDENTIALS_ERROR); |
| LOG(ERROR) << "Unable to create the compromised credentials table."; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| } |
| |
| if (!field_info_table_.CreateTableIfNecessary()) { |
| LogDatabaseInitError(INIT_FIELD_INFO_ERROR); |
| LOG(ERROR) << "Unable to create the field info table."; |
| transaction.Rollback(); |
| db_.Close(); |
| return false; |
| } |
| |
| if (!transaction.Commit()) { |
| LogDatabaseInitError(COMMIT_TRANSACTION_ERROR); |
| LOG(ERROR) << "Unable to commit a transaction."; |
| db_.Close(); |
| return false; |
| } |
| |
| LogDatabaseInitError(INIT_OK); |
| return true; |
| } |
| |
| #if defined(OS_MAC) |
| void LoginDatabase::InitPasswordRecoveryUtil( |
| std::unique_ptr<PasswordRecoveryUtilMac> password_recovery_util) { |
| password_recovery_util_ = std::move(password_recovery_util); |
| } |
| #endif |
| |
| base::StringPiece LoginDatabase::GetMetricsSuffixForStore() const { |
| // Note: For historic reasons, the profile store does not use a suffix, only |
| // the account store does. |
| return is_account_store_.value() ? ".AccountStore" : ""; |
| } |
| |
| void LoginDatabase::ReportNumberOfAccountsMetrics( |
| bool custom_passphrase_sync_enabled) { |
| sql::Statement s(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| "SELECT signon_realm, password_type, blacklisted_by_user," |
| "COUNT(username_value) FROM logins GROUP BY " |
| "signon_realm, password_type, blacklisted_by_user")); |
| |
| if (!s.is_valid()) |
| return; |
| |
| base::StringPiece store_suffix = GetMetricsSuffixForStore(); |
| base::StringPiece custom_passphrase_suffix = |
| GetCustomPassphraseSuffix(custom_passphrase_sync_enabled); |
| |
| int total_user_created_accounts = 0; |
| int total_generated_accounts = 0; |
| int blacklisted_sites = 0; |
| while (s.Step()) { |
| auto password_type = static_cast<PasswordForm::Type>(s.ColumnInt(1)); |
| int blacklisted = s.ColumnInt(2); |
| int accounts_per_site = s.ColumnInt(3); |
| if (blacklisted) { |
| ++blacklisted_sites; |
| continue; |
| } |
| |
| constexpr base::StringPiece kAccountsPerSiteSuffix = |
| ".AccountsPerSiteHiRes"; |
| |
| if (password_type == PasswordForm::Type::kGenerated) { |
| total_generated_accounts += accounts_per_site; |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, kAccountsPerSiteSuffix, |
| kAutoGeneratedSuffix, custom_passphrase_suffix}), |
| accounts_per_site); |
| } else { |
| total_user_created_accounts += accounts_per_site; |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, kAccountsPerSiteSuffix, |
| kUserCreatedSuffix, custom_passphrase_suffix}), |
| accounts_per_site); |
| } |
| |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, kAccountsPerSiteSuffix, |
| kOverallSuffix, custom_passphrase_suffix}), |
| accounts_per_site); |
| } |
| |
| static constexpr base::StringPiece kTotalAccountsByTypeSuffix = |
| ".TotalAccountsHiRes.ByType"; |
| |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, kTotalAccountsByTypeSuffix, |
| kUserCreatedSuffix, custom_passphrase_suffix}), |
| total_user_created_accounts); |
| |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, kTotalAccountsByTypeSuffix, |
| kAutoGeneratedSuffix, custom_passphrase_suffix}), |
| total_generated_accounts); |
| |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, kTotalAccountsByTypeSuffix, |
| kOverallSuffix, custom_passphrase_suffix}), |
| total_user_created_accounts + total_generated_accounts); |
| |
| LogAccountStatHiRes( |
| base::StrCat({kPasswordManager, store_suffix, ".BlacklistedSitesHiRes", |
| custom_passphrase_suffix}), |
| blacklisted_sites); |
| } |
| |
| void LoginDatabase::ReportTimesPasswordUsedMetrics( |
| bool custom_passphrase_sync_enabled) { |
| sql::Statement usage_statement(db_.GetCachedStatement( |
| SQL_FROM_HERE, "SELECT password_type, times_used FROM logins")); |
| |
| if (!usage_statement.is_valid()) |
| return; |
| |
| base::StringPiece store_suffix = GetMetricsSuffixForStore(); |
| base::StringPiece custom_passphrase_suffix = |
| GetCustomPassphraseSuffix(custom_passphrase_sync_enabled); |
| |
| while (usage_statement.Step()) { |
| auto type = static_cast<PasswordForm::Type>(usage_statement.ColumnInt(0)); |
| const int times_used = usage_statement.ColumnInt(1); |
| |
| static constexpr base::StringPiece kTimesPasswordUsedSuffix = |
| ".TimesPasswordUsed"; |
| |
| if (type == PasswordForm::Type::kGenerated) { |
| LogTimesUsedStat( |
| base::StrCat({kPasswordManager, store_suffix, |
| kTimesPasswordUsedSuffix, kAutoGeneratedSuffix, |
| custom_passphrase_suffix}), |
| times_used); |
| } else { |
| LogTimesUsedStat( |
| base::StrCat({kPasswordManager, store_suffix, |
| kTimesPasswordUsedSuffix, kUserCreatedSuffix, |
| custom_passphrase_suffix}), |
| times_used); |
| } |
| LogTimesUsedStat( |
| base::StrCat({kPasswordManager, store_suffix, kTimesPasswordUsedSuffix, |
| kOverallSuffix, custom_passphrase_suffix}), |
| times_used); |
| } |
| } |
| |
| void LoginDatabase::ReportSyncingAccountStateMetrics( |
| const std::string& sync_username) { |
| bool syncing_account_saved = false; |
| if (!sync_username.empty()) { |
| sql::Statement sync_statement( |
| db_.GetCachedStatement(SQL_FROM_HERE, |
| "SELECT username_value FROM logins " |
| "WHERE signon_realm == ?")); |
| sync_statement.BindString( |
| 0, GaiaUrls::GetInstance()->gaia_url().GetOrigin().spec()); |
| |
| if (!sync_statement.is_valid()) |
| return; |
| |
| while (sync_statement.Step()) { |
| std::string username = sync_statement.ColumnString(0); |
| if (gaia::AreEmailsSame(sync_username, username)) { |
| syncing_account_saved = true; |
| break; |
| } |
| } |
| } |
| UMA_HISTOGRAM_ENUMERATION("PasswordManager.SyncingAccountState", |
| 2 * sync_username.empty() + syncing_account_saved, |
| 4); |
| } |
| |
| void LoginDatabase::ReportEmptyUsernamesMetrics() { |
| sql::Statement empty_usernames_statement(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| "SELECT COUNT(*) FROM logins " |
| "WHERE blacklisted_by_user=0 AND username_value=''")); |
| if (empty_usernames_statement.Step()) { |
| int empty_forms = empty_usernames_statement.ColumnInt(0); |
| base::UmaHistogramCounts100( |
| base::StrCat({kPasswordManager, GetMetricsSuffixForStore(), |
| ".EmptyUsernames.CountInDatabase"}), |
| empty_forms); |
| } |
| } |
| |
| void LoginDatabase::ReportLoginsWithSchemesMetrics() { |
| sql::Statement logins_with_schemes_statement(db_.GetUniqueStatement( |
| "SELECT signon_realm, origin_url, blacklisted_by_user FROM logins;")); |
| |
| if (!logins_with_schemes_statement.is_valid()) |
| return; |
| |
| int android_logins = 0; |
| int ftp_logins = 0; |
| int http_logins = 0; |
| int https_logins = 0; |
| int other_logins = 0; |
| |
| while (logins_with_schemes_statement.Step()) { |
| std::string signon_realm = logins_with_schemes_statement.ColumnString(0); |
| GURL origin_url = GURL(logins_with_schemes_statement.ColumnString(1)); |
| bool blacklisted_by_user = !!logins_with_schemes_statement.ColumnInt(2); |
| if (blacklisted_by_user) |
| continue; |
| |
| if (IsValidAndroidFacetURI(signon_realm)) { |
| ++android_logins; |
| } else if (origin_url.SchemeIs(url::kHttpsScheme)) { |
| ++https_logins; |
| } else if (origin_url.SchemeIs(url::kHttpScheme)) { |
| ++http_logins; |
| } else if (origin_url.SchemeIs(url::kFtpScheme)) { |
| ++ftp_logins; |
| } else { |
| ++other_logins; |
| } |
| } |
| |
| base::StringPiece suffix_for_store = GetMetricsSuffixForStore(); |
| |
| LogNumberOfAccountsForScheme(suffix_for_store, "Android", android_logins); |
| LogNumberOfAccountsForScheme(suffix_for_store, "Ftp", ftp_logins); |
| LogNumberOfAccountsForScheme(suffix_for_store, "Http", http_logins); |
| LogNumberOfAccountsForScheme(suffix_for_store, "Https", https_logins); |
| LogNumberOfAccountsForScheme(suffix_for_store, "Other", other_logins); |
| } |
| |
| void LoginDatabase::ReportBubbleSuppressionMetrics() { |
| #if !defined(OS_IOS) && !defined(OS_ANDROID) |
| LogAccountStatHiRes( |
| "PasswordManager.BubbleSuppression.AccountsInStatisticsTable", |
| stats_table_.GetNumAccounts()); |
| #endif // !defined(OS_IOS) && !defined(OS_ANDROID) |
| } |
| |
| void LoginDatabase::ReportInaccessiblePasswordsMetrics() { |
| sql::Statement get_passwords_statement( |
| db_.GetUniqueStatement("SELECT password_value " |
| "FROM logins WHERE blacklisted_by_user = 0")); |
| |
| size_t failed_encryption = 0; |
| while (get_passwords_statement.Step()) { |
| base::string16 decrypted_password; |
| if (DecryptedString(get_passwords_statement.ColumnString(0), |
| &decrypted_password) != ENCRYPTION_RESULT_SUCCESS) { |
| ++failed_encryption; |
| } |
| } |
| base::UmaHistogramCounts100( |
| base::StrCat({kPasswordManager, GetMetricsSuffixForStore(), |
| ".InaccessiblePasswords"}), |
| failed_encryption); |
| } |
| |
| void LoginDatabase::ReportDuplicateCredentialsMetrics() { |
| sql::Statement duplicates_statement(db_.GetUniqueStatement( |
| "SELECT signon_realm, username_value, password_value FROM logins " |
| "WHERE blacklisted_by_user = 0")); |
| // First group the passwords by [signon_realm, username] (which should be a |
| // unique identifier). |
| std::map<std::pair<std::string, std::string>, std::vector<std::string>> |
| passwords_by_realm_and_user; |
| while (duplicates_statement.Step()) { |
| std::string signon_realm = duplicates_statement.ColumnString(0); |
| std::string username = duplicates_statement.ColumnString(1); |
| std::string encrypted_password = duplicates_statement.ColumnString(2); |
| #if defined(OS_WIN) |
| // Note: CryptProtectData() (used on Windows for encrypting passwords) is |
| // non-deterministic, so passwords must be decrypted before checking |
| // equality. |
| base::string16 password16; |
| if (DecryptedString(encrypted_password, &password16) != |
| ENCRYPTION_RESULT_SUCCESS) { |
| continue; |
| } |
| std::string password = base::UTF16ToUTF8(password16); |
| #else |
| // On non-Windows platforms, passwords are encrypted in a deterministic |
| // way. Since we're only interested in equality, don't bother decrypting |
| // them, just use the encrypted data directly. |
| std::string password = encrypted_password; |
| #endif |
| passwords_by_realm_and_user[std::make_pair(signon_realm, username)] |
| .push_back(password); |
| } |
| // Now go over the passwords by [realm, username] - typically there should |
| // be only one password each. |
| size_t credentials_with_duplicates = 0; |
| size_t credentials_with_mismatched_duplicates = 0; |
| for (auto& entry : passwords_by_realm_and_user) { |
| std::vector<std::string>& passwords = entry.second; |
| // Only one password -> no duplicates, move on. |
| if (passwords.size() == 1) |
| continue; |
| std::sort(passwords.begin(), passwords.end()); |
| auto last = std::unique(passwords.begin(), passwords.end()); |
| // If |last| moved from |.end()|, that means there were duplicate |
| // passwords. |
| if (last != passwords.end()) |
| credentials_with_duplicates++; |
| // If there is more than 1 password left after de-duping, then there were |
| // mismatched duplicates. |
| if (std::distance(passwords.begin(), last) > 1) |
| credentials_with_mismatched_duplicates++; |
| } |
| LogAccountStat("PasswordManager.CredentialsWithDuplicates", |
| credentials_with_duplicates); |
| LogAccountStat("PasswordManager.CredentialsWithMismatchedDuplicates", |
| credentials_with_mismatched_duplicates); |
| } |
| |
| void LoginDatabase::ReportMetrics(const std::string& sync_username, |
| bool custom_passphrase_sync_enabled, |
| BulkCheckDone bulk_check_done) { |
| TRACE_EVENT0("passwords", "LoginDatabase::ReportMetrics"); |
| |
| ReportNumberOfAccountsMetrics(custom_passphrase_sync_enabled); |
| ReportLoginsWithSchemesMetrics(); |
| ReportTimesPasswordUsedMetrics(custom_passphrase_sync_enabled); |
| ReportEmptyUsernamesMetrics(); |
| ReportInaccessiblePasswordsMetrics(); |
| |
| // The remaining metrics are not recorded for the account store: |
| // - SyncingAccountState just doesn't make sense, since syncing users only use |
| // the profile store. |
| // - BubbleSuppression fields aren't used in the account store. |
| // - DuplicateCredentials *could* be recorded for the profile store, but are |
| // not very critical. |
| // - Compromised credentials are only stored in the profile store. |
| if (is_account_store_.value()) |
| return; |
| |
| ReportSyncingAccountStateMetrics(sync_username); |
| ReportBubbleSuppressionMetrics(); |
| ReportDuplicateCredentialsMetrics(); |
| |
| compromised_credentials_table_.ReportMetrics(bulk_check_done); |
| } |
| |
| PasswordStoreChangeList LoginDatabase::AddLogin(const PasswordForm& form, |
| AddLoginError* error) { |
| TRACE_EVENT0("passwords", "LoginDatabase::AddLogin"); |
| if (error) { |
| *error = AddLoginError::kNone; |
| } |
| PasswordStoreChangeList list; |
| if (!DoesMatchConstraints(form)) { |
| if (error) { |
| *error = AddLoginError::kConstraintViolation; |
| } |
| return list; |
| } |
| std::string encrypted_password; |
| if (EncryptedString(form.password_value, &encrypted_password) != |
| ENCRYPTION_RESULT_SUCCESS) { |
| if (error) { |
| *error = AddLoginError::kEncrytionServiceFailure; |
| } |
| return list; |
| } |
| PasswordForm form_with_encrypted_password = form; |
| form_with_encrypted_password.encrypted_password = encrypted_password; |
| |
| DCHECK(!add_statement_.empty()); |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, add_statement_.c_str())); |
| BindAddStatement(form_with_encrypted_password, &s); |
| int sqlite_error_code; |
| db_.set_error_callback(base::BindRepeating(&AddCallback, &sqlite_error_code)); |
| const bool success = s.Run(); |
| if (success) { |
| // If success, the row never existed so password was not changed. |
| FillFormInStore(&form_with_encrypted_password); |
| list.emplace_back(PasswordStoreChange::ADD, |
| std::move(form_with_encrypted_password), |
| db_.GetLastInsertRowId(), |
| /*password_changed=*/false); |
| return list; |
| } |
| |
| // Repeat the same statement but with REPLACE semantic. |
| sqlite_error_code = 0; |
| DCHECK(!add_replace_statement_.empty()); |
| PrimaryKeyAndPassword old_primary_key_password = |
| GetPrimaryKeyAndPassword(form); |
| bool password_changed = |
| form.password_value != old_primary_key_password.decrypted_password; |
| s.Assign( |
| db_.GetCachedStatement(SQL_FROM_HERE, add_replace_statement_.c_str())); |
| BindAddStatement(form_with_encrypted_password, &s); |
| if (s.Run()) { |
| PasswordForm removed_form = form; |
| FillFormInStore(&removed_form); |
| list.emplace_back(PasswordStoreChange::REMOVE, removed_form, |
| old_primary_key_password.primary_key); |
| FillFormInStore(&form_with_encrypted_password); |
| list.emplace_back(PasswordStoreChange::ADD, |
| std::move(form_with_encrypted_password), |
| db_.GetLastInsertRowId(), password_changed); |
| } else if (error) { |
| if (sqlite_error_code == 19 /*SQLITE_CONSTRAINT*/) { |
| *error = AddLoginError::kConstraintViolation; |
| } else { |
| *error = AddLoginError::kDbError; |
| } |
| } |
| db_.reset_error_callback(); |
| return list; |
| } |
| |
| PasswordStoreChangeList LoginDatabase::UpdateLogin(const PasswordForm& form, |
| UpdateLoginError* error) { |
| TRACE_EVENT0("passwords", "LoginDatabase::UpdateLogin"); |
| if (error) { |
| *error = UpdateLoginError::kNone; |
| } |
| std::string encrypted_password; |
| if (EncryptedString(form.password_value, &encrypted_password) != |
| ENCRYPTION_RESULT_SUCCESS) { |
| if (error) { |
| *error = UpdateLoginError::kEncrytionServiceFailure; |
| } |
| return PasswordStoreChangeList(); |
| } |
| |
| const PrimaryKeyAndPassword old_primary_key_password = |
| GetPrimaryKeyAndPassword(form); |
| |
| #if defined(OS_IOS) |
| DeleteEncryptedPasswordFromKeychain( |
| old_primary_key_password.encrypted_password); |
| #endif |
| DCHECK(!update_statement_.empty()); |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, update_statement_.c_str())); |
| int next_param = 0; |
| s.BindString(next_param++, form.action.spec()); |
| s.BindBlob(next_param++, encrypted_password.data(), |
| static_cast<int>(encrypted_password.length())); |
| s.BindString16(next_param++, form.submit_element); |
| s.BindInt64(next_param++, form.date_created.ToInternalValue()); |
| s.BindInt(next_param++, form.blocked_by_user); |
| s.BindInt(next_param++, static_cast<int>(form.scheme)); |
| s.BindInt(next_param++, static_cast<int>(form.type)); |
| s.BindInt(next_param++, form.times_used); |
| base::Pickle form_data_pickle; |
| autofill::SerializeFormData(form.form_data, &form_data_pickle); |
| s.BindBlob(next_param++, form_data_pickle.data(), form_data_pickle.size()); |
| s.BindInt64(next_param++, form.date_synced.ToInternalValue()); |
| s.BindString16(next_param++, form.display_name); |
| s.BindString(next_param++, form.icon_url.spec()); |
| // An empty Origin serializes as "null" which would be strange to store here. |
| s.BindString(next_param++, form.federation_origin.opaque() |
| ? std::string() |
| : form.federation_origin.Serialize()); |
| s.BindInt(next_param++, form.skip_zero_click); |
| s.BindInt(next_param++, static_cast<int>(form.generation_upload_status)); |
| base::Pickle username_pickle = |
| SerializeValueElementPairs(form.all_possible_usernames); |
| s.BindBlob(next_param++, username_pickle.data(), username_pickle.size()); |
| s.BindInt64(next_param++, |
| form.date_last_used.ToDeltaSinceWindowsEpoch().InMicroseconds()); |
| base::Pickle moving_blocked_for_pickle = |
| SerializeGaiaIdHashVector(form.moving_blocked_for_list); |
| s.BindBlob(next_param++, moving_blocked_for_pickle.data(), |
| moving_blocked_for_pickle.size()); |
| // NOTE: Add new fields here unless the field is a part of the unique key. |
| // If so, add new field below. |
| |
| // WHERE starts here. |
| s.BindString(next_param++, form.url.spec()); |
| s.BindString16(next_param++, form.username_element); |
| s.BindString16(next_param++, form.username_value); |
| s.BindString16(next_param++, form.password_element); |
| s.BindString(next_param++, form.signon_realm); |
| // NOTE: Add new fields here only if the field is a part of the unique key. |
| // Otherwise, add the field above "WHERE starts here" comment. |
| |
| if (!s.Run()) { |
| if (error) { |
| *error = UpdateLoginError::kDbError; |
| } |
| return PasswordStoreChangeList(); |
| } |
| |
| PasswordStoreChangeList list; |
| if (db_.GetLastChangeCount()) { |
| bool password_changed = |
| form.password_value != old_primary_key_password.decrypted_password; |
| PasswordForm form_with_encrypted_password = form; |
| form_with_encrypted_password.encrypted_password = encrypted_password; |
| FillFormInStore(&form_with_encrypted_password); |
| list.emplace_back(PasswordStoreChange::UPDATE, |
| std::move(form_with_encrypted_password), |
| old_primary_key_password.primary_key, password_changed); |
| } else if (error) { |
| *error = UpdateLoginError::kNoUpdatedRecords; |
| } |
| |
| return list; |
| } |
| |
| bool LoginDatabase::RemoveLogin(const PasswordForm& form, |
| PasswordStoreChangeList* changes) { |
| TRACE_EVENT0("passwords", "LoginDatabase::RemoveLogin"); |
| if (changes) { |
| changes->clear(); |
| } |
| const PrimaryKeyAndPassword old_primary_key_password = |
| GetPrimaryKeyAndPassword(form); |
| #if defined(OS_IOS) |
| DeleteEncryptedPasswordFromKeychain( |
| old_primary_key_password.encrypted_password); |
| #endif |
| // Remove a login by UNIQUE-constrained fields. |
| DCHECK(!delete_statement_.empty()); |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, delete_statement_.c_str())); |
| s.BindString(0, form.url.spec()); |
| s.BindString16(1, form.username_element); |
| s.BindString16(2, form.username_value); |
| s.BindString16(3, form.password_element); |
| s.BindString(4, form.signon_realm); |
| |
| if (!s.Run() || db_.GetLastChangeCount() == 0) { |
| return false; |
| } |
| if (changes) { |
| PasswordForm removed_form = form; |
| FillFormInStore(&removed_form); |
| changes->emplace_back(PasswordStoreChange::REMOVE, removed_form, |
| old_primary_key_password.primary_key, |
| /*password_changed=*/true); |
| } |
| return true; |
| } |
| |
| bool LoginDatabase::RemoveLoginByPrimaryKey(int primary_key, |
| PasswordStoreChangeList* changes) { |
| TRACE_EVENT0("passwords", "LoginDatabase::RemoveLoginByPrimaryKey"); |
| PasswordForm form; |
| if (changes) { |
| changes->clear(); |
| sql::Statement s1(db_.GetCachedStatement( |
| SQL_FROM_HERE, "SELECT * FROM logins WHERE id = ?")); |
| s1.BindInt(0, primary_key); |
| if (!s1.Step()) { |
| return false; |
| } |
| int db_primary_key = -1; |
| EncryptionResult result = InitPasswordFormFromStatement( |
| s1, /*decrypt_and_fill_password_value=*/false, &db_primary_key, &form); |
| DCHECK_EQ(result, ENCRYPTION_RESULT_SUCCESS); |
| DCHECK_EQ(db_primary_key, primary_key); |
| } |
| |
| #if defined(OS_IOS) |
| DeleteEncryptedPasswordById(primary_key); |
| #endif |
| DCHECK(!delete_by_id_statement_.empty()); |
| sql::Statement s2( |
| db_.GetCachedStatement(SQL_FROM_HERE, delete_by_id_statement_.c_str())); |
| s2.BindInt(0, primary_key); |
| if (!s2.Run() || db_.GetLastChangeCount() == 0) { |
| return false; |
| } |
| if (changes) { |
| FillFormInStore(&form); |
| changes->emplace_back(PasswordStoreChange::REMOVE, std::move(form), |
| primary_key, /*password_changed=*/true); |
| } |
| return true; |
| } |
| |
| bool LoginDatabase::RemoveLoginsCreatedBetween( |
| base::Time delete_begin, |
| base::Time delete_end, |
| PasswordStoreChangeList* changes) { |
| TRACE_EVENT0("passwords", "LoginDatabase::RemoveLoginsCreatedBetween"); |
| if (changes) { |
| changes->clear(); |
| } |
| PrimaryKeyToFormMap key_to_form_map; |
| ScopedTransaction transaction(this); |
| if (!GetLoginsCreatedBetween(delete_begin, delete_end, &key_to_form_map)) { |
| return false; |
| } |
| |
| #if defined(OS_IOS) |
| for (const auto& pair : key_to_form_map) { |
| DeleteEncryptedPasswordById(pair.first); |
| } |
| #endif |
| |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, |
| "DELETE FROM logins WHERE " |
| "date_created >= ? AND date_created < ?")); |
| s.BindInt64(0, delete_begin.ToInternalValue()); |
| s.BindInt64(1, delete_end.is_null() ? std::numeric_limits<int64_t>::max() |
| : delete_end.ToInternalValue()); |
| |
| if (!s.Run()) { |
| return false; |
| } |
| if (changes) { |
| for (const auto& pair : key_to_form_map) { |
| changes->emplace_back(PasswordStoreChange::REMOVE, |
| /*form=*/std::move(*pair.second), |
| /*primary_key=*/pair.first, |
| /*password_changed=*/true); |
| } |
| } |
| return true; |
| } |
| |
| bool LoginDatabase::GetAutoSignInLogins(PrimaryKeyToFormMap* key_to_form_map) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetAutoSignInLogins"); |
| DCHECK(key_to_form_map); |
| DCHECK(!autosignin_statement_.empty()); |
| key_to_form_map->clear(); |
| |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, autosignin_statement_.c_str())); |
| FormRetrievalResult result = StatementToForms(&s, nullptr, key_to_form_map); |
| return result == FormRetrievalResult::kSuccess; |
| } |
| |
| bool LoginDatabase::DisableAutoSignInForOrigin(const GURL& origin) { |
| sql::Statement s(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| "UPDATE logins SET skip_zero_click = 1 WHERE origin_url = ?;")); |
| s.BindString(0, origin.spec()); |
| |
| return s.Run(); |
| } |
| |
| LoginDatabase::EncryptionResult LoginDatabase::InitPasswordFormFromStatement( |
| const sql::Statement& s, |
| bool decrypt_and_fill_password_value, |
| int* primary_key, |
| PasswordForm* form) const { |
| std::string encrypted_password; |
| s.ColumnBlobAsString(COLUMN_PASSWORD_VALUE, &encrypted_password); |
| base::string16 decrypted_password; |
| if (decrypt_and_fill_password_value) { |
| EncryptionResult encryption_result = |
| DecryptedString(encrypted_password, &decrypted_password); |
| if (encryption_result != ENCRYPTION_RESULT_SUCCESS) { |
| VLOG(0) << "Password decryption failed, encryption_result is " |
| << encryption_result; |
| return encryption_result; |
| } |
| } |
| |
| *primary_key = s.ColumnInt(COLUMN_ID); |
| std::string tmp = s.ColumnString(COLUMN_ORIGIN_URL); |
| form->url = GURL(tmp); |
| tmp = s.ColumnString(COLUMN_ACTION_URL); |
| form->action = GURL(tmp); |
| form->username_element = s.ColumnString16(COLUMN_USERNAME_ELEMENT); |
| form->username_value = s.ColumnString16(COLUMN_USERNAME_VALUE); |
| form->password_element = s.ColumnString16(COLUMN_PASSWORD_ELEMENT); |
| form->password_value = decrypted_password; |
| form->encrypted_password = encrypted_password; |
| form->submit_element = s.ColumnString16(COLUMN_SUBMIT_ELEMENT); |
| tmp = s.ColumnString(COLUMN_SIGNON_REALM); |
| form->signon_realm = tmp; |
| form->date_created = |
| base::Time::FromInternalValue(s.ColumnInt64(COLUMN_DATE_CREATED)); |
| form->blocked_by_user = (s.ColumnInt(COLUMN_BLACKLISTED_BY_USER) > 0); |
| // TODO(crbug.com/1151214): Add metrics to capture how often these values fall |
| // out of the valid enum range. |
| form->scheme = static_cast<PasswordForm::Scheme>(s.ColumnInt(COLUMN_SCHEME)); |
| form->type = |
| static_cast<PasswordForm::Type>(s.ColumnInt(COLUMN_PASSWORD_TYPE)); |
| if (s.ColumnByteLength(COLUMN_POSSIBLE_USERNAME_PAIRS)) { |
| base::Pickle pickle( |
| static_cast<const char*>(s.ColumnBlob(COLUMN_POSSIBLE_USERNAME_PAIRS)), |
| s.ColumnByteLength(COLUMN_POSSIBLE_USERNAME_PAIRS)); |
| form->all_possible_usernames = DeserializeValueElementPairs(pickle); |
| } |
| form->times_used = s.ColumnInt(COLUMN_TIMES_USED); |
| if (s.ColumnByteLength(COLUMN_FORM_DATA)) { |
| base::Pickle form_data_pickle( |
| static_cast<const char*>(s.ColumnBlob(COLUMN_FORM_DATA)), |
| s.ColumnByteLength(COLUMN_FORM_DATA)); |
| base::PickleIterator form_data_iter(form_data_pickle); |
| bool success = |
| autofill::DeserializeFormData(&form_data_iter, &form->form_data); |
| metrics_util::FormDeserializationStatus status = |
| success ? metrics_util::LOGIN_DATABASE_SUCCESS |
| : metrics_util::LOGIN_DATABASE_FAILURE; |
| metrics_util::LogFormDataDeserializationStatus(status); |
| } |
| form->date_synced = |
| base::Time::FromInternalValue(s.ColumnInt64(COLUMN_DATE_SYNCED)); |
| form->display_name = s.ColumnString16(COLUMN_DISPLAY_NAME); |
| form->icon_url = GURL(s.ColumnString(COLUMN_ICON_URL)); |
| form->federation_origin = |
| url::Origin::Create(GURL(s.ColumnString(COLUMN_FEDERATION_URL))); |
| form->skip_zero_click = (s.ColumnInt(COLUMN_SKIP_ZERO_CLICK) > 0); |
| form->generation_upload_status = |
| static_cast<PasswordForm::GenerationUploadStatus>( |
| s.ColumnInt(COLUMN_GENERATION_UPLOAD_STATUS)); |
| form->date_last_used = base::Time::FromDeltaSinceWindowsEpoch( |
| base::TimeDelta::FromMicroseconds(s.ColumnInt64(COLUMN_DATE_LAST_USED))); |
| if (s.ColumnByteLength(COLUMN_MOVING_BLOCKED_FOR)) { |
| base::Pickle pickle( |
| static_cast<const char*>(s.ColumnBlob(COLUMN_MOVING_BLOCKED_FOR)), |
| s.ColumnByteLength(COLUMN_MOVING_BLOCKED_FOR)); |
| form->moving_blocked_for_list = DeserializeGaiaIdHashVector(pickle); |
| } |
| return ENCRYPTION_RESULT_SUCCESS; |
| } |
| |
| bool LoginDatabase::GetLogins( |
| const PasswordStore::FormDigest& form, |
| std::vector<std::unique_ptr<PasswordForm>>* forms) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetLogins"); |
| DCHECK(forms); |
| forms->clear(); |
| |
| const GURL signon_realm(form.signon_realm); |
| std::string registered_domain = GetRegistryControlledDomain(signon_realm); |
| const bool should_PSL_matching_apply = |
| !registered_domain.empty() && form.scheme == PasswordForm::Scheme::kHtml; |
| const bool should_federated_apply = |
| form.scheme == PasswordForm::Scheme::kHtml; |
| DCHECK(!get_statement_.empty()); |
| DCHECK(!get_statement_psl_.empty()); |
| DCHECK(!get_statement_federated_.empty()); |
| DCHECK(!get_statement_psl_federated_.empty()); |
| const std::string* sql_query = &get_statement_; |
| if (should_PSL_matching_apply && should_federated_apply) |
| sql_query = &get_statement_psl_federated_; |
| else if (should_PSL_matching_apply) |
| sql_query = &get_statement_psl_; |
| else if (should_federated_apply) |
| sql_query = &get_statement_federated_; |
| |
| // TODO(nyquist) Consider usage of GetCachedStatement when |
| // http://crbug.com/248608 is fixed. |
| sql::Statement s(db_.GetUniqueStatement(sql_query->c_str())); |
| s.BindString(0, form.signon_realm); |
| int placeholder = 1; |
| |
| // PSL matching only applies to HTML forms. |
| if (should_PSL_matching_apply) { |
| // We are extending the original SQL query with one that includes more |
| // possible matches based on public suffix domain matching. Using a regexp |
| // here is just an optimization to not have to parse all the stored entries |
| // in the |logins| table. The result (scheme, domain and port) is verified |
| // further down using GURL. See the functions SchemeMatches, |
| // RegistryControlledDomainMatches and PortMatches. |
| // We need to escape . in the domain. Since the domain has already been |
| // sanitized using GURL, we do not need to escape any other characters. |
| base::ReplaceChars(registered_domain, ".", "\\.", ®istered_domain); |
| std::string scheme = signon_realm.scheme(); |
| // We need to escape . in the scheme. Since the scheme has already been |
| // sanitized using GURL, we do not need to escape any other characters. |
| // The scheme soap.beep is an example with '.'. |
| base::ReplaceChars(scheme, ".", "\\.", &scheme); |
| const std::string port = signon_realm.port(); |
| // For a signon realm such as http://foo.bar/, this regexp will match |
| // domains on the form http://foo.bar/, http://www.foo.bar/, |
| // http://www.mobile.foo.bar/. It will not match http://notfoo.bar/. |
| // The scheme and port has to be the same as the observed form. |
| std::string regexp = "^(" + scheme + ":\\/\\/)([\\w-]+\\.)*" + |
| registered_domain + "(:" + port + ")?\\/$"; |
| s.BindString(placeholder++, regexp); |
| |
| if (should_federated_apply) { |
| // This regex matches any subdomain of |registered_domain|, in particular |
| // it matches the empty subdomain. Hence exact domain matches are also |
| // retrieved. |
| s.BindString(placeholder++, |
| "^federation://([\\w-]+\\.)*" + registered_domain + "/.+$"); |
| } |
| } else if (should_federated_apply) { |
| std::string expression = |
| base::StringPrintf("federation://%s/%%", form.url.host().c_str()); |
| s.BindString(placeholder++, expression); |
| } |
| |
| PrimaryKeyToFormMap key_to_form_map; |
| FormRetrievalResult result = StatementToForms( |
| &s, should_PSL_matching_apply || should_federated_apply ? &form : nullptr, |
| &key_to_form_map); |
| if (result != FormRetrievalResult::kSuccess) { |
| return false; |
| } |
| for (auto& pair : key_to_form_map) { |
| forms->push_back(std::move(pair.second)); |
| } |
| return true; |
| } |
| |
| bool LoginDatabase::GetLoginsByPassword( |
| const base::string16& plain_text_password, |
| std::vector<std::unique_ptr<PasswordForm>>* forms) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetLoginsByPassword"); |
| DCHECK(forms); |
| forms->clear(); |
| |
| // Get all autofillable (not blacklisted) logins. |
| DCHECK(!blacklisted_statement_.empty()); |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, blacklisted_statement_.c_str())); |
| s.BindInt(0, 0); // blacklisted = false |
| |
| // Apply query, check status and copy results if successful. |
| PrimaryKeyToFormMap key_to_form_map; |
| FormRetrievalResult result = |
| StatementToForms(&s, /*matched_form=*/nullptr, &key_to_form_map); |
| |
| if (result != FormRetrievalResult::kSuccess) { |
| return false; |
| } |
| for (auto& pair : key_to_form_map) { |
| if (pair.second->password_value == plain_text_password) { |
| // Only add the form the result if the password value matches. |
| forms->push_back(std::move(pair.second)); |
| } |
| } |
| return true; |
| } |
| |
| bool LoginDatabase::GetLoginsCreatedBetween( |
| const base::Time begin, |
| const base::Time end, |
| PrimaryKeyToFormMap* key_to_form_map) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetLoginsCreatedBetween"); |
| DCHECK(key_to_form_map); |
| DCHECK(!created_statement_.empty()); |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, created_statement_.c_str())); |
| s.BindInt64(0, begin.ToInternalValue()); |
| s.BindInt64(1, end.is_null() ? std::numeric_limits<int64_t>::max() |
| : end.ToInternalValue()); |
| |
| return StatementToForms(&s, nullptr, key_to_form_map) == |
| FormRetrievalResult::kSuccess; |
| } |
| |
| FormRetrievalResult LoginDatabase::GetAllLogins( |
| PrimaryKeyToFormMap* key_to_form_map) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetAllLogins"); |
| DCHECK(key_to_form_map); |
| key_to_form_map->clear(); |
| |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, "SELECT * FROM logins")); |
| |
| return StatementToForms(&s, nullptr, key_to_form_map); |
| } |
| |
| bool LoginDatabase::GetAutofillableLogins( |
| std::vector<std::unique_ptr<PasswordForm>>* forms) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetAutofillableLogins"); |
| return GetAllLoginsWithBlacklistSetting(false, forms); |
| } |
| |
| bool LoginDatabase::GetBlacklistLogins( |
| std::vector<std::unique_ptr<PasswordForm>>* forms) { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetBlacklistLogins"); |
| return GetAllLoginsWithBlacklistSetting(true, forms); |
| } |
| |
| bool LoginDatabase::GetAllLoginsWithBlacklistSetting( |
| bool blacklisted, |
| std::vector<std::unique_ptr<PasswordForm>>* forms) { |
| DCHECK(forms); |
| DCHECK(!blacklisted_statement_.empty()); |
| forms->clear(); |
| |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, blacklisted_statement_.c_str())); |
| s.BindInt(0, blacklisted ? 1 : 0); |
| |
| PrimaryKeyToFormMap key_to_form_map; |
| |
| if (StatementToForms(&s, nullptr, &key_to_form_map) != |
| FormRetrievalResult::kSuccess) { |
| return false; |
| } |
| |
| for (auto& pair : key_to_form_map) { |
| forms->push_back(std::move(pair.second)); |
| } |
| return true; |
| } |
| |
| bool LoginDatabase::IsEmpty() { |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, "SELECT COUNT(*) FROM logins")); |
| return s.Step() && s.ColumnInt(0) == 0; |
| } |
| |
| // static |
| void LoginDatabase::DeleteDatabaseFile(const base::FilePath& db_path) { |
| sql::Database::Delete(db_path); |
| } |
| |
| bool LoginDatabase::DeleteAndRecreateDatabaseFile() { |
| TRACE_EVENT0("passwords", "LoginDatabase::DeleteAndRecreateDatabaseFile"); |
| DCHECK(db_.is_open()); |
| meta_table_.Reset(); |
| db_.Close(); |
| DeleteDatabaseFile(db_path_); |
| return Init(); |
| } |
| |
| DatabaseCleanupResult LoginDatabase::DeleteUndecryptableLogins() { |
| #if defined(OS_MAC) |
| TRACE_EVENT0("passwords", "LoginDatabase::DeleteUndecryptableLogins"); |
| // If the Keychain is unavailable, don't delete any logins. |
| if (!OSCrypt::IsEncryptionAvailable()) { |
| metrics_util::LogDeleteUndecryptableLoginsReturnValue( |
| metrics_util::DeleteCorruptedPasswordsResult::kEncryptionUnavailable); |
| return DatabaseCleanupResult::kEncryptionUnavailable; |
| } |
| |
| DCHECK(db_.is_open()); |
| |
| // Get all autofillable (not blacklisted) logins. |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, blacklisted_statement_.c_str())); |
| s.BindInt(0, 0); // blacklisted = false |
| |
| std::vector<PasswordForm> forms_to_be_deleted; |
| |
| while (s.Step()) { |
| std::string encrypted_password; |
| s.ColumnBlobAsString(COLUMN_PASSWORD_VALUE, &encrypted_password); |
| base::string16 decrypted_password; |
| if (DecryptedString(encrypted_password, &decrypted_password) == |
| ENCRYPTION_RESULT_SUCCESS) |
| continue; |
| |
| // If it was not possible to decrypt the password, remove it from the |
| // database. |
| forms_to_be_deleted.push_back(GetFormForRemoval(s)); |
| } |
| |
| for (const auto& form : forms_to_be_deleted) { |
| if (!RemoveLogin(form, nullptr)) { |
| metrics_util::LogDeleteUndecryptableLoginsReturnValue( |
| metrics_util::DeleteCorruptedPasswordsResult::kItemFailure); |
| return DatabaseCleanupResult::kItemFailure; |
| } |
| } |
| |
| if (forms_to_be_deleted.empty()) { |
| metrics_util::LogDeleteUndecryptableLoginsReturnValue( |
| metrics_util::DeleteCorruptedPasswordsResult::kSuccessNoDeletions); |
| } else { |
| DCHECK(password_recovery_util_); |
| password_recovery_util_->RecordPasswordRecovery(); |
| metrics_util::LogDeleteUndecryptableLoginsReturnValue( |
| metrics_util::DeleteCorruptedPasswordsResult::kSuccessPasswordsDeleted); |
| UMA_HISTOGRAM_COUNTS_100("PasswordManager.CleanedUpPasswords", |
| forms_to_be_deleted.size()); |
| } |
| #endif |
| |
| return DatabaseCleanupResult::kSuccess; |
| } |
| |
| std::unique_ptr<syncer::MetadataBatch> LoginDatabase::GetAllSyncMetadata() { |
| TRACE_EVENT0("passwords", "LoginDatabase::GetAllSyncMetadata"); |
| std::unique_ptr<syncer::MetadataBatch> metadata_batch = |
| GetAllSyncEntityMetadata(); |
| if (metadata_batch == nullptr) { |
| return nullptr; |
| } |
| |
| std::unique_ptr<sync_pb::ModelTypeState> model_type_state = |
| GetModelTypeState(); |
| if (model_type_state == nullptr) { |
| return nullptr; |
| } |
| |
| metadata_batch->SetModelTypeState(*model_type_state); |
| return metadata_batch; |
| } |
| |
| void LoginDatabase::DeleteAllSyncMetadata() { |
| TRACE_EVENT0("passwords", "LoginDatabase::DeleteAllSyncMetadata"); |
| bool had_unsynced_deletions = HasUnsyncedDeletions(); |
| ClearAllSyncMetadata(&db_); |
| if (had_unsynced_deletions && deletions_have_synced_callback_) { |
| // Note: At this point we can't be fully sure whether the deletions actually |
| // reached the server yet. We might have sent a commit, but haven't received |
| // the commit confirmation. Let's be conservative and assume they haven't |
| // been successfully deleted. |
| deletions_have_synced_callback_.Run(/*success=*/false); |
| } |
| } |
| |
| bool LoginDatabase::UpdateSyncMetadata( |
| syncer::ModelType model_type, |
| const std::string& storage_key, |
| const sync_pb::EntityMetadata& metadata) { |
| TRACE_EVENT0("passwords", "LoginDatabase::UpdateSyncMetadata"); |
| DCHECK_EQ(model_type, syncer::PASSWORDS); |
| |
| int storage_key_int = 0; |
| if (!base::StringToInt(storage_key, &storage_key_int)) { |
| DLOG(ERROR) << "Invalid storage key. Failed to convert the storage key to " |
| "an integer."; |
| return false; |
| } |
| |
| std::string encrypted_metadata; |
| if (!OSCrypt::EncryptString(metadata.SerializeAsString(), |
| &encrypted_metadata)) { |
| DLOG(ERROR) << "Cannot encrypt the sync metadata"; |
| return false; |
| } |
| |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, |
| "INSERT OR REPLACE INTO sync_entities_metadata " |
| "(storage_key, metadata) VALUES(?, ?)")); |
| |
| s.BindInt(0, storage_key_int); |
| s.BindString(1, encrypted_metadata); |
| |
| bool had_unsynced_deletions = HasUnsyncedDeletions(); |
| bool result = s.Run(); |
| if (result && had_unsynced_deletions && !HasUnsyncedDeletions() && |
| deletions_have_synced_callback_) { |
| deletions_have_synced_callback_.Run(/*success=*/true); |
| } |
| return result; |
| } |
| |
| bool LoginDatabase::ClearSyncMetadata(syncer::ModelType model_type, |
| const std::string& storage_key) { |
| TRACE_EVENT0("passwords", "LoginDatabase::ClearSyncMetadata"); |
| DCHECK_EQ(model_type, syncer::PASSWORDS); |
| |
| int storage_key_int = 0; |
| if (!base::StringToInt(storage_key, &storage_key_int)) { |
| DLOG(ERROR) << "Invalid storage key. Failed to convert the storage key to " |
| "an integer."; |
| return false; |
| } |
| |
| sql::Statement s( |
| db_.GetCachedStatement(SQL_FROM_HERE, |
| "DELETE FROM sync_entities_metadata WHERE " |
| "storage_key=?")); |
| s.BindInt(0, storage_key_int); |
| |
| bool had_unsynced_deletions = HasUnsyncedDeletions(); |
| bool result = s.Run(); |
| if (result && had_unsynced_deletions && !HasUnsyncedDeletions() && |
| deletions_have_synced_callback_) { |
| deletions_have_synced_callback_.Run(/*success=*/true); |
| } |
| return result; |
| } |
| |
| bool LoginDatabase::UpdateModelTypeState( |
| syncer::ModelType model_type, |
| const sync_pb::ModelTypeState& model_type_state) { |
| TRACE_EVENT0("passwords", "LoginDatabase::UpdateModelTypeState"); |
| DCHECK_EQ(model_type, syncer::PASSWORDS); |
| |
| // Make sure only one row is left by storing it in the entry with id=1 |
| // every time. |
| sql::Statement s(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| "INSERT OR REPLACE INTO sync_model_metadata (id, model_metadata) " |
| "VALUES(1, ?)")); |
| s.BindString(0, model_type_state.SerializeAsString()); |
| |
| return s.Run(); |
| } |
| |
| bool LoginDatabase::ClearModelTypeState(syncer::ModelType model_type) { |
| TRACE_EVENT0("passwords", "LoginDatabase::ClearModelTypeState"); |
| DCHECK_EQ(model_type, syncer::PASSWORDS); |
| |
| sql::Statement s(db_.GetCachedStatement( |
| SQL_FROM_HERE, "DELETE FROM sync_model_metadata WHERE id=1")); |
| |
| return s.Run(); |
| } |
| |
| void LoginDatabase::SetDeletionsHaveSyncedCallback( |
| base::RepeatingCallback<void(bool)> callback) { |
| deletions_have_synced_callback_ = std::move(callback); |
| } |
| |
| bool LoginDatabase::HasUnsyncedDeletions() { |
| TRACE_EVENT0("passwords", "LoginDatabase::HasUnsyncedDeletions"); |
| |
| std::unique_ptr<syncer::MetadataBatch> batch = GetAllSyncEntityMetadata(); |
| if (!batch) |
| return false; |
| for (const auto& metadata_entry : batch->GetAllMetadata()) { |
| // Note: No need for an explicit "is unsynced" check: Once the deletion is |
| // committed, the metadata entry is removed. |
| if (metadata_entry.second->is_deleted()) |
| return true; |
| } |
| return false; |
| } |
| |
| bool LoginDatabase::BeginTransaction() { |
| TRACE_EVENT0("passwords", "LoginDatabase::BeginTransaction"); |
| return db_.BeginTransaction(); |
| } |
| |
| void LoginDatabase::RollbackTransaction() { |
| TRACE_EVENT0("passwords", "LoginDatabase::RollbackTransaction"); |
| db_.RollbackTransaction(); |
| } |
| |
| bool LoginDatabase::CommitTransaction() { |
| TRACE_EVENT0("passwords", "LoginDatabase::CommitTransaction"); |
| return db_.CommitTransaction(); |
| } |
| |
| LoginDatabase::PrimaryKeyAndPassword LoginDatabase::GetPrimaryKeyAndPassword( |
| const PasswordForm& form) const { |
| DCHECK(!id_and_password_statement_.empty()); |
| sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE, |
| id_and_password_statement_.c_str())); |
| |
| s.BindString(0, form.url.spec()); |
| s.BindString16(1, form.username_element); |
| s.BindString16(2, form.username_value); |
| s.BindString16(3, form.password_element); |
| s.BindString(4, form.signon_realm); |
| |
| if (s.Step()) { |
| PrimaryKeyAndPassword result = {s.ColumnInt(0)}; |
| s.ColumnBlobAsString(1, &result.encrypted_password); |
| if (DecryptedString(result.encrypted_password, |
| &result.decrypted_password) != |
| ENCRYPTION_RESULT_SUCCESS) { |
| result.decrypted_password.clear(); |
| } |
| return result; |
| } |
| return {-1, std::string(), base::string16()}; |
| } |
| |
| std::unique_ptr<syncer::MetadataBatch> |
| LoginDatabase::GetAllSyncEntityMetadata() { |
| auto metadata_batch = std::make_unique<syncer::MetadataBatch>(); |
| sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE, |
| "SELECT storage_key, metadata FROM " |
| "sync_entities_metadata")); |
| |
| while (s.Step()) { |
| int storage_key_int = s.ColumnInt(0); |
| std::string storage_key = base::NumberToString(storage_key_int); |
| std::string encrypted_serialized_metadata = s.ColumnString(1); |
| std::string decrypted_serialized_metadata; |
| if (!OSCrypt::DecryptString(encrypted_serialized_metadata, |
| &decrypted_serialized_metadata)) { |
| DLOG(WARNING) << "Failed to decrypt PASSWORD model type " |
| "sync_pb::EntityMetadata."; |
| return nullptr; |
| } |
| |
| auto entity_metadata = std::make_unique<sync_pb::EntityMetadata>(); |
| if (entity_metadata->ParseFromString(decrypted_serialized_metadata)) { |
| metadata_batch->AddMetadata(storage_key, std::move(entity_metadata)); |
| } else { |
| DLOG(WARNING) << "Failed to deserialize PASSWORD model type " |
| "sync_pb::EntityMetadata."; |
| return nullptr; |
| } |
| } |
| if (!s.Succeeded()) { |
| return nullptr; |
| } |
| return metadata_batch; |
| } |
| |
| std::unique_ptr<sync_pb::ModelTypeState> LoginDatabase::GetModelTypeState() { |
| auto state = std::make_unique<sync_pb::ModelTypeState>(); |
| sql::Statement s(db_.GetCachedStatement( |
| SQL_FROM_HERE, |
| "SELECT model_metadata FROM sync_model_metadata WHERE id=1")); |
| |
| if (!s.Step()) { |
| if (s.Succeeded()) |
| return state; |
| else |
| return nullptr; |
| } |
| |
| std::string serialized_state = s.ColumnString(0); |
| if (state->ParseFromString(serialized_state)) { |
| return state; |
| } |
| return nullptr; |
| } |
| |
| FormRetrievalResult LoginDatabase::StatementToForms( |
| sql::Statement* statement, |
| const PasswordStore::FormDigest* matched_form, |
| PrimaryKeyToFormMap* key_to_form_map) { |
| std::vector<PasswordForm> forms_to_be_deleted; |
| |
| key_to_form_map->clear(); |
| while (statement->Step()) { |
| auto new_form = std::make_unique<PasswordForm>(); |
| FillFormInStore(new_form.get()); |
| |
| int primary_key = -1; |
| EncryptionResult result = InitPasswordFormFromStatement( |
| *statement, /*decrypt_and_fill_password_value=*/true, &primary_key, |
| new_form.get()); |
| if (result == ENCRYPTION_RESULT_SERVICE_FAILURE) |
| return FormRetrievalResult::kEncrytionServiceFailure; |
| if (result == ENCRYPTION_RESULT_ITEM_FAILURE) { |
| continue; |
| } |
| DCHECK_EQ(ENCRYPTION_RESULT_SUCCESS, result); |
| |
| if (matched_form) { |
| switch (GetMatchResult(*new_form, *matched_form)) { |
| case MatchResult::NO_MATCH: |
| continue; |
| case MatchResult::EXACT_MATCH: |
| case MatchResult::FEDERATED_MATCH: |
| break; |
| case MatchResult::PSL_MATCH: |
| case MatchResult::FEDERATED_PSL_MATCH: |
| new_form->is_public_suffix_match = true; |
| break; |
| } |
| } |
| |
| key_to_form_map->emplace(primary_key, std::move(new_form)); |
| } |
| |
| #if defined(OS_MAC) |
| // Remove corrupted passwords. |
| size_t count_removed_logins = 0; |
| for (const auto& form : forms_to_be_deleted) { |
| if (RemoveLogin(form, nullptr)) { |
| count_removed_logins++; |
| } |
| } |
| if (count_removed_logins == forms_to_be_deleted.size() && |
| count_removed_logins > 0) { |
| DCHECK(password_recovery_util_); |
| password_recovery_util_->RecordPasswordRecovery(); |
| } |
| #endif |
| |
| if (!statement->Succeeded()) |
| return FormRetrievalResult::kDbError; |
| return FormRetrievalResult::kSuccess; |
| } |
| |
| void LoginDatabase::InitializeStatementStrings(const SQLTableBuilder& builder) { |
| // This method may be called multiple times, if Chrome switches backends and |
| // LoginDatabase::DeleteAndRecreateDatabaseFile ends up being called. In those |
| // case do not recompute the SQL statements, because they would end up the |
| // same. |
| if (!add_statement_.empty()) |
| return; |
| |
| // Initialize the cached strings. |
| std::string all_column_names = builder.ListAllColumnNames(); |
| std::string right_amount_of_placeholders = |
| GeneratePlaceholders(builder.NumberOfColumns()); |
| std::string all_unique_key_column_names = builder.ListAllUniqueKeyNames(); |
| std::string all_nonunique_key_column_names = |
| builder.ListAllNonuniqueKeyNames(); |
| |
| add_statement_ = "INSERT INTO logins (" + all_column_names + ") VALUES " + |
| right_amount_of_placeholders; |
| DCHECK(add_replace_statement_.empty()); |
| add_replace_statement_ = "INSERT OR REPLACE INTO logins (" + |
| all_column_names + ") VALUES " + |
| right_amount_of_placeholders; |
| DCHECK(update_statement_.empty()); |
| update_statement_ = "UPDATE logins SET " + all_nonunique_key_column_names + |
| " WHERE " + all_unique_key_column_names; |
| DCHECK(delete_statement_.empty()); |
| delete_statement_ = "DELETE FROM logins WHERE " + all_unique_key_column_names; |
| DCHECK(delete_by_id_statement_.empty()); |
| delete_by_id_statement_ = "DELETE FROM logins WHERE id=?"; |
| DCHECK(autosignin_statement_.empty()); |
| autosignin_statement_ = "SELECT " + all_column_names + |
| " FROM logins " |
| "WHERE skip_zero_click = 0 ORDER BY origin_url"; |
| DCHECK(get_statement_.empty()); |
| get_statement_ = "SELECT " + all_column_names + |
| " FROM logins " |
| "WHERE signon_realm == ?"; |
| std::string psl_statement = "OR signon_realm REGEXP ? "; |
| std::string federated_statement = |
| "OR (signon_realm LIKE ? AND password_type == 2) "; |
| std::string psl_federated_statement = |
| "OR (signon_realm REGEXP ? AND password_type == 2) "; |
| DCHECK(get_statement_psl_.empty()); |
| get_statement_psl_ = get_statement_ + psl_statement; |
| DCHECK(get_statement_federated_.empty()); |
| get_statement_federated_ = get_statement_ + federated_statement; |
| DCHECK(get_statement_psl_federated_.empty()); |
| get_statement_psl_federated_ = |
| get_statement_ + psl_statement + psl_federated_statement; |
| DCHECK(created_statement_.empty()); |
| created_statement_ = |
| "SELECT " + all_column_names + |
| " FROM logins WHERE date_created >= ? AND date_created < " |
| "? ORDER BY origin_url"; |
| DCHECK(blacklisted_statement_.empty()); |
| blacklisted_statement_ = |
| "SELECT " + all_column_names + |
| " FROM logins WHERE blacklisted_by_user == ? ORDER BY origin_url"; |
| DCHECK(encrypted_password_statement_by_id_.empty()); |
| encrypted_password_statement_by_id_ = |
| "SELECT password_value FROM logins WHERE id=?"; |
| DCHECK(id_and_password_statement_.empty()); |
| id_and_password_statement_ = "SELECT id, password_value FROM logins WHERE " + |
| all_unique_key_column_names; |
| } |
| |
| void LoginDatabase::FillFormInStore(PasswordForm* form) const { |
| form->in_store = is_account_store() ? PasswordForm::Store::kAccountStore |
| : PasswordForm::Store::kProfileStore; |
| } |
| |
| } // namespace password_manager |