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/command_line.h"
11 #include "base/files/file_path.h"
12 #include "base/logging.h"
13 #include "base/metrics/histogram.h"
14 #include "base/pickle.h"
15 #include "base/strings/string_util.h"
16 #include "base/time/time.h"
17 #include "chrome/common/chrome_switches.h"
18 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
19 #include "sql/connection.h"
20 #include "sql/statement.h"
21 #include "sql/transaction.h"
23 using autofill::PasswordForm;
25 static const int kCurrentVersionNumber = 4;
26 static const int kCompatibleVersionNumber = 1;
30 // Convenience enum for interacting with SQL queries that use all the columns.
31 enum LoginTableColumns {
32 COLUMN_ORIGIN_URL = 0,
34 COLUMN_USERNAME_ELEMENT,
35 COLUMN_USERNAME_VALUE,
36 COLUMN_PASSWORD_ELEMENT,
37 COLUMN_PASSWORD_VALUE,
38 COLUMN_SUBMIT_ELEMENT,
43 COLUMN_BLACKLISTED_BY_USER,
46 COLUMN_POSSIBLE_USERNAMES,
51 // Enum used for histogram tracking PSL Domain triggering.
52 // New entries should only be added to the end of the enum (before *_COUNT) so
53 // as to not disrupt existing data.
54 enum PSLDomainMatchMetric {
55 PSL_DOMAIN_MATCH_DISABLED = 0,
56 PSL_DOMAIN_MATCH_NONE,
57 PSL_DOMAIN_MATCH_FOUND,
58 PSL_DOMAIN_MATCH_COUNT
61 // Using the public suffix list for matching the origin is only needed for
62 // websites that do not have a single hostname for entering credentials. It
63 // would be better for their users if they did, but until then we help them find
64 // credentials across different hostnames. We know that accounts.google.com is
65 // the only hostname we should be accepting credentials on for any domain under
66 // google.com, so we can apply a tighter policy for that domain.
67 // For owners of domains where a single hostname is always used when your
68 // users are entering their credentials, please contact palmer@chromium.org,
69 // nyquist@chromium.org or file a bug at http://crbug.com/ to be added here.
70 bool ShouldPSLDomainMatchingApply(
71 const std::string& registry_controlled_domain) {
72 return registry_controlled_domain != "google.com";
75 std::string GetRegistryControlledDomain(const GURL& signon_realm) {
76 return net::registry_controlled_domains::GetDomainAndRegistry(
78 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
81 std::string GetRegistryControlledDomain(const std::string& signon_realm_str) {
82 GURL signon_realm(signon_realm_str);
83 return net::registry_controlled_domains::GetDomainAndRegistry(
85 net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
88 bool RegistryControlledDomainMatches(const scoped_ptr<PasswordForm>& found,
89 const PasswordForm current) {
90 const std::string found_registry_controlled_domain =
91 GetRegistryControlledDomain(found->signon_realm);
92 const std::string form_registry_controlled_domain =
93 GetRegistryControlledDomain(current.signon_realm);
94 return found_registry_controlled_domain == form_registry_controlled_domain;
97 bool SchemeMatches(const scoped_ptr<PasswordForm>& found,
98 const PasswordForm current) {
99 const std::string found_scheme = GURL(found->signon_realm).scheme();
100 const std::string form_scheme = GURL(current.signon_realm).scheme();
101 return found_scheme == form_scheme;
104 bool PortMatches(const scoped_ptr<PasswordForm>& found,
105 const PasswordForm current) {
106 const std::string found_port = GURL(found->signon_realm).port();
107 const std::string form_port = GURL(current.signon_realm).port();
108 return found_port == form_port;
111 bool IsPublicSuffixDomainMatchingEnabled() {
112 #if defined(OS_ANDROID)
113 if (CommandLine::ForCurrentProcess()->HasSwitch(
114 switches::kEnablePasswordAutofillPublicSuffixDomainMatching)) {
117 if (CommandLine::ForCurrentProcess()->HasSwitch(
118 switches::kDisablePasswordAutofillPublicSuffixDomainMatching)) {
129 LoginDatabase::LoginDatabase() : public_suffix_domain_matching_(false) {
132 LoginDatabase::~LoginDatabase() {
135 bool LoginDatabase::Init(const base::FilePath& db_path) {
136 // Set pragmas for a small, private database (based on WebDatabase).
137 db_.set_page_size(2048);
138 db_.set_cache_size(32);
139 db_.set_exclusive_locking();
140 db_.set_restrict_to_user();
142 if (!db_.Open(db_path)) {
143 LOG(WARNING) << "Unable to open the password store database.";
147 sql::Transaction transaction(&db_);
150 // Check the database version.
151 if (!meta_table_.Init(&db_, kCurrentVersionNumber,
152 kCompatibleVersionNumber)) {
156 if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) {
157 LOG(WARNING) << "Password store database is too new.";
162 // Initialize the tables.
163 if (!InitLoginsTable()) {
164 LOG(WARNING) << "Unable to initialize the password store database.";
169 // Save the path for DeleteDatabaseFile().
172 // If the file on disk is an older database version, bring it up to date.
173 if (!MigrateOldVersionsAsNeeded()) {
174 LOG(WARNING) << "Unable to migrate database";
179 if (!transaction.Commit()) {
184 public_suffix_domain_matching_ = IsPublicSuffixDomainMatchingEnabled();
189 bool LoginDatabase::MigrateOldVersionsAsNeeded() {
190 switch (meta_table_.GetVersionNumber()) {
192 if (!db_.Execute("ALTER TABLE logins "
193 "ADD COLUMN password_type INTEGER") ||
194 !db_.Execute("ALTER TABLE logins "
195 "ADD COLUMN possible_usernames BLOB")) {
198 meta_table_.SetVersionNumber(2);
201 if (!db_.Execute("ALTER TABLE logins ADD COLUMN times_used INTEGER")) {
204 meta_table_.SetVersionNumber(3);
207 // We need to check if the column exists because of
208 // https://crbug.com/295851
209 if (!db_.DoesColumnExist("logins", "form_data") &&
210 !db_.Execute("ALTER TABLE logins ADD COLUMN form_data BLOB")) {
213 meta_table_.SetVersionNumber(4);
215 case kCurrentVersionNumber:
216 // Already up to date
224 bool LoginDatabase::InitLoginsTable() {
225 if (!db_.DoesTableExist("logins")) {
226 if (!db_.Execute("CREATE TABLE logins ("
227 "origin_url VARCHAR NOT NULL, "
228 "action_url VARCHAR, "
229 "username_element VARCHAR, "
230 "username_value VARCHAR, "
231 "password_element VARCHAR, "
232 "password_value BLOB, "
233 "submit_element VARCHAR, "
234 "signon_realm VARCHAR NOT NULL,"
235 "ssl_valid INTEGER NOT NULL,"
236 "preferred INTEGER NOT NULL,"
237 "date_created INTEGER NOT NULL,"
238 "blacklisted_by_user INTEGER NOT NULL,"
239 "scheme INTEGER NOT NULL,"
240 "password_type INTEGER,"
241 "possible_usernames BLOB,"
242 "times_used INTEGER,"
245 "(origin_url, username_element, "
246 "username_value, password_element, "
247 "submit_element, signon_realm))")) {
251 if (!db_.Execute("CREATE INDEX logins_signon ON "
252 "logins (signon_realm)")) {
260 void LoginDatabase::ReportMetrics() {
261 sql::Statement s(db_.GetCachedStatement(
263 "SELECT signon_realm, blacklisted_by_user, COUNT(username_value) "
264 "FROM logins GROUP BY signon_realm, blacklisted_by_user"));
269 int total_accounts = 0;
270 int blacklisted_sites = 0;
272 int blacklisted = s.ColumnInt(1);
273 int accounts_per_site = s.ColumnInt(2);
277 total_accounts += accounts_per_site;
278 UMA_HISTOGRAM_CUSTOM_COUNTS("PasswordManager.AccountsPerSite",
279 accounts_per_site, 0, 32, 6);
282 UMA_HISTOGRAM_CUSTOM_COUNTS("PasswordManager.TotalAccounts",
283 total_accounts, 0, 32, 6);
284 UMA_HISTOGRAM_CUSTOM_COUNTS("PasswordManager.BlacklistedSites",
285 blacklisted_sites, 0, 32, 6);
287 sql::Statement usage_statement(db_.GetCachedStatement(
289 "SELECT password_type, times_used FROM logins"));
291 if (!usage_statement.is_valid())
294 while (usage_statement.Step()) {
295 PasswordForm::Type type = static_cast<PasswordForm::Type>(
296 usage_statement.ColumnInt(0));
298 if (type == PasswordForm::TYPE_GENERATED) {
299 UMA_HISTOGRAM_CUSTOM_COUNTS(
300 "PasswordManager.TimesGeneratedPasswordUsed",
301 usage_statement.ColumnInt(1), 0, 100, 10);
303 UMA_HISTOGRAM_CUSTOM_COUNTS(
304 "PasswordManager.TimesPasswordUsed",
305 usage_statement.ColumnInt(1), 0, 100, 10);
310 bool LoginDatabase::AddLogin(const PasswordForm& form) {
311 std::string encrypted_password;
312 if (EncryptedString(form.password_value, &encrypted_password) !=
313 ENCRYPTION_RESULT_SUCCESS)
316 // You *must* change LoginTableColumns if this query changes.
317 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE,
318 "INSERT OR REPLACE INTO logins "
319 "(origin_url, action_url, username_element, username_value, "
320 " password_element, password_value, submit_element, "
321 " signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
322 " scheme, password_type, possible_usernames, times_used, form_data) "
324 "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"));
325 s.BindString(COLUMN_ORIGIN_URL, form.origin.spec());
326 s.BindString(COLUMN_ACTION_URL, form.action.spec());
327 s.BindString16(COLUMN_USERNAME_ELEMENT, form.username_element);
328 s.BindString16(COLUMN_USERNAME_VALUE, form.username_value);
329 s.BindString16(COLUMN_PASSWORD_ELEMENT, form.password_element);
330 s.BindBlob(COLUMN_PASSWORD_VALUE, encrypted_password.data(),
331 static_cast<int>(encrypted_password.length()));
332 s.BindString16(COLUMN_SUBMIT_ELEMENT, form.submit_element);
333 s.BindString(COLUMN_SIGNON_REALM, form.signon_realm);
334 s.BindInt(COLUMN_SSL_VALID, form.ssl_valid);
335 s.BindInt(COLUMN_PREFERRED, form.preferred);
336 s.BindInt64(COLUMN_DATE_CREATED, form.date_created.ToTimeT());
337 s.BindInt(COLUMN_BLACKLISTED_BY_USER, form.blacklisted_by_user);
338 s.BindInt(COLUMN_SCHEME, form.scheme);
339 s.BindInt(COLUMN_PASSWORD_TYPE, form.type);
340 Pickle usernames_pickle = SerializeVector(form.other_possible_usernames);
341 s.BindBlob(COLUMN_POSSIBLE_USERNAMES,
342 usernames_pickle.data(),
343 usernames_pickle.size());
344 s.BindInt(COLUMN_TIMES_USED, form.times_used);
345 Pickle form_data_pickle;
346 autofill::SerializeFormData(form.form_data, &form_data_pickle);
347 s.BindBlob(COLUMN_FORM_DATA,
348 form_data_pickle.data(),
349 form_data_pickle.size());
354 bool LoginDatabase::UpdateLogin(const PasswordForm& form, int* items_changed) {
355 std::string encrypted_password;
356 if (EncryptedString(form.password_value, &encrypted_password) !=
357 ENCRYPTION_RESULT_SUCCESS)
360 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE,
363 "password_value = ?, "
366 "possible_usernames = ?, "
368 "WHERE origin_url = ? AND "
369 "username_element = ? AND "
370 "username_value = ? AND "
371 "password_element = ? AND "
372 "signon_realm = ?"));
373 s.BindString(0, form.action.spec());
374 s.BindBlob(1, encrypted_password.data(),
375 static_cast<int>(encrypted_password.length()));
376 s.BindInt(2, form.ssl_valid);
377 s.BindInt(3, form.preferred);
378 Pickle pickle = SerializeVector(form.other_possible_usernames);
379 s.BindBlob(4, pickle.data(), pickle.size());
380 s.BindInt(5, form.times_used);
381 s.BindString(6, form.origin.spec());
382 s.BindString16(7, form.username_element);
383 s.BindString16(8, form.username_value);
384 s.BindString16(9, form.password_element);
385 s.BindString(10, form.signon_realm);
391 *items_changed = db_.GetLastChangeCount();
396 bool LoginDatabase::RemoveLogin(const PasswordForm& form) {
397 // Remove a login by UNIQUE-constrained fields.
398 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE,
399 "DELETE FROM logins WHERE "
400 "origin_url = ? AND "
401 "username_element = ? AND "
402 "username_value = ? AND "
403 "password_element = ? AND "
404 "submit_element = ? AND "
405 "signon_realm = ? "));
406 s.BindString(0, form.origin.spec());
407 s.BindString16(1, form.username_element);
408 s.BindString16(2, form.username_value);
409 s.BindString16(3, form.password_element);
410 s.BindString16(4, form.submit_element);
411 s.BindString(5, form.signon_realm);
416 bool LoginDatabase::RemoveLoginsCreatedBetween(const base::Time delete_begin,
417 const base::Time delete_end) {
418 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE,
419 "DELETE FROM logins WHERE "
420 "date_created >= ? AND date_created < ?"));
421 s.BindInt64(0, delete_begin.ToTimeT());
422 s.BindInt64(1, delete_end.is_null() ? std::numeric_limits<int64>::max()
423 : delete_end.ToTimeT());
428 LoginDatabase::EncryptionResult LoginDatabase::InitPasswordFormFromStatement(
430 sql::Statement& s) const {
431 std::string encrypted_password;
432 s.ColumnBlobAsString(COLUMN_PASSWORD_VALUE, &encrypted_password);
433 string16 decrypted_password;
434 EncryptionResult encryption_result =
435 DecryptedString(encrypted_password, &decrypted_password);
436 if (encryption_result != ENCRYPTION_RESULT_SUCCESS)
437 return encryption_result;
439 std::string tmp = s.ColumnString(COLUMN_ORIGIN_URL);
440 form->origin = GURL(tmp);
441 tmp = s.ColumnString(COLUMN_ACTION_URL);
442 form->action = GURL(tmp);
443 form->username_element = s.ColumnString16(COLUMN_USERNAME_ELEMENT);
444 form->username_value = s.ColumnString16(COLUMN_USERNAME_VALUE);
445 form->password_element = s.ColumnString16(COLUMN_PASSWORD_ELEMENT);
446 form->password_value = decrypted_password;
447 form->submit_element = s.ColumnString16(COLUMN_SUBMIT_ELEMENT);
448 tmp = s.ColumnString(COLUMN_SIGNON_REALM);
449 form->signon_realm = tmp;
450 form->ssl_valid = (s.ColumnInt(COLUMN_SSL_VALID) > 0);
451 form->preferred = (s.ColumnInt(COLUMN_PREFERRED) > 0);
452 form->date_created = base::Time::FromTimeT(
453 s.ColumnInt64(COLUMN_DATE_CREATED));
454 form->blacklisted_by_user = (s.ColumnInt(COLUMN_BLACKLISTED_BY_USER) > 0);
455 int scheme_int = s.ColumnInt(COLUMN_SCHEME);
456 DCHECK((scheme_int >= 0) && (scheme_int <= PasswordForm::SCHEME_OTHER));
457 form->scheme = static_cast<PasswordForm::Scheme>(scheme_int);
458 int type_int = s.ColumnInt(COLUMN_PASSWORD_TYPE);
459 DCHECK(type_int >= 0 && type_int <= PasswordForm::TYPE_GENERATED);
460 form->type = static_cast<PasswordForm::Type>(type_int);
462 static_cast<const char*>(s.ColumnBlob(COLUMN_POSSIBLE_USERNAMES)),
463 s.ColumnByteLength(COLUMN_POSSIBLE_USERNAMES));
464 form->other_possible_usernames = DeserializeVector(pickle);
465 form->times_used = s.ColumnInt(COLUMN_TIMES_USED);
466 Pickle form_data_pickle(
467 static_cast<const char*>(s.ColumnBlob(COLUMN_FORM_DATA)),
468 s.ColumnByteLength(COLUMN_FORM_DATA));
469 PickleIterator form_data_iter(form_data_pickle);
470 autofill::DeserializeFormData(&form_data_iter, &form->form_data);
471 return ENCRYPTION_RESULT_SUCCESS;
474 bool LoginDatabase::GetLogins(const PasswordForm& form,
475 std::vector<PasswordForm*>* forms) const {
477 // You *must* change LoginTableColumns if this query changes.
478 const std::string sql_query = "SELECT origin_url, action_url, "
479 "username_element, username_value, "
480 "password_element, password_value, submit_element, "
481 "signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
482 "scheme, password_type, possible_usernames, times_used, form_data "
483 "FROM logins WHERE signon_realm == ? ";
485 const GURL signon_realm(form.signon_realm);
486 std::string registered_domain = GetRegistryControlledDomain(signon_realm);
487 PSLDomainMatchMetric psl_domain_match_metric = PSL_DOMAIN_MATCH_NONE;
488 if (public_suffix_domain_matching_ &&
489 ShouldPSLDomainMatchingApply(registered_domain)) {
490 // We are extending the original SQL query with one that includes more
491 // possible matches based on public suffix domain matching. Using a regexp
492 // here is just an optimization to not have to parse all the stored entries
493 // in the |logins| table. The result (scheme, domain and port) is verified
494 // further down using GURL. See the functions SchemeMatches,
495 // RegistryControlledDomainMatches and PortMatches.
496 const std::string extended_sql_query =
497 sql_query + "OR signon_realm REGEXP ? ";
498 // TODO(nyquist) Re-enable usage of GetCachedStatement when
499 // http://crbug.com/248608 is fixed.
500 s.Assign(db_.GetUniqueStatement(extended_sql_query.c_str()));
501 // We need to escape . in the domain. Since the domain has already been
502 // sanitized using GURL, we do not need to escape any other characters.
503 ReplaceChars(registered_domain, ".", "\\.", ®istered_domain);
504 std::string scheme = signon_realm.scheme();
505 // We need to escape . in the scheme. Since the scheme has already been
506 // sanitized using GURL, we do not need to escape any other characters.
507 // The scheme soap.beep is an example with '.'.
508 ReplaceChars(scheme, ".", "\\.", &scheme);
509 const std::string port = signon_realm.port();
510 // For a signon realm such as http://foo.bar/, this regexp will match
511 // domains on the form http://foo.bar/, http://www.foo.bar/,
512 // http://www.mobile.foo.bar/. It will not match http://notfoo.bar/.
513 // The scheme and port has to be the same as the observed form.
514 std::string regexp = "^(" + scheme + ":\\/\\/)([\\w-]+\\.)*" +
515 registered_domain + "(:" + port + ")?\\/$";
516 s.BindString(0, form.signon_realm);
517 s.BindString(1, regexp);
519 psl_domain_match_metric = PSL_DOMAIN_MATCH_DISABLED;
520 s.Assign(db_.GetCachedStatement(SQL_FROM_HERE, sql_query.c_str()));
521 s.BindString(0, form.signon_realm);
525 scoped_ptr<PasswordForm> new_form(new PasswordForm());
526 EncryptionResult result = InitPasswordFormFromStatement(new_form.get(), s);
527 if (result == ENCRYPTION_RESULT_SERVICE_FAILURE)
529 if (result == ENCRYPTION_RESULT_ITEM_FAILURE)
531 DCHECK(result == ENCRYPTION_RESULT_SUCCESS);
532 if (public_suffix_domain_matching_) {
533 if (!SchemeMatches(new_form, form) ||
534 !RegistryControlledDomainMatches(new_form, form) ||
535 !PortMatches(new_form, form)) {
536 // The database returned results that should not match. Skipping result.
539 if (form.signon_realm != new_form->signon_realm) {
540 psl_domain_match_metric = PSL_DOMAIN_MATCH_FOUND;
541 // This is not a perfect match, so we need to create a new valid result.
542 // We do this by copying over origin, signon realm and action from the
543 // observed form and setting the original signon realm to what we found
544 // in the database. We use the fact that |original_signon_realm| is
545 // non-empty to communicate that this match was found using public
547 new_form->original_signon_realm = new_form->signon_realm;
548 new_form->origin = form.origin;
549 new_form->signon_realm = form.signon_realm;
550 new_form->action = form.action;
553 forms->push_back(new_form.release());
555 UMA_HISTOGRAM_ENUMERATION("PasswordManager.PslDomainMatchTriggering",
556 psl_domain_match_metric,
557 PSL_DOMAIN_MATCH_COUNT);
558 return s.Succeeded();
561 bool LoginDatabase::GetLoginsCreatedBetween(
562 const base::Time begin,
563 const base::Time end,
564 std::vector<autofill::PasswordForm*>* forms) const {
566 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE,
567 "SELECT origin_url, action_url, "
568 "username_element, username_value, "
569 "password_element, password_value, submit_element, "
570 "signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
571 "scheme, password_type, possible_usernames, times_used, form_data "
572 "FROM logins WHERE date_created >= ? AND date_created < ?"
573 "ORDER BY origin_url"));
574 s.BindInt64(0, begin.ToTimeT());
575 s.BindInt64(1, end.is_null() ? std::numeric_limits<int64>::max()
579 scoped_ptr<PasswordForm> new_form(new PasswordForm());
580 EncryptionResult result = InitPasswordFormFromStatement(new_form.get(), s);
581 if (result == ENCRYPTION_RESULT_SERVICE_FAILURE)
583 if (result == ENCRYPTION_RESULT_ITEM_FAILURE)
585 DCHECK(result == ENCRYPTION_RESULT_SUCCESS);
586 forms->push_back(new_form.release());
588 return s.Succeeded();
591 bool LoginDatabase::GetAutofillableLogins(
592 std::vector<PasswordForm*>* forms) const {
593 return GetAllLoginsWithBlacklistSetting(false, forms);
596 bool LoginDatabase::GetBlacklistLogins(
597 std::vector<PasswordForm*>* forms) const {
598 return GetAllLoginsWithBlacklistSetting(true, forms);
601 bool LoginDatabase::GetAllLoginsWithBlacklistSetting(
602 bool blacklisted, std::vector<PasswordForm*>* forms) const {
604 // You *must* change LoginTableColumns if this query changes.
605 sql::Statement s(db_.GetCachedStatement(SQL_FROM_HERE,
606 "SELECT origin_url, action_url, "
607 "username_element, username_value, "
608 "password_element, password_value, submit_element, "
609 "signon_realm, ssl_valid, preferred, date_created, blacklisted_by_user, "
610 "scheme, password_type, possible_usernames, times_used, form_data "
611 "FROM logins WHERE blacklisted_by_user == ? "
612 "ORDER BY origin_url"));
613 s.BindInt(0, blacklisted ? 1 : 0);
616 scoped_ptr<PasswordForm> new_form(new PasswordForm());
617 EncryptionResult result = InitPasswordFormFromStatement(new_form.get(), s);
618 if (result == ENCRYPTION_RESULT_SERVICE_FAILURE)
620 if (result == ENCRYPTION_RESULT_ITEM_FAILURE)
622 DCHECK(result == ENCRYPTION_RESULT_SUCCESS);
623 forms->push_back(new_form.release());
625 return s.Succeeded();
628 bool LoginDatabase::DeleteAndRecreateDatabaseFile() {
629 DCHECK(db_.is_open());
632 sql::Connection::Delete(db_path_);
633 return Init(db_path_);
636 Pickle LoginDatabase::SerializeVector(const std::vector<string16>& vec) const {
638 for (size_t i = 0; i < vec.size(); ++i) {
639 p.WriteString16(vec[i]);
644 std::vector<string16> LoginDatabase::DeserializeVector(const Pickle& p) const {
645 std::vector<string16> ret;
648 PickleIterator iterator(p);
649 while (iterator.ReadString16(&str)) {