Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(970)

Side by Side Diff: chrome/browser/password_manager/password_store_mac_unittest.cc

Issue 2909283002: Delete PasswordStoreMac and SimplePasswordStoreMac. (Closed)
Patch Set: test Created 3 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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.
4
5 #include "chrome/browser/password_manager/password_store_mac.h"
6
7 #include <stddef.h>
8
9 #include <string>
10
11 #include "base/files/scoped_temp_dir.h"
12 #include "base/macros.h"
13 #include "base/memory/ptr_util.h"
14 #include "base/message_loop/message_loop.h"
15 #include "base/run_loop.h"
16 #include "base/scoped_observer.h"
17 #include "base/stl_util.h"
18 #include "base/strings/string_util.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "base/synchronization/waitable_event.h"
21 #include "base/test/histogram_tester.h"
22 #include "base/threading/thread_task_runner_handle.h"
23 #include "chrome/browser/password_manager/password_store_mac_internal.h"
24 #include "chrome/common/chrome_paths.h"
25 #include "components/os_crypt/os_crypt_mocker.h"
26 #include "components/password_manager/core/browser/login_database.h"
27 #include "components/password_manager/core/browser/password_manager_test_utils.h "
28 #include "components/password_manager/core/browser/password_manager_util.h"
29 #include "components/password_manager/core/browser/password_store_consumer.h"
30 #include "components/password_manager/core/browser/password_store_origin_unittes t.h"
31 #include "content/public/browser/browser_thread.h"
32 #include "content/public/test/test_browser_thread_bundle.h"
33 #include "content/public/test/test_utils.h"
34 #include "crypto/mock_apple_keychain.h"
35 #include "testing/gmock/include/gmock/gmock.h"
36 #include "testing/gtest/include/gtest/gtest.h"
37 #include "url/origin.h"
38
39 using autofill::PasswordForm;
40 using base::ASCIIToUTF16;
41 using base::WideToUTF16;
42 using content::BrowserThread;
43 using crypto::MockAppleKeychain;
44 using internal_keychain_helpers::FormsMatchForMerge;
45 using internal_keychain_helpers::STRICT_FORM_MATCH;
46 using password_manager::CreatePasswordFormFromDataForTesting;
47 using password_manager::LoginDatabase;
48 using password_manager::PasswordFormData;
49 using password_manager::PasswordStore;
50 using password_manager::PasswordStoreChange;
51 using password_manager::PasswordStoreChangeList;
52 using password_manager::PasswordStoreConsumer;
53 using testing::_;
54 using testing::DoAll;
55 using testing::Invoke;
56 using testing::IsEmpty;
57 using testing::SizeIs;
58 using testing::WithArg;
59
60 namespace {
61
62 ACTION(QuitUIMessageLoop) {
63 DCHECK_CURRENTLY_ON(BrowserThread::UI);
64 base::MessageLoop::current()->QuitWhenIdle();
65 }
66
67 // From the mock's argument #0 of type const std::vector<PasswordForm*>& takes
68 // the first form and copies it to the form pointed to by |target_form_ptr|.
69 ACTION_P(SaveACopyOfFirstForm, target_form_ptr) {
70 ASSERT_FALSE(arg0.empty());
71 *target_form_ptr = *arg0[0];
72 }
73
74 void Noop() {}
75
76 class MockPasswordStoreConsumer : public PasswordStoreConsumer {
77 public:
78 MOCK_METHOD1(OnGetPasswordStoreResultsConstRef,
79 void(const std::vector<std::unique_ptr<PasswordForm>>&));
80
81 // GMock cannot mock methods with move-only args.
82 void OnGetPasswordStoreResults(
83 std::vector<std::unique_ptr<PasswordForm>> results) override {
84 OnGetPasswordStoreResultsConstRef(results);
85 }
86 };
87
88 // A LoginDatabase that simulates an Init() method that takes a long time.
89 class SlowToInitLoginDatabase : public password_manager::LoginDatabase {
90 public:
91 // Creates an instance whose Init() method will block until |event| is
92 // signaled. |event| must outlive |this|.
93 SlowToInitLoginDatabase(const base::FilePath& db_path,
94 base::WaitableEvent* event)
95 : password_manager::LoginDatabase(db_path), event_(event) {}
96 ~SlowToInitLoginDatabase() override {}
97
98 // LoginDatabase:
99 bool Init() override {
100 event_->Wait();
101 return password_manager::LoginDatabase::Init();
102 }
103
104 private:
105 base::WaitableEvent* event_;
106
107 DISALLOW_COPY_AND_ASSIGN(SlowToInitLoginDatabase);
108 };
109
110 #pragma mark -
111
112 // Macro to simplify calling CheckFormsAgainstExpectations with a useful label.
113 #define CHECK_FORMS(forms, expectations, i) \
114 CheckFormsAgainstExpectations(forms, expectations, #forms, i)
115
116 // Ensures that the data in |forms| match |expectations|, causing test failures
117 // for any discrepencies.
118 // TODO(stuartmorgan): This is current order-dependent; ideally it shouldn't
119 // matter if |forms| and |expectations| are scrambled.
120 void CheckFormsAgainstExpectations(
121 const std::vector<std::unique_ptr<PasswordForm>>& forms,
122 const std::vector<PasswordFormData*>& expectations,
123
124 const char* forms_label,
125 unsigned int test_number) {
126 EXPECT_EQ(expectations.size(), forms.size()) << forms_label << " in test "
127 << test_number;
128 if (expectations.size() != forms.size())
129 return;
130
131 for (unsigned int i = 0; i < expectations.size(); ++i) {
132 SCOPED_TRACE(testing::Message() << forms_label << " in test " << test_number
133 << ", item " << i);
134 PasswordForm* form = forms[i].get();
135 PasswordFormData* expectation = expectations[i];
136 EXPECT_EQ(expectation->scheme, form->scheme);
137 EXPECT_EQ(std::string(expectation->signon_realm), form->signon_realm);
138 EXPECT_EQ(GURL(expectation->origin), form->origin);
139 EXPECT_EQ(GURL(expectation->action), form->action);
140 EXPECT_EQ(WideToUTF16(expectation->submit_element), form->submit_element);
141 EXPECT_EQ(WideToUTF16(expectation->username_element),
142 form->username_element);
143 EXPECT_EQ(WideToUTF16(expectation->password_element),
144 form->password_element);
145 if (expectation->username_value) {
146 EXPECT_EQ(WideToUTF16(expectation->username_value), form->username_value);
147 EXPECT_EQ(WideToUTF16(expectation->username_value), form->display_name);
148 EXPECT_TRUE(form->skip_zero_click);
149 if (expectation->password_value &&
150 wcscmp(expectation->password_value,
151 password_manager::kTestingFederatedLoginMarker) == 0) {
152 EXPECT_TRUE(form->password_value.empty());
153 EXPECT_EQ(
154 url::Origin(GURL(password_manager::kTestingFederationUrlSpec)),
155 form->federation_origin);
156 } else {
157 EXPECT_EQ(WideToUTF16(expectation->password_value),
158 form->password_value);
159 EXPECT_TRUE(form->federation_origin.unique());
160 }
161 } else {
162 EXPECT_TRUE(form->blacklisted_by_user);
163 }
164 EXPECT_EQ(expectation->preferred, form->preferred);
165 EXPECT_DOUBLE_EQ(expectation->creation_time,
166 form->date_created.ToDoubleT());
167 base::Time created = base::Time::FromDoubleT(expectation->creation_time);
168 EXPECT_EQ(
169 created + base::TimeDelta::FromDays(
170 password_manager::kTestingDaysAfterPasswordsAreSynced),
171 form->date_synced);
172 EXPECT_EQ(GURL(password_manager::kTestingIconUrlSpec), form->icon_url);
173 }
174 }
175
176 PasswordStoreChangeList AddChangeForForm(const PasswordForm& form) {
177 return PasswordStoreChangeList(
178 1, PasswordStoreChange(PasswordStoreChange::ADD, form));
179 }
180
181 class PasswordStoreMacTestDelegate {
182 public:
183 PasswordStoreMacTestDelegate();
184 ~PasswordStoreMacTestDelegate();
185
186 PasswordStoreMac* store() { return store_.get(); }
187
188 static void FinishAsyncProcessing();
189
190 private:
191 void Initialize();
192
193 void ClosePasswordStore();
194
195 base::FilePath test_login_db_file_path() const;
196
197 base::MessageLoopForUI message_loop_;
198 base::ScopedTempDir db_dir_;
199 std::unique_ptr<LoginDatabase> login_db_;
200 scoped_refptr<PasswordStoreMac> store_;
201
202 DISALLOW_COPY_AND_ASSIGN(PasswordStoreMacTestDelegate);
203 };
204
205 PasswordStoreMacTestDelegate::PasswordStoreMacTestDelegate() {
206 Initialize();
207 }
208
209 PasswordStoreMacTestDelegate::~PasswordStoreMacTestDelegate() {
210 ClosePasswordStore();
211 }
212
213 void PasswordStoreMacTestDelegate::FinishAsyncProcessing() {
214 base::RunLoop().RunUntilIdle();
215 }
216
217 void PasswordStoreMacTestDelegate::Initialize() {
218 ASSERT_TRUE(db_dir_.CreateUniqueTempDir());
219
220 // Ensure that LoginDatabase will use the mock keychain if it needs to
221 // encrypt/decrypt a password.
222 OSCryptMocker::SetUpWithSingleton();
223 login_db_.reset(new LoginDatabase(test_login_db_file_path()));
224 ASSERT_TRUE(login_db_->Init());
225
226 // Create and initialize the password store.
227 store_ = new PasswordStoreMac(base::ThreadTaskRunnerHandle::Get(),
228 base::ThreadTaskRunnerHandle::Get(),
229 base::WrapUnique(new MockAppleKeychain));
230 store_->set_login_metadata_db(login_db_.get());
231 store_->login_metadata_db()->set_clear_password_values(false);
232 }
233
234 void PasswordStoreMacTestDelegate::ClosePasswordStore() {
235 store_->ShutdownOnUIThread();
236 FinishAsyncProcessing();
237 OSCryptMocker::TearDown();
238 }
239
240 base::FilePath PasswordStoreMacTestDelegate::test_login_db_file_path() const {
241 return db_dir_.GetPath().Append(FILE_PATH_LITERAL("login.db"));
242 }
243
244 } // namespace
245
246 namespace password_manager {
247
248 INSTANTIATE_TYPED_TEST_CASE_P(Mac,
249 PasswordStoreOriginTest,
250 PasswordStoreMacTestDelegate);
251
252 } // namespace password_manager
253
254 #pragma mark -
255
256 class PasswordStoreMacInternalsTest : public testing::Test {
257 public:
258 void SetUp() override {
259 MockAppleKeychain::KeychainTestData test_data[] = {
260 // Basic HTML form.
261 {kSecAuthenticationTypeHTMLForm, "some.domain.com",
262 kSecProtocolTypeHTTP, NULL, 0, NULL, "20020601171500Z", "joe_user",
263 "sekrit", false},
264 // HTML form with path.
265 {kSecAuthenticationTypeHTMLForm, "some.domain.com",
266 kSecProtocolTypeHTTP, "/insecure.html", 0, NULL, "19991231235959Z",
267 "joe_user", "sekrit", false},
268 // Secure HTML form with path.
269 {kSecAuthenticationTypeHTMLForm, "some.domain.com",
270 kSecProtocolTypeHTTPS, "/secure.html", 0, NULL, "20100908070605Z",
271 "secure_user", "password", false},
272 // True negative item.
273 {kSecAuthenticationTypeHTMLForm, "dont.remember.com",
274 kSecProtocolTypeHTTP, NULL, 0, NULL, "20000101000000Z", "", "", true},
275 // De-facto negative item, type one.
276 {kSecAuthenticationTypeHTMLForm, "dont.remember.com",
277 kSecProtocolTypeHTTP, NULL, 0, NULL, "20000101000000Z",
278 "Password Not Stored", "", false},
279 // De-facto negative item, type two.
280 {kSecAuthenticationTypeHTMLForm, "dont.remember.com",
281 kSecProtocolTypeHTTPS, NULL, 0, NULL, "20000101000000Z",
282 "Password Not Stored", " ", false},
283 // HTTP auth basic, with port and path.
284 {kSecAuthenticationTypeHTTPBasic, "some.domain.com",
285 kSecProtocolTypeHTTP, "/insecure.html", 4567, "low_security",
286 "19980330100000Z", "basic_auth_user", "basic", false},
287 // HTTP auth digest, secure.
288 {kSecAuthenticationTypeHTTPDigest, "some.domain.com",
289 kSecProtocolTypeHTTPS, NULL, 0, "high_security", "19980330100000Z",
290 "digest_auth_user", "digest", false},
291 // An FTP password with an invalid date, for edge-case testing.
292 {kSecAuthenticationTypeDefault, "a.server.com", kSecProtocolTypeFTP,
293 NULL, 0, NULL, "20010203040", "abc", "123", false},
294 // Password for an Android application.
295 {kSecAuthenticationTypeHTMLForm, "android://hash@com.domain.some/",
296 kSecProtocolTypeHTTPS, "", 0, NULL, "20150515141312Z", "joe_user",
297 "secret", false},
298 };
299
300 keychain_ = new MockAppleKeychain();
301
302 for (unsigned int i = 0; i < arraysize(test_data); ++i) {
303 keychain_->AddTestItem(test_data[i]);
304 }
305 }
306
307 void TearDown() override {
308 ExpectCreatesAndFreesBalanced();
309 ExpectCreatorCodesSet();
310 delete keychain_;
311 }
312
313 protected:
314 // Causes a test failure unless everything returned from keychain_'s
315 // ItemCopyAttributesAndData, SearchCreateFromAttributes, and SearchCopyNext
316 // was correctly freed.
317 void ExpectCreatesAndFreesBalanced() {
318 EXPECT_EQ(0, keychain_->UnfreedSearchCount());
319 EXPECT_EQ(0, keychain_->UnfreedKeychainItemCount());
320 EXPECT_EQ(0, keychain_->UnfreedAttributeDataCount());
321 }
322
323 // Causes a test failure unless any Keychain items added during the test have
324 // their creator code set.
325 void ExpectCreatorCodesSet() {
326 EXPECT_TRUE(keychain_->CreatorCodesSetForAddedItems());
327 }
328
329 MockAppleKeychain* keychain_;
330 };
331
332 #pragma mark -
333
334 TEST_F(PasswordStoreMacInternalsTest, TestKeychainToFormTranslation) {
335 typedef struct {
336 const PasswordForm::Scheme scheme;
337 const char* signon_realm;
338 const char* origin;
339 const wchar_t* username; // Set to NULL to check for a blacklist entry.
340 const wchar_t* password;
341 const int creation_year;
342 const int creation_month;
343 const int creation_day;
344 const int creation_hour;
345 const int creation_minute;
346 const int creation_second;
347 } TestExpectations;
348
349 TestExpectations expected[] = {
350 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
351 "http://some.domain.com/", L"joe_user", L"sekrit", 2002, 6, 1, 17, 15,
352 0},
353 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
354 "http://some.domain.com/insecure.html", L"joe_user", L"sekrit", 1999, 12,
355 31, 23, 59, 59},
356 {PasswordForm::SCHEME_HTML, "https://some.domain.com/",
357 "https://some.domain.com/secure.html", L"secure_user", L"password", 2010,
358 9, 8, 7, 6, 5},
359 {PasswordForm::SCHEME_HTML, "http://dont.remember.com/",
360 "http://dont.remember.com/", NULL, NULL, 2000, 1, 1, 0, 0, 0},
361 {PasswordForm::SCHEME_HTML, "http://dont.remember.com/",
362 "http://dont.remember.com/", NULL, NULL, 2000, 1, 1, 0, 0, 0},
363 {PasswordForm::SCHEME_HTML, "https://dont.remember.com/",
364 "https://dont.remember.com/", NULL, NULL, 2000, 1, 1, 0, 0, 0},
365 {PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security",
366 "http://some.domain.com:4567/insecure.html", L"basic_auth_user",
367 L"basic", 1998, 03, 30, 10, 00, 00},
368 {PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security",
369 "https://some.domain.com/", L"digest_auth_user", L"digest", 1998, 3, 30,
370 10, 0, 0},
371 // This one gives us an invalid date, which we will treat as a "NULL" date
372 // which is 1601.
373 {PasswordForm::SCHEME_OTHER, "http://a.server.com/",
374 "http://a.server.com/", L"abc", L"123", 1601, 1, 1, 0, 0, 0},
375 {PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", "",
376 L"joe_user", L"secret", 2015, 5, 15, 14, 13, 12},
377 };
378
379 for (unsigned int i = 0; i < arraysize(expected); ++i) {
380 SCOPED_TRACE(testing::Message("In iteration ") << i);
381 // Create our fake KeychainItemRef; see MockAppleKeychain docs.
382 SecKeychainItemRef keychain_item =
383 reinterpret_cast<SecKeychainItemRef>(i + 1);
384 PasswordForm form;
385 bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
386 *keychain_, keychain_item, &form, true);
387
388 EXPECT_TRUE(parsed);
389
390 EXPECT_EQ(expected[i].scheme, form.scheme);
391 EXPECT_EQ(GURL(expected[i].origin), form.origin);
392 EXPECT_EQ(std::string(expected[i].signon_realm), form.signon_realm);
393 if (expected[i].username) {
394 EXPECT_EQ(WideToUTF16(expected[i].username), form.username_value);
395 EXPECT_EQ(WideToUTF16(expected[i].password), form.password_value);
396 EXPECT_FALSE(form.blacklisted_by_user);
397 } else {
398 EXPECT_TRUE(form.blacklisted_by_user);
399 }
400 base::Time::Exploded exploded_time;
401 form.date_created.UTCExplode(&exploded_time);
402 EXPECT_EQ(expected[i].creation_year, exploded_time.year);
403 EXPECT_EQ(expected[i].creation_month, exploded_time.month);
404 EXPECT_EQ(expected[i].creation_day, exploded_time.day_of_month);
405 EXPECT_EQ(expected[i].creation_hour, exploded_time.hour);
406 EXPECT_EQ(expected[i].creation_minute, exploded_time.minute);
407 EXPECT_EQ(expected[i].creation_second, exploded_time.second);
408 }
409
410 {
411 // Use an invalid ref, to make sure errors are reported.
412 SecKeychainItemRef keychain_item = reinterpret_cast<SecKeychainItemRef>(99);
413 PasswordForm form;
414 bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
415 *keychain_, keychain_item, &form, true);
416 EXPECT_FALSE(parsed);
417 }
418 }
419
420 TEST_F(PasswordStoreMacInternalsTest, TestKeychainSearch) {
421 struct TestDataAndExpectation {
422 const PasswordFormData data;
423 const size_t expected_fill_matches;
424 const size_t expected_merge_matches;
425 };
426 // Most fields are left blank because we don't care about them for searching.
427 TestDataAndExpectation test_data[] = {
428 // An HTML form we've seen.
429 {{PasswordForm::SCHEME_HTML, "http://some.domain.com/", NULL, NULL, NULL,
430 NULL, NULL, L"joe_user", NULL, false, 0},
431 2,
432 2},
433 {{PasswordForm::SCHEME_HTML, "http://some.domain.com/", NULL, NULL, NULL,
434 NULL, NULL, L"wrong_user", NULL, false, 0},
435 2,
436 0},
437 // An HTML form we haven't seen
438 {{PasswordForm::SCHEME_HTML, "http://www.unseendomain.com/", NULL, NULL,
439 NULL, NULL, NULL, L"joe_user", NULL, false, 0},
440 0,
441 0},
442 // Basic auth that should match.
443 {{PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security",
444 NULL, NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, false, 0},
445 1,
446 1},
447 // Basic auth with the wrong port.
448 {{PasswordForm::SCHEME_BASIC, "http://some.domain.com:1111/low_security",
449 NULL, NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, false, 0},
450 0,
451 0},
452 // Digest auth we've saved under https, visited with http.
453 {{PasswordForm::SCHEME_DIGEST, "http://some.domain.com/high_security",
454 NULL, NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, false, 0},
455 0,
456 0},
457 // Digest auth that should match.
458 {{PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security",
459 NULL, NULL, NULL, NULL, NULL, L"wrong_user", NULL, false, 0},
460 1,
461 0},
462 // Digest auth with the wrong domain.
463 {{PasswordForm::SCHEME_DIGEST, "https://some.domain.com/other_domain",
464 NULL, NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, false, 0},
465 0,
466 0},
467 // Android credentials (both legacy ones with origin, and without).
468 {{PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/",
469 "android://hash@com.domain.some/", NULL, NULL, NULL, NULL, L"joe_user",
470 NULL, false, 0},
471 1,
472 1},
473 {{PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", NULL,
474 NULL, NULL, NULL, NULL, L"joe_user", NULL, false, 0},
475 1,
476 1},
477 // Federated logins do not have a corresponding Keychain entry, and should
478 // not match the username/password stored for the same application. Note
479 // that it will match for filling, however, because that part does not
480 // know
481 // that it is a federated login.
482 {{PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", NULL,
483 NULL, NULL, NULL, NULL, L"joe_user",
484 password_manager::kTestingFederatedLoginMarker, false, 0},
485 1,
486 0},
487 /// Garbage forms should have no matches.
488 {{PasswordForm::SCHEME_HTML, "foo/bar/baz", NULL, NULL, NULL, NULL, NULL,
489 NULL, NULL, false, 0},
490 0,
491 0},
492 };
493
494 MacKeychainPasswordFormAdapter keychain_adapter(keychain_);
495 MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_);
496 owned_keychain_adapter.SetFindsOnlyOwnedItems(true);
497 for (unsigned int i = 0; i < arraysize(test_data); ++i) {
498 std::unique_ptr<PasswordForm> query_form =
499 CreatePasswordFormFromDataForTesting(test_data[i].data);
500
501 // Check matches treating the form as a fill target.
502 std::vector<std::unique_ptr<PasswordForm>> matching_items =
503 keychain_adapter.PasswordsFillingForm(query_form->signon_realm,
504 query_form->scheme);
505 EXPECT_EQ(test_data[i].expected_fill_matches, matching_items.size());
506
507 // Check matches treating the form as a merging target.
508 EXPECT_EQ(test_data[i].expected_merge_matches > 0,
509 keychain_adapter.HasPasswordsMergeableWithForm(*query_form));
510 std::vector<SecKeychainItemRef> keychain_items;
511 std::vector<internal_keychain_helpers::ItemFormPair> item_form_pairs =
512 internal_keychain_helpers::
513 ExtractAllKeychainItemAttributesIntoPasswordForms(&keychain_items,
514 *keychain_);
515 matching_items =
516 internal_keychain_helpers::ExtractPasswordsMergeableWithForm(
517 *keychain_, item_form_pairs, *query_form);
518 EXPECT_EQ(test_data[i].expected_merge_matches, matching_items.size());
519 for (std::vector<SecKeychainItemRef>::iterator i = keychain_items.begin();
520 i != keychain_items.end(); ++i) {
521 keychain_->Free(*i);
522 }
523
524 // None of the pre-seeded items are owned by us, so none should match an
525 // owned-passwords-only search.
526 matching_items = owned_keychain_adapter.PasswordsFillingForm(
527 query_form->signon_realm, query_form->scheme);
528 EXPECT_EQ(0U, matching_items.size());
529 }
530 }
531
532 // Changes just the origin path of |form|.
533 static void SetPasswordFormPath(PasswordForm* form, const char* path) {
534 GURL::Replacements replacement;
535 std::string new_value(path);
536 replacement.SetPathStr(new_value);
537 form->origin = form->origin.ReplaceComponents(replacement);
538 }
539
540 // Changes just the signon_realm port of |form|.
541 static void SetPasswordFormPort(PasswordForm* form, const char* port) {
542 GURL::Replacements replacement;
543 std::string new_value(port);
544 replacement.SetPortStr(new_value);
545 GURL signon_gurl = GURL(form->signon_realm);
546 form->signon_realm = signon_gurl.ReplaceComponents(replacement).spec();
547 }
548
549 // Changes just the signon_ream auth realm of |form|.
550 static void SetPasswordFormRealm(PasswordForm* form, const char* realm) {
551 GURL::Replacements replacement;
552 std::string new_value(realm);
553 replacement.SetPathStr(new_value);
554 GURL signon_gurl = GURL(form->signon_realm);
555 form->signon_realm = signon_gurl.ReplaceComponents(replacement).spec();
556 }
557
558 TEST_F(PasswordStoreMacInternalsTest, TestKeychainExactSearch) {
559 MacKeychainPasswordFormAdapter keychain_adapter(keychain_);
560
561 PasswordFormData base_form_data[] = {
562 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
563 "http://some.domain.com/insecure.html", NULL, NULL, NULL, NULL,
564 L"joe_user", NULL, true, 0},
565 {PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security",
566 "http://some.domain.com:4567/insecure.html", NULL, NULL, NULL, NULL,
567 L"basic_auth_user", NULL, true, 0},
568 {PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security",
569 "https://some.domain.com", NULL, NULL, NULL, NULL, L"digest_auth_user",
570 NULL, true, 0},
571 };
572
573 for (unsigned int i = 0; i < arraysize(base_form_data); ++i) {
574 // Create a base form and make sure we find a match.
575 std::unique_ptr<PasswordForm> base_form =
576 CreatePasswordFormFromDataForTesting(base_form_data[i]);
577 EXPECT_TRUE(keychain_adapter.HasPasswordsMergeableWithForm(*base_form));
578 EXPECT_TRUE(keychain_adapter.HasPasswordExactlyMatchingForm(*base_form));
579
580 // Make sure that the matching isn't looser than it should be by checking
581 // that slightly altered forms don't match.
582 std::vector<std::unique_ptr<PasswordForm>> modified_forms;
583
584 modified_forms.push_back(base::MakeUnique<PasswordForm>(*base_form));
585 modified_forms.back()->username_value = ASCIIToUTF16("wrong_user");
586
587 modified_forms.push_back(base::MakeUnique<PasswordForm>(*base_form));
588 SetPasswordFormPath(modified_forms.back().get(), "elsewhere.html");
589
590 modified_forms.push_back(base::MakeUnique<PasswordForm>(*base_form));
591 modified_forms.back()->scheme = PasswordForm::SCHEME_OTHER;
592
593 modified_forms.push_back(base::MakeUnique<PasswordForm>(*base_form));
594 SetPasswordFormPort(modified_forms.back().get(), "1234");
595
596 modified_forms.push_back(base::MakeUnique<PasswordForm>(*base_form));
597 modified_forms.back()->blacklisted_by_user = true;
598
599 if (base_form->scheme == PasswordForm::SCHEME_BASIC ||
600 base_form->scheme == PasswordForm::SCHEME_DIGEST) {
601 modified_forms.push_back(base::MakeUnique<PasswordForm>(*base_form));
602 SetPasswordFormRealm(modified_forms.back().get(), "incorrect");
603 }
604
605 for (unsigned int j = 0; j < modified_forms.size(); ++j) {
606 bool match =
607 keychain_adapter.HasPasswordExactlyMatchingForm(*modified_forms[j]);
608 EXPECT_FALSE(match) << "In modified version " << j << " of base form "
609 << i;
610 }
611 }
612 }
613
614 TEST_F(PasswordStoreMacInternalsTest, TestKeychainAdd) {
615 struct TestDataAndExpectation {
616 PasswordFormData data;
617 bool should_succeed;
618 };
619 TestDataAndExpectation test_data[] = {
620 // Test a variety of scheme/port/protocol/path variations.
621 {{PasswordForm::SCHEME_HTML, "http://web.site.com/",
622 "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL,
623 L"anonymous", L"knock-knock", false, 0},
624 true},
625 {{PasswordForm::SCHEME_HTML, "https://web.site.com/",
626 "https://web.site.com/", NULL, NULL, NULL, NULL, L"admin", L"p4ssw0rd",
627 false, 0},
628 true},
629 {{PasswordForm::SCHEME_BASIC, "http://a.site.com:2222/therealm",
630 "http://a.site.com:2222/", NULL, NULL, NULL, NULL, L"username",
631 L"password", false, 0},
632 true},
633 {{PasswordForm::SCHEME_DIGEST, "https://digest.site.com/differentrealm",
634 "https://digest.site.com/secure.html", NULL, NULL, NULL, NULL,
635 L"testname", L"testpass", false, 0},
636 true},
637 // Test that Android credentials can be stored. Also check the legacy form
638 // when |origin| was still filled with the Android URI (and not left
639 // empty).
640 {{PasswordForm::SCHEME_HTML, "android://hash@com.example.alpha/", "",
641 NULL, NULL, NULL, NULL, L"joe_user", L"password", false, 0},
642 true},
643 {{PasswordForm::SCHEME_HTML, "android://hash@com.example.beta/",
644 "android://hash@com.example.beta/", NULL, NULL, NULL, NULL,
645 L"jane_user", L"password2", false, 0},
646 true},
647 // Make sure that garbage forms are rejected.
648 {{PasswordForm::SCHEME_HTML, "gobbledygook", "gobbledygook", NULL, NULL,
649 NULL, NULL, L"anonymous", L"knock-knock", false, 0},
650 false},
651 // Test that failing to update a duplicate (forced using the magic failure
652 // password; see MockAppleKeychain::ItemModifyAttributesAndData) is
653 // reported.
654 {{PasswordForm::SCHEME_HTML, "http://some.domain.com",
655 "http://some.domain.com/insecure.html", NULL, NULL, NULL, NULL,
656 L"joe_user", L"fail_me", false, 0},
657 false},
658 };
659
660 MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_);
661 owned_keychain_adapter.SetFindsOnlyOwnedItems(true);
662
663 for (unsigned int i = 0; i < arraysize(test_data); ++i) {
664 std::unique_ptr<PasswordForm> in_form =
665 CreatePasswordFormFromDataForTesting(test_data[i].data);
666 bool add_succeeded = owned_keychain_adapter.AddPassword(*in_form);
667 EXPECT_EQ(test_data[i].should_succeed, add_succeeded);
668 if (add_succeeded) {
669 EXPECT_TRUE(
670 owned_keychain_adapter.HasPasswordsMergeableWithForm(*in_form));
671 EXPECT_TRUE(
672 owned_keychain_adapter.HasPasswordExactlyMatchingForm(*in_form));
673 }
674 }
675
676 // Test that adding duplicate item updates the existing item.
677 // TODO(engedy): Add a test to verify that updating Android credentials work.
678 // See: https://crbug.com/476851.
679 {
680 PasswordFormData data = {PasswordForm::SCHEME_HTML,
681 "http://some.domain.com",
682 "http://some.domain.com/insecure.html",
683 NULL,
684 NULL,
685 NULL,
686 NULL,
687 L"joe_user",
688 L"updated_password",
689 false,
690 0};
691 std::unique_ptr<PasswordForm> update_form =
692 CreatePasswordFormFromDataForTesting(data);
693 MacKeychainPasswordFormAdapter keychain_adapter(keychain_);
694 EXPECT_TRUE(keychain_adapter.AddPassword(*update_form));
695 SecKeychainItemRef keychain_item = reinterpret_cast<SecKeychainItemRef>(2);
696 PasswordForm stored_form;
697 internal_keychain_helpers::FillPasswordFormFromKeychainItem(
698 *keychain_, keychain_item, &stored_form, true);
699 EXPECT_EQ(update_form->password_value, stored_form.password_value);
700 }
701 }
702
703 TEST_F(PasswordStoreMacInternalsTest, TestKeychainRemove) {
704 struct TestDataAndExpectation {
705 PasswordFormData data;
706 bool should_succeed;
707 };
708 TestDataAndExpectation test_data[] = {
709 // Test deletion of an item that we add.
710 {{PasswordForm::SCHEME_HTML, "http://web.site.com/",
711 "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL,
712 L"anonymous", L"knock-knock", false, 0},
713 true},
714 // Test that Android credentials can be removed. Also check the legacy
715 // case when |origin| was still filled with the Android URI (and not left
716 // empty).
717 {{PasswordForm::SCHEME_HTML, "android://hash@com.example.alpha/", "",
718 NULL, NULL, NULL, NULL, L"joe_user", L"secret", false, 0},
719 true},
720 {{PasswordForm::SCHEME_HTML, "android://hash@com.example.beta/",
721 "android://hash@com.example.beta/", NULL, NULL, NULL, NULL,
722 L"jane_user", L"secret", false, 0},
723 true},
724 // Make sure we don't delete items we don't own.
725 {{PasswordForm::SCHEME_HTML, "http://some.domain.com/",
726 "http://some.domain.com/insecure.html", NULL, NULL, NULL, NULL,
727 L"joe_user", NULL, true, 0},
728 false},
729 };
730
731 MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_);
732 owned_keychain_adapter.SetFindsOnlyOwnedItems(true);
733
734 // Add our test items (except the last one) so that we can delete them.
735 for (unsigned int i = 0; i + 1 < arraysize(test_data); ++i) {
736 std::unique_ptr<PasswordForm> add_form =
737 CreatePasswordFormFromDataForTesting(test_data[i].data);
738 EXPECT_TRUE(owned_keychain_adapter.AddPassword(*add_form));
739 }
740
741 for (unsigned int i = 0; i < arraysize(test_data); ++i) {
742 std::unique_ptr<PasswordForm> form =
743 CreatePasswordFormFromDataForTesting(test_data[i].data);
744 EXPECT_EQ(test_data[i].should_succeed,
745 owned_keychain_adapter.RemovePassword(*form));
746
747 MacKeychainPasswordFormAdapter keychain_adapter(keychain_);
748 bool match = keychain_adapter.HasPasswordExactlyMatchingForm(*form);
749 EXPECT_EQ(test_data[i].should_succeed, !match);
750 }
751 }
752
753 TEST_F(PasswordStoreMacInternalsTest, TestFormMatch) {
754 PasswordForm base_form;
755 base_form.signon_realm = std::string("http://some.domain.com/");
756 base_form.origin = GURL("http://some.domain.com/page.html");
757 base_form.username_value = ASCIIToUTF16("joe_user");
758
759 {
760 // Check that everything unimportant can be changed.
761 PasswordForm different_form(base_form);
762 different_form.username_element = ASCIIToUTF16("username");
763 different_form.submit_element = ASCIIToUTF16("submit");
764 different_form.username_element = ASCIIToUTF16("password");
765 different_form.password_value = ASCIIToUTF16("sekrit");
766 different_form.action = GURL("http://some.domain.com/action.cgi");
767 different_form.preferred = true;
768 different_form.date_created = base::Time::Now();
769 EXPECT_TRUE(
770 FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH));
771
772 // Check that path differences don't prevent a match.
773 base_form.origin = GURL("http://some.domain.com/other_page.html");
774 EXPECT_TRUE(
775 FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH));
776 }
777
778 // Check that any one primary key changing is enough to prevent matching.
779 {
780 PasswordForm different_form(base_form);
781 different_form.scheme = PasswordForm::SCHEME_DIGEST;
782 EXPECT_FALSE(
783 FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH));
784 }
785 {
786 PasswordForm different_form(base_form);
787 different_form.signon_realm = std::string("http://some.domain.com:8080/");
788 EXPECT_FALSE(
789 FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH));
790 }
791 {
792 PasswordForm different_form(base_form);
793 different_form.username_value = ASCIIToUTF16("john.doe");
794 EXPECT_FALSE(
795 FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH));
796 }
797 {
798 PasswordForm different_form(base_form);
799 different_form.blacklisted_by_user = true;
800 EXPECT_FALSE(
801 FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH));
802 }
803
804 // Blacklist forms should *never* match for merging, even when identical
805 // (and certainly not when only one is a blacklist entry).
806 {
807 PasswordForm form_a(base_form);
808 form_a.blacklisted_by_user = true;
809 PasswordForm form_b(form_a);
810 EXPECT_FALSE(FormsMatchForMerge(form_a, form_b, STRICT_FORM_MATCH));
811 }
812
813 // Federated login forms should never match for merging either.
814 {
815 PasswordForm form_b(base_form);
816 form_b.federation_origin =
817 url::Origin(GURL(password_manager::kTestingFederationUrlSpec));
818 EXPECT_FALSE(FormsMatchForMerge(base_form, form_b, STRICT_FORM_MATCH));
819 EXPECT_FALSE(FormsMatchForMerge(form_b, base_form, STRICT_FORM_MATCH));
820 EXPECT_FALSE(FormsMatchForMerge(form_b, form_b, STRICT_FORM_MATCH));
821 }
822 }
823
824 TEST_F(PasswordStoreMacInternalsTest, TestFormMerge) {
825 // Set up a bunch of test data to use in varying combinations.
826 PasswordFormData keychain_user_1 = {PasswordForm::SCHEME_HTML,
827 "http://some.domain.com/",
828 "http://some.domain.com/",
829 "",
830 L"",
831 L"",
832 L"",
833 L"joe_user",
834 L"sekrit",
835 false,
836 1010101010};
837 PasswordFormData keychain_user_1_with_path = {
838 PasswordForm::SCHEME_HTML,
839 "http://some.domain.com/",
840 "http://some.domain.com/page.html",
841 "",
842 L"",
843 L"",
844 L"",
845 L"joe_user",
846 L"otherpassword",
847 false,
848 1010101010};
849 PasswordFormData keychain_user_2 = {PasswordForm::SCHEME_HTML,
850 "http://some.domain.com/",
851 "http://some.domain.com/",
852 "",
853 L"",
854 L"",
855 L"",
856 L"john.doe",
857 L"sesame",
858 false,
859 958739876};
860 PasswordFormData keychain_blacklist = {PasswordForm::SCHEME_HTML,
861 "http://some.domain.com/",
862 "http://some.domain.com/",
863 "",
864 L"",
865 L"",
866 L"",
867 NULL,
868 NULL,
869 false,
870 1010101010};
871 PasswordFormData keychain_android = {PasswordForm::SCHEME_HTML,
872 "android://hash@com.domain.some/",
873 "",
874 "",
875 L"",
876 L"",
877 L"",
878 L"joe_user",
879 L"secret",
880 false,
881 1234567890};
882
883 PasswordFormData db_user_1 = {PasswordForm::SCHEME_HTML,
884 "http://some.domain.com/",
885 "http://some.domain.com/",
886 "http://some.domain.com/action.cgi",
887 L"submit",
888 L"username",
889 L"password",
890 L"joe_user",
891 L"",
892 true,
893 1212121212};
894 PasswordFormData db_user_1_with_path = {
895 PasswordForm::SCHEME_HTML,
896 "http://some.domain.com/",
897 "http://some.domain.com/page.html",
898 "http://some.domain.com/handlepage.cgi",
899 L"submit",
900 L"username",
901 L"password",
902 L"joe_user",
903 L"",
904 true,
905 1234567890};
906 PasswordFormData db_user_3_with_path = {
907 PasswordForm::SCHEME_HTML,
908 "http://some.domain.com/",
909 "http://some.domain.com/page.html",
910 "http://some.domain.com/handlepage.cgi",
911 L"submit",
912 L"username",
913 L"password",
914 L"second-account",
915 L"",
916 true,
917 1240000000};
918 PasswordFormData database_blacklist_with_path = {
919 PasswordForm::SCHEME_HTML,
920 "http://some.domain.com/",
921 "http://some.domain.com/path.html",
922 "http://some.domain.com/action.cgi",
923 L"submit",
924 L"username",
925 L"password",
926 NULL,
927 NULL,
928 true,
929 1212121212};
930 PasswordFormData db_android = {PasswordForm::SCHEME_HTML,
931 "android://hash@com.domain.some/",
932 "android://hash@com.domain.some/",
933 "",
934 L"",
935 L"",
936 L"",
937 L"joe_user",
938 L"",
939 false,
940 1234567890};
941 PasswordFormData db_federated = {
942 PasswordForm::SCHEME_HTML,
943 "android://hash@com.domain.some/",
944 "android://hash@com.domain.some/",
945 "",
946 L"",
947 L"",
948 L"",
949 L"joe_user",
950 password_manager::kTestingFederatedLoginMarker,
951 false,
952 3434343434};
953
954 PasswordFormData merged_user_1 = {PasswordForm::SCHEME_HTML,
955 "http://some.domain.com/",
956 "http://some.domain.com/",
957 "http://some.domain.com/action.cgi",
958 L"submit",
959 L"username",
960 L"password",
961 L"joe_user",
962 L"sekrit",
963 true,
964 1212121212};
965 PasswordFormData merged_user_1_with_db_path = {
966 PasswordForm::SCHEME_HTML,
967 "http://some.domain.com/",
968 "http://some.domain.com/page.html",
969 "http://some.domain.com/handlepage.cgi",
970 L"submit",
971 L"username",
972 L"password",
973 L"joe_user",
974 L"sekrit",
975 true,
976 1234567890};
977 PasswordFormData merged_user_1_with_both_paths = {
978 PasswordForm::SCHEME_HTML,
979 "http://some.domain.com/",
980 "http://some.domain.com/page.html",
981 "http://some.domain.com/handlepage.cgi",
982 L"submit",
983 L"username",
984 L"password",
985 L"joe_user",
986 L"otherpassword",
987 true,
988 1234567890};
989 PasswordFormData merged_android = {PasswordForm::SCHEME_HTML,
990 "android://hash@com.domain.some/",
991 "android://hash@com.domain.some/",
992 "",
993 L"",
994 L"",
995 L"",
996 L"joe_user",
997 L"secret",
998 false,
999 1234567890};
1000
1001 // Build up the big multi-dimensional array of data sets that will actually
1002 // drive the test. Use vectors rather than arrays so that initialization is
1003 // simple.
1004 enum {
1005 KEYCHAIN_INPUT = 0,
1006 DATABASE_INPUT,
1007 MERGE_OUTPUT,
1008 KEYCHAIN_OUTPUT,
1009 DATABASE_OUTPUT,
1010 MERGE_IO_ARRAY_COUNT // termination marker
1011 };
1012 const unsigned int kTestCount = 5;
1013 std::vector<std::vector<std::vector<PasswordFormData*>>> test_data(
1014 MERGE_IO_ARRAY_COUNT, std::vector<std::vector<PasswordFormData*>>(
1015 kTestCount, std::vector<PasswordFormData*>()));
1016 unsigned int current_test = 0;
1017
1018 // Test a merge with a few accounts in both systems, with partial overlap.
1019 CHECK(current_test < kTestCount);
1020 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1);
1021 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_2);
1022 test_data[DATABASE_INPUT][current_test].push_back(&db_user_1);
1023 test_data[DATABASE_INPUT][current_test].push_back(&db_user_1_with_path);
1024 test_data[DATABASE_INPUT][current_test].push_back(&db_user_3_with_path);
1025 test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1);
1026 test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1_with_db_path);
1027 test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_user_2);
1028 test_data[DATABASE_OUTPUT][current_test].push_back(&db_user_3_with_path);
1029
1030 // Test a merge where Chrome has a blacklist entry, and the keychain has
1031 // a stored account.
1032 ++current_test;
1033 CHECK(current_test < kTestCount);
1034 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1);
1035 test_data[DATABASE_INPUT][current_test].push_back(
1036 &database_blacklist_with_path);
1037 // We expect both to be present because a blacklist could be specific to a
1038 // subpath, and we want access to the password on other paths.
1039 test_data[MERGE_OUTPUT][current_test].push_back(
1040 &database_blacklist_with_path);
1041 test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_user_1);
1042
1043 // Test a merge where Chrome has an account, and Keychain has a blacklist
1044 // (from another browser) and the Chrome password data.
1045 ++current_test;
1046 CHECK(current_test < kTestCount);
1047 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_blacklist);
1048 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1);
1049 test_data[DATABASE_INPUT][current_test].push_back(&db_user_1);
1050 test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1);
1051 test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_blacklist);
1052
1053 // Test that matches are done using exact path when possible.
1054 ++current_test;
1055 CHECK(current_test < kTestCount);
1056 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1);
1057 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1_with_path);
1058 test_data[DATABASE_INPUT][current_test].push_back(&db_user_1);
1059 test_data[DATABASE_INPUT][current_test].push_back(&db_user_1_with_path);
1060 test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1);
1061 test_data[MERGE_OUTPUT][current_test].push_back(
1062 &merged_user_1_with_both_paths);
1063
1064 // Test that Android credentails are matched correctly and that federated
1065 // credentials are not tried to be matched with a Keychain item.
1066 ++current_test;
1067 CHECK(current_test < kTestCount);
1068 test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_android);
1069 test_data[DATABASE_INPUT][current_test].push_back(&db_federated);
1070 test_data[DATABASE_INPUT][current_test].push_back(&db_android);
1071 test_data[MERGE_OUTPUT][current_test].push_back(&db_federated);
1072 test_data[MERGE_OUTPUT][current_test].push_back(&merged_android);
1073
1074 for (unsigned int test_case = 0; test_case <= current_test; ++test_case) {
1075 std::vector<std::unique_ptr<PasswordForm>> keychain_forms;
1076 for (std::vector<PasswordFormData*>::iterator i =
1077 test_data[KEYCHAIN_INPUT][test_case].begin();
1078 i != test_data[KEYCHAIN_INPUT][test_case].end(); ++i) {
1079 keychain_forms.push_back(CreatePasswordFormFromDataForTesting(*(*i)));
1080 }
1081 std::vector<std::unique_ptr<PasswordForm>> database_forms;
1082 for (std::vector<PasswordFormData*>::iterator i =
1083 test_data[DATABASE_INPUT][test_case].begin();
1084 i != test_data[DATABASE_INPUT][test_case].end(); ++i) {
1085 database_forms.push_back(CreatePasswordFormFromDataForTesting(*(*i)));
1086 }
1087
1088 std::vector<std::unique_ptr<PasswordForm>> merged_forms;
1089 internal_keychain_helpers::MergePasswordForms(
1090 &keychain_forms, &database_forms, &merged_forms);
1091
1092 CHECK_FORMS(keychain_forms, test_data[KEYCHAIN_OUTPUT][test_case],
1093 test_case);
1094 CHECK_FORMS(database_forms, test_data[DATABASE_OUTPUT][test_case],
1095 test_case);
1096 CHECK_FORMS(merged_forms, test_data[MERGE_OUTPUT][test_case], test_case);
1097 }
1098 }
1099
1100 TEST_F(PasswordStoreMacInternalsTest, TestPasswordBulkLookup) {
1101 PasswordFormData db_data[] = {
1102 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
1103 "http://some.domain.com/", "http://some.domain.com/action.cgi",
1104 L"submit", L"username", L"password", L"joe_user", L"", true, 1212121212},
1105 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
1106 "http://some.domain.com/page.html",
1107 "http://some.domain.com/handlepage.cgi", L"submit", L"username",
1108 L"password", L"joe_user", L"", true, 1234567890},
1109 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
1110 "http://some.domain.com/page.html",
1111 "http://some.domain.com/handlepage.cgi", L"submit", L"username",
1112 L"password", L"second-account", L"", true, 1240000000},
1113 {PasswordForm::SCHEME_HTML, "http://dont.remember.com/",
1114 "http://dont.remember.com/", "http://dont.remember.com/handlepage.cgi",
1115 L"submit", L"username", L"password", L"joe_user", L"", true, 1240000000},
1116 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
1117 "http://some.domain.com/path.html", "http://some.domain.com/action.cgi",
1118 L"submit", L"username", L"password", NULL, NULL, true, 1212121212},
1119 };
1120 std::vector<std::unique_ptr<PasswordForm>> database_forms;
1121 for (const PasswordFormData& form_data : db_data) {
1122 database_forms.push_back(CreatePasswordFormFromDataForTesting(form_data));
1123 }
1124 std::vector<std::unique_ptr<PasswordForm>> merged_forms;
1125 internal_keychain_helpers::GetPasswordsForForms(*keychain_, &database_forms,
1126 &merged_forms);
1127 EXPECT_EQ(2U, database_forms.size());
1128 ASSERT_EQ(3U, merged_forms.size());
1129 EXPECT_EQ(ASCIIToUTF16("sekrit"), merged_forms[0]->password_value);
1130 EXPECT_EQ(ASCIIToUTF16("sekrit"), merged_forms[1]->password_value);
1131 EXPECT_TRUE(merged_forms[2]->blacklisted_by_user);
1132 }
1133
1134 TEST_F(PasswordStoreMacInternalsTest, TestBlacklistedFiltering) {
1135 PasswordFormData db_data[] = {
1136 {PasswordForm::SCHEME_HTML, "http://dont.remember.com/",
1137 "http://dont.remember.com/", "http://dont.remember.com/handlepage.cgi",
1138 L"submit", L"username", L"password", L"joe_user", L"non_empty_password",
1139 true, 1240000000},
1140 {PasswordForm::SCHEME_HTML, "https://dont.remember.com/",
1141 "https://dont.remember.com/",
1142 "https://dont.remember.com/handlepage_secure.cgi", L"submit",
1143 L"username", L"password", L"joe_user", L"non_empty_password", true,
1144 1240000000},
1145 };
1146 std::vector<std::unique_ptr<PasswordForm>> database_forms;
1147 for (const PasswordFormData& form_data : db_data) {
1148 database_forms.push_back(CreatePasswordFormFromDataForTesting(form_data));
1149 }
1150 std::vector<std::unique_ptr<PasswordForm>> merged_forms;
1151 internal_keychain_helpers::GetPasswordsForForms(*keychain_, &database_forms,
1152 &merged_forms);
1153 EXPECT_EQ(2U, database_forms.size());
1154 ASSERT_EQ(0U, merged_forms.size());
1155 }
1156
1157 TEST_F(PasswordStoreMacInternalsTest, TestFillPasswordFormFromKeychainItem) {
1158 // When |extract_password_data| is false, the password field must be empty,
1159 // and |blacklisted_by_user| must be false.
1160 SecKeychainItemRef keychain_item = reinterpret_cast<SecKeychainItemRef>(1);
1161 PasswordForm form_without_extracted_password;
1162 bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
1163 *keychain_, keychain_item, &form_without_extracted_password,
1164 false); // Do not extract password.
1165 EXPECT_TRUE(parsed);
1166 ASSERT_TRUE(form_without_extracted_password.password_value.empty());
1167 ASSERT_FALSE(form_without_extracted_password.blacklisted_by_user);
1168
1169 // When |extract_password_data| is true and the keychain entry has a non-empty
1170 // password, the password field must be non-empty, and the value of
1171 // |blacklisted_by_user| must be false.
1172 keychain_item = reinterpret_cast<SecKeychainItemRef>(1);
1173 PasswordForm form_with_extracted_password;
1174 parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
1175 *keychain_, keychain_item, &form_with_extracted_password,
1176 true); // Extract password.
1177 EXPECT_TRUE(parsed);
1178 ASSERT_EQ(ASCIIToUTF16("sekrit"),
1179 form_with_extracted_password.password_value);
1180 ASSERT_FALSE(form_with_extracted_password.blacklisted_by_user);
1181
1182 // When |extract_password_data| is true and the keychain entry has an empty
1183 // username and password (""), the password field must be empty, and the value
1184 // of |blacklisted_by_user| must be true.
1185 keychain_item = reinterpret_cast<SecKeychainItemRef>(4);
1186 PasswordForm negative_form;
1187 parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
1188 *keychain_, keychain_item, &negative_form,
1189 true); // Extract password.
1190 EXPECT_TRUE(parsed);
1191 ASSERT_TRUE(negative_form.username_value.empty());
1192 ASSERT_TRUE(negative_form.password_value.empty());
1193 ASSERT_TRUE(negative_form.blacklisted_by_user);
1194
1195 // When |extract_password_data| is true and the keychain entry has an empty
1196 // password (""), the password field must be empty (""), and the value of
1197 // |blacklisted_by_user| must be true.
1198 keychain_item = reinterpret_cast<SecKeychainItemRef>(5);
1199 PasswordForm form_with_empty_password_a;
1200 parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
1201 *keychain_, keychain_item, &form_with_empty_password_a,
1202 true); // Extract password.
1203 EXPECT_TRUE(parsed);
1204 ASSERT_TRUE(form_with_empty_password_a.password_value.empty());
1205 ASSERT_TRUE(form_with_empty_password_a.blacklisted_by_user);
1206
1207 // When |extract_password_data| is true and the keychain entry has a single
1208 // space password (" "), the password field must be a single space (" "), and
1209 // the value of |blacklisted_by_user| must be true.
1210 keychain_item = reinterpret_cast<SecKeychainItemRef>(6);
1211 PasswordForm form_with_empty_password_b;
1212 parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem(
1213 *keychain_, keychain_item, &form_with_empty_password_b,
1214 true); // Extract password.
1215 EXPECT_TRUE(parsed);
1216 ASSERT_EQ(ASCIIToUTF16(" "), form_with_empty_password_b.password_value);
1217 ASSERT_TRUE(form_with_empty_password_b.blacklisted_by_user);
1218 }
1219
1220 TEST_F(PasswordStoreMacInternalsTest, TestPasswordGetAll) {
1221 MacKeychainPasswordFormAdapter keychain_adapter(keychain_);
1222 MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_);
1223 owned_keychain_adapter.SetFindsOnlyOwnedItems(true);
1224
1225 // Add a few passwords of various types so that we own some.
1226 PasswordFormData owned_password_data[] = {
1227 {PasswordForm::SCHEME_HTML, "http://web.site.com/",
1228 "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL,
1229 L"anonymous", L"knock-knock", false, 0},
1230 {PasswordForm::SCHEME_BASIC, "http://a.site.com:2222/therealm",
1231 "http://a.site.com:2222/", NULL, NULL, NULL, NULL, L"username",
1232 L"password", false, 0},
1233 {PasswordForm::SCHEME_DIGEST, "https://digest.site.com/differentrealm",
1234 "https://digest.site.com/secure.html", NULL, NULL, NULL, NULL,
1235 L"testname", L"testpass", false, 0},
1236 };
1237 for (unsigned int i = 0; i < arraysize(owned_password_data); ++i) {
1238 std::unique_ptr<PasswordForm> form =
1239 CreatePasswordFormFromDataForTesting(owned_password_data[i]);
1240 owned_keychain_adapter.AddPassword(*form);
1241 }
1242
1243 std::vector<std::unique_ptr<PasswordForm>> all_passwords =
1244 keychain_adapter.GetAllPasswordFormPasswords();
1245 EXPECT_EQ(9 + arraysize(owned_password_data), all_passwords.size());
1246
1247 std::vector<std::unique_ptr<PasswordForm>> owned_passwords =
1248 owned_keychain_adapter.GetAllPasswordFormPasswords();
1249 EXPECT_EQ(arraysize(owned_password_data), owned_passwords.size());
1250 }
1251
1252 #pragma mark -
1253
1254 class PasswordStoreMacTest : public testing::Test {
1255 public:
1256 PasswordStoreMacTest() = default;
1257
1258 void SetUp() override {
1259 ASSERT_TRUE(db_dir_.CreateUniqueTempDir());
1260 histogram_tester_.reset(new base::HistogramTester);
1261
1262 // Ensure that LoginDatabase will use the mock keychain if it needs to
1263 // encrypt/decrypt a password.
1264 OSCryptMocker::SetUpWithSingleton();
1265 login_db_.reset(
1266 new password_manager::LoginDatabase(test_login_db_file_path()));
1267 thread_.reset(new base::Thread("Chrome_PasswordStore_Thread"));
1268 ASSERT_TRUE(thread_->Start());
1269 ASSERT_TRUE(thread_->task_runner()->PostTask(
1270 FROM_HERE, base::Bind(&PasswordStoreMacTest::InitLoginDatabase,
1271 base::Unretained(login_db_.get()))));
1272 CreateAndInitPasswordStore(login_db_.get());
1273 // Make sure deferred initialization is performed before some tests start
1274 // accessing the |login_db| directly.
1275 FinishAsyncProcessing();
1276 }
1277
1278 void TearDown() override {
1279 ClosePasswordStore();
1280 thread_.reset();
1281 login_db_.reset();
1282 // Whatever a test did, PasswordStoreMac stores only empty password values
1283 // in LoginDatabase. The empty valus do not require encryption and therefore
1284 // OSCrypt shouldn't call the Keychain. The histogram doesn't cover the
1285 // internet passwords.
1286 if (histogram_tester_) {
1287 histogram_tester_->ExpectTotalCount("OSX.Keychain.Access", 0);
1288 }
1289 OSCryptMocker::TearDown();
1290 }
1291
1292 static void InitLoginDatabase(password_manager::LoginDatabase* login_db) {
1293 ASSERT_TRUE(login_db->Init());
1294 }
1295
1296 void CreateAndInitPasswordStore(password_manager::LoginDatabase* login_db) {
1297 store_ = new PasswordStoreMac(
1298 base::ThreadTaskRunnerHandle::Get(), nullptr,
1299 base::WrapUnique<AppleKeychain>(new MockAppleKeychain));
1300 ASSERT_TRUE(thread_->task_runner()->PostTask(
1301 FROM_HERE, base::Bind(&PasswordStoreMac::InitWithTaskRunner, store_,
1302 thread_->task_runner())));
1303
1304 ASSERT_TRUE(thread_->task_runner()->PostTask(
1305 FROM_HERE, base::Bind(&PasswordStoreMac::set_login_metadata_db, store_,
1306 base::Unretained(login_db))));
1307 }
1308
1309 void ClosePasswordStore() {
1310 if (!store_)
1311 return;
1312
1313 store_->ShutdownOnUIThread();
1314 store_ = nullptr;
1315 }
1316
1317 // Verifies that the given |form| can be properly stored so that it can be
1318 // retrieved by FillMatchingLogins() and GetAutofillableLogins(), and then it
1319 // can be properly removed.
1320 void VerifyCredentialLifecycle(const PasswordForm& form) {
1321 // Run everything twice to make sure no garbage is left behind that would
1322 // prevent storing the form a second time.
1323 for (size_t iteration = 0; iteration < 2; ++iteration) {
1324 SCOPED_TRACE(testing::Message("Iteration: ") << iteration);
1325
1326 MockPasswordStoreConsumer mock_consumer;
1327 EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef(IsEmpty()))
1328 .WillOnce(QuitUIMessageLoop());
1329 store()->GetAutofillableLogins(&mock_consumer);
1330 base::RunLoop().Run();
1331 ::testing::Mock::VerifyAndClearExpectations(&mock_consumer);
1332
1333 store()->AddLogin(form);
1334 FinishAsyncProcessing();
1335
1336 PasswordForm returned_form;
1337 EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef(SizeIs(1u)))
1338 .WillOnce(
1339 DoAll(SaveACopyOfFirstForm(&returned_form), QuitUIMessageLoop()));
1340
1341 // The query operations will also do some housekeeping: they will remove
1342 // dangling credentials in the LoginDatabase without a matching Keychain
1343 // item when one is expected. If the logic that stores the Keychain item
1344 // is incorrect, this will wipe the newly added form before the second
1345 // query.
1346 store()->GetAutofillableLogins(&mock_consumer);
1347 base::RunLoop().Run();
1348 ::testing::Mock::VerifyAndClearExpectations(&mock_consumer);
1349 EXPECT_EQ(form, returned_form);
1350
1351 PasswordStore::FormDigest query_form(form);
1352 EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef(SizeIs(1u)))
1353 .WillOnce(
1354 DoAll(SaveACopyOfFirstForm(&returned_form), QuitUIMessageLoop()));
1355 store()->GetLogins(query_form, &mock_consumer);
1356 base::RunLoop().Run();
1357 ::testing::Mock::VerifyAndClearExpectations(&mock_consumer);
1358 EXPECT_EQ(form, returned_form);
1359
1360 store()->RemoveLogin(form);
1361 }
1362 }
1363
1364 base::FilePath test_login_db_file_path() const {
1365 return db_dir_.GetPath().Append(FILE_PATH_LITERAL("login.db"));
1366 }
1367
1368 password_manager::LoginDatabase* login_db() const {
1369 return store_->login_metadata_db();
1370 }
1371
1372 MockAppleKeychain* keychain() {
1373 return static_cast<MockAppleKeychain*>(store_->keychain());
1374 }
1375
1376 void FinishAsyncProcessing() {
1377 scoped_refptr<content::MessageLoopRunner> runner =
1378 new content::MessageLoopRunner;
1379 ASSERT_TRUE(thread_->task_runner()->PostTaskAndReply(
1380 FROM_HERE, base::Bind(&Noop), runner->QuitClosure()));
1381 runner->Run();
1382 }
1383
1384 PasswordStoreMac* store() { return store_.get(); }
1385
1386 protected:
1387 content::TestBrowserThreadBundle test_browser_thread_bundle_;
1388 // Thread that the synchronous methods are run on.
1389 std::unique_ptr<base::Thread> thread_;
1390
1391 base::ScopedTempDir db_dir_;
1392 std::unique_ptr<password_manager::LoginDatabase> login_db_;
1393 scoped_refptr<PasswordStoreMac> store_;
1394 std::unique_ptr<base::HistogramTester> histogram_tester_;
1395 };
1396
1397 TEST_F(PasswordStoreMacTest, TestStoreUpdate) {
1398 // Insert a password into both the database and the keychain.
1399 // This is done manually, rather than through store_->AddLogin, because the
1400 // Mock Keychain isn't smart enough to be able to support update generically,
1401 // so some.domain.com triggers special handling to test it that make inserting
1402 // fail.
1403 PasswordFormData joint_data = {PasswordForm::SCHEME_HTML,
1404 "http://some.domain.com/",
1405 "http://some.domain.com/insecure.html",
1406 "login.cgi",
1407 L"username",
1408 L"password",
1409 L"submit",
1410 L"joe_user",
1411 L"sekrit",
1412 true,
1413 1};
1414 std::unique_ptr<PasswordForm> joint_form =
1415 CreatePasswordFormFromDataForTesting(joint_data);
1416 EXPECT_EQ(AddChangeForForm(*joint_form), login_db()->AddLogin(*joint_form));
1417 MockAppleKeychain::KeychainTestData joint_keychain_data = {
1418 kSecAuthenticationTypeHTMLForm,
1419 "some.domain.com",
1420 kSecProtocolTypeHTTP,
1421 "/insecure.html",
1422 0,
1423 NULL,
1424 "20020601171500Z",
1425 "joe_user",
1426 "sekrit",
1427 false};
1428 keychain()->AddTestItem(joint_keychain_data);
1429
1430 // Insert a password into the keychain only.
1431 MockAppleKeychain::KeychainTestData keychain_only_data = {
1432 kSecAuthenticationTypeHTMLForm,
1433 "keychain.only.com",
1434 kSecProtocolTypeHTTP,
1435 NULL,
1436 0,
1437 NULL,
1438 "20020601171500Z",
1439 "keychain",
1440 "only",
1441 false};
1442 keychain()->AddTestItem(keychain_only_data);
1443
1444 struct UpdateData {
1445 PasswordFormData form_data;
1446 const char* password; // NULL indicates no entry should be present.
1447 };
1448
1449 // Make a series of update calls.
1450 UpdateData updates[] = {
1451 // Update the keychain+db passwords (the normal password update case).
1452 {
1453 {PasswordForm::SCHEME_HTML, "http://some.domain.com/",
1454 "http://some.domain.com/insecure.html", "login.cgi", L"username",
1455 L"password", L"submit", L"joe_user", L"53krit", true, 2},
1456 "53krit",
1457 },
1458 // Update the keychain-only password; this simulates the initial use of a
1459 // password stored by another browsers.
1460 {
1461 {PasswordForm::SCHEME_HTML, "http://keychain.only.com/",
1462 "http://keychain.only.com/login.html", "login.cgi", L"username",
1463 L"password", L"submit", L"keychain", L"only", true, 2},
1464 "only",
1465 },
1466 // Update a password that doesn't exist in either location. This tests the
1467 // case where a form is filled, then the stored login is removed, then the
1468 // form is submitted.
1469 {
1470 {PasswordForm::SCHEME_HTML, "http://different.com/",
1471 "http://different.com/index.html", "login.cgi", L"username",
1472 L"password", L"submit", L"abc", L"123", true, 2},
1473 NULL,
1474 },
1475 };
1476 for (unsigned int i = 0; i < arraysize(updates); ++i) {
1477 std::unique_ptr<PasswordForm> form =
1478 CreatePasswordFormFromDataForTesting(updates[i].form_data);
1479 store_->UpdateLogin(*form);
1480 }
1481
1482 FinishAsyncProcessing();
1483
1484 MacKeychainPasswordFormAdapter keychain_adapter(keychain());
1485 for (unsigned int i = 0; i < arraysize(updates); ++i) {
1486 SCOPED_TRACE(testing::Message("iteration ") << i);
1487 std::unique_ptr<PasswordForm> query_form =
1488 CreatePasswordFormFromDataForTesting(updates[i].form_data);
1489
1490 std::vector<std::unique_ptr<PasswordForm>> matching_items =
1491 keychain_adapter.PasswordsFillingForm(query_form->signon_realm,
1492 query_form->scheme);
1493 if (updates[i].password) {
1494 EXPECT_GT(matching_items.size(), 0U);
1495 if (matching_items.size() >= 1)
1496 EXPECT_EQ(ASCIIToUTF16(updates[i].password),
1497 matching_items[0]->password_value);
1498 } else {
1499 EXPECT_EQ(0U, matching_items.size());
1500 }
1501
1502 std::vector<std::unique_ptr<PasswordForm>> matching_db_items;
1503 EXPECT_TRUE(login_db()->GetLogins(PasswordStore::FormDigest(*query_form),
1504 &matching_db_items));
1505 EXPECT_EQ(updates[i].password ? 1U : 0U, matching_db_items.size());
1506 }
1507 }
1508
1509 TEST_F(PasswordStoreMacTest, TestDBKeychainAssociation) {
1510 // Tests that association between the keychain and login database parts of a
1511 // password added by fuzzy (PSL) matching works.
1512 // 1. Add a password for www.facebook.com
1513 // 2. Get a password for m.facebook.com. This fuzzy matches and returns the
1514 // www.facebook.com password.
1515 // 3. Add the returned password for m.facebook.com.
1516 // 4. Remove both passwords.
1517 // -> check: that both are gone from the login DB and the keychain
1518 // This test should in particular ensure that we don't keep passwords in the
1519 // keychain just before we think we still have other (fuzzy-)matching entries
1520 // for them in the login database. (For example, here if we deleted the
1521 // www.facebook.com password from the login database, we should not be blocked
1522 // from deleting it from the keystore just becaus the m.facebook.com password
1523 // fuzzy-matches the www.facebook.com one.)
1524
1525 // 1. Add a password for www.facebook.com
1526 PasswordFormData www_form_data = {PasswordForm::SCHEME_HTML,
1527 "http://www.facebook.com/",
1528 "http://www.facebook.com/index.html",
1529 "login",
1530 L"username",
1531 L"password",
1532 L"submit",
1533 L"joe_user",
1534 L"sekrit",
1535 true,
1536 1};
1537 std::unique_ptr<PasswordForm> www_form =
1538 CreatePasswordFormFromDataForTesting(www_form_data);
1539 EXPECT_EQ(AddChangeForForm(*www_form), login_db()->AddLogin(*www_form));
1540 MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain());
1541 owned_keychain_adapter.SetFindsOnlyOwnedItems(true);
1542 owned_keychain_adapter.AddPassword(*www_form);
1543
1544 // 2. Get a password for m.facebook.com.
1545 PasswordForm m_form(*www_form);
1546 m_form.signon_realm = "http://m.facebook.com";
1547 m_form.origin = GURL("http://m.facebook.com/index.html");
1548
1549 MockPasswordStoreConsumer consumer;
1550 store_->GetLogins(PasswordStore::FormDigest(m_form), &consumer);
1551 PasswordForm returned_form;
1552 EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(SizeIs(1u)))
1553 .WillOnce(
1554 DoAll(SaveACopyOfFirstForm(&returned_form), QuitUIMessageLoop()));
1555 base::RunLoop().Run();
1556
1557 // 3. Add the returned password for m.facebook.com.
1558 returned_form.signon_realm = "http://m.facebook.com";
1559 returned_form.origin = GURL("http://m.facebook.com/index.html");
1560 EXPECT_EQ(AddChangeForForm(returned_form),
1561 login_db()->AddLogin(returned_form));
1562 owned_keychain_adapter.AddPassword(m_form);
1563
1564 // 4. Remove both passwords.
1565 store_->RemoveLogin(*www_form);
1566 store_->RemoveLogin(m_form);
1567 FinishAsyncProcessing();
1568
1569 // No trace of www.facebook.com.
1570 std::vector<std::unique_ptr<PasswordForm>> matching_items =
1571 owned_keychain_adapter.PasswordsFillingForm(www_form->signon_realm,
1572 www_form->scheme);
1573 EXPECT_EQ(0u, matching_items.size());
1574
1575 std::vector<std::unique_ptr<PasswordForm>> matching_db_items;
1576 EXPECT_TRUE(login_db()->GetLogins(PasswordStore::FormDigest(*www_form),
1577 &matching_db_items));
1578 EXPECT_EQ(0u, matching_db_items.size());
1579
1580 // No trace of m.facebook.com.
1581 matching_items = owned_keychain_adapter.PasswordsFillingForm(
1582 m_form.signon_realm, m_form.scheme);
1583 EXPECT_EQ(0u, matching_items.size());
1584
1585 EXPECT_TRUE(login_db()->GetLogins(PasswordStore::FormDigest(m_form),
1586 &matching_db_items));
1587 EXPECT_EQ(0u, matching_db_items.size());
1588 }
1589
1590 namespace {
1591
1592 class PasswordsChangeObserver
1593 : public password_manager::PasswordStore::Observer {
1594 public:
1595 explicit PasswordsChangeObserver(PasswordStoreMac* store) : observer_(this) {
1596 observer_.Add(store);
1597 }
1598
1599 void WaitAndVerify(PasswordStoreMacTest* test) {
1600 test->FinishAsyncProcessing();
1601 ::testing::Mock::VerifyAndClearExpectations(this);
1602 }
1603
1604 // password_manager::PasswordStore::Observer:
1605 MOCK_METHOD1(OnLoginsChanged,
1606 void(const password_manager::PasswordStoreChangeList& changes));
1607
1608 private:
1609 ScopedObserver<password_manager::PasswordStore, PasswordsChangeObserver>
1610 observer_;
1611 };
1612
1613 password_manager::PasswordStoreChangeList GetAddChangeList(
1614 const PasswordForm& form) {
1615 password_manager::PasswordStoreChange change(
1616 password_manager::PasswordStoreChange::ADD, form);
1617 return password_manager::PasswordStoreChangeList(1, change);
1618 }
1619
1620 // Tests RemoveLoginsCreatedBetween or RemoveLoginsSyncedBetween depending on
1621 // |check_created|.
1622 void CheckRemoveLoginsBetween(PasswordStoreMacTest* test, bool check_created) {
1623 PasswordFormData www_form_data_facebook = {
1624 PasswordForm::SCHEME_HTML,
1625 "http://www.facebook.com/",
1626 "http://www.facebook.com/index.html",
1627 "login",
1628 L"submit",
1629 L"username",
1630 L"password",
1631 L"joe_user",
1632 L"sekrit",
1633 true,
1634 0};
1635 // The old form doesn't have elements names.
1636 PasswordFormData www_form_data_facebook_old = {
1637 PasswordForm::SCHEME_HTML,
1638 "http://www.facebook.com/",
1639 "http://www.facebook.com/index.html",
1640 "login",
1641 L"",
1642 L"",
1643 L"",
1644 L"joe_user",
1645 L"oldsekrit",
1646 true,
1647 0};
1648 PasswordFormData www_form_data_other = {PasswordForm::SCHEME_HTML,
1649 "http://different.com/",
1650 "http://different.com/index.html",
1651 "login",
1652 L"submit",
1653 L"username",
1654 L"password",
1655 L"different_joe_user",
1656 L"sekrit",
1657 true,
1658 0};
1659 std::unique_ptr<PasswordForm> form_facebook =
1660 CreatePasswordFormFromDataForTesting(www_form_data_facebook);
1661 std::unique_ptr<PasswordForm> form_facebook_old =
1662 CreatePasswordFormFromDataForTesting(www_form_data_facebook_old);
1663 std::unique_ptr<PasswordForm> form_other =
1664 CreatePasswordFormFromDataForTesting(www_form_data_other);
1665 base::Time now = base::Time::Now();
1666 base::Time next_day = now + base::TimeDelta::FromDays(1);
1667 if (check_created) {
1668 form_facebook_old->date_created = now;
1669 form_facebook->date_created = next_day;
1670 form_other->date_created = next_day;
1671 } else {
1672 form_facebook_old->date_synced = now;
1673 form_facebook->date_synced = next_day;
1674 form_other->date_synced = next_day;
1675 }
1676
1677 PasswordsChangeObserver observer(test->store());
1678 test->store()->AddLogin(*form_facebook_old);
1679 test->store()->AddLogin(*form_facebook);
1680 test->store()->AddLogin(*form_other);
1681 EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_facebook_old)));
1682 EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_facebook)));
1683 EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_other)));
1684 observer.WaitAndVerify(test);
1685
1686 // Check the keychain content.
1687 MacKeychainPasswordFormAdapter owned_keychain_adapter(test->keychain());
1688 owned_keychain_adapter.SetFindsOnlyOwnedItems(false);
1689 std::vector<std::unique_ptr<PasswordForm>> matching_items(
1690 owned_keychain_adapter.PasswordsFillingForm(form_facebook->signon_realm,
1691 form_facebook->scheme));
1692 EXPECT_EQ(1u, matching_items.size());
1693 matching_items = owned_keychain_adapter.PasswordsFillingForm(
1694 form_other->signon_realm, form_other->scheme);
1695 EXPECT_EQ(1u, matching_items.size());
1696
1697 // Remove facebook.
1698 if (check_created) {
1699 test->store()->RemoveLoginsCreatedBetween(base::Time(), next_day,
1700 base::Closure());
1701 } else {
1702 test->store()->RemoveLoginsSyncedBetween(base::Time(), next_day);
1703 }
1704 password_manager::PasswordStoreChangeList list;
1705 form_facebook_old->password_value.clear();
1706 form_facebook->password_value.clear();
1707 list.push_back(password_manager::PasswordStoreChange(
1708 password_manager::PasswordStoreChange::REMOVE, *form_facebook_old));
1709 list.push_back(password_manager::PasswordStoreChange(
1710 password_manager::PasswordStoreChange::REMOVE, *form_facebook));
1711 EXPECT_CALL(observer, OnLoginsChanged(list));
1712 list.clear();
1713 observer.WaitAndVerify(test);
1714
1715 matching_items = owned_keychain_adapter.PasswordsFillingForm(
1716 form_facebook->signon_realm, form_facebook->scheme);
1717 EXPECT_EQ(0u, matching_items.size());
1718 matching_items = owned_keychain_adapter.PasswordsFillingForm(
1719 form_other->signon_realm, form_other->scheme);
1720 EXPECT_EQ(1u, matching_items.size());
1721
1722 // Remove form_other.
1723 if (check_created) {
1724 test->store()->RemoveLoginsCreatedBetween(next_day, base::Time(),
1725 base::Closure());
1726 } else {
1727 test->store()->RemoveLoginsSyncedBetween(next_day, base::Time());
1728 }
1729 form_other->password_value.clear();
1730 list.push_back(password_manager::PasswordStoreChange(
1731 password_manager::PasswordStoreChange::REMOVE, *form_other));
1732 EXPECT_CALL(observer, OnLoginsChanged(list));
1733 observer.WaitAndVerify(test);
1734 matching_items = owned_keychain_adapter.PasswordsFillingForm(
1735 form_other->signon_realm, form_other->scheme);
1736 EXPECT_EQ(0u, matching_items.size());
1737 }
1738
1739 } // namespace
1740
1741 TEST_F(PasswordStoreMacTest, TestRemoveLoginsCreatedBetween) {
1742 CheckRemoveLoginsBetween(this, true);
1743 }
1744
1745 TEST_F(PasswordStoreMacTest, TestRemoveLoginsSyncedBetween) {
1746 CheckRemoveLoginsBetween(this, false);
1747 }
1748
1749 TEST_F(PasswordStoreMacTest, TestDisableAutoSignInForOrigins) {
1750 PasswordFormData www_form_data_facebook = {
1751 PasswordForm::SCHEME_HTML,
1752 "http://www.facebook.com/",
1753 "http://www.facebook.com/index.html",
1754 "login",
1755 L"submit",
1756 L"username",
1757 L"password",
1758 L"joe_user",
1759 L"sekrit",
1760 true,
1761 0};
1762 std::unique_ptr<PasswordForm> form_facebook =
1763 CreatePasswordFormFromDataForTesting(www_form_data_facebook);
1764 form_facebook->skip_zero_click = false;
1765
1766 PasswordFormData www_form_data_google = {
1767 PasswordForm::SCHEME_HTML,
1768 "http://www.google.com/",
1769 "http://www.google.com/foo/bar/index.html",
1770 "login",
1771 L"submit",
1772 L"username",
1773 L"password",
1774 L"joe_user",
1775 L"sekrit",
1776 true,
1777 0};
1778 std::unique_ptr<PasswordForm> form_google =
1779 CreatePasswordFormFromDataForTesting(www_form_data_google);
1780 form_google->skip_zero_click = false;
1781
1782 // Add the zero-clickable forms to the database.
1783 PasswordsChangeObserver observer(store());
1784 store()->AddLogin(*form_facebook);
1785 store()->AddLogin(*form_google);
1786 EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_facebook)));
1787 EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_google)));
1788 observer.WaitAndVerify(this);
1789
1790 std::vector<std::unique_ptr<PasswordForm>> forms;
1791 EXPECT_TRUE(login_db()->GetAutoSignInLogins(&forms));
1792 EXPECT_EQ(2u, forms.size());
1793 EXPECT_FALSE(forms[0]->skip_zero_click);
1794 EXPECT_FALSE(forms[1]->skip_zero_click);
1795
1796 store()->DisableAutoSignInForOrigins(
1797 base::Bind(static_cast<bool (*)(const GURL&, const GURL&)>(operator==),
1798 form_google->origin),
1799 base::Closure());
1800 FinishAsyncProcessing();
1801
1802 EXPECT_TRUE(login_db()->GetAutoSignInLogins(&forms));
1803 EXPECT_EQ(1u, forms.size());
1804 EXPECT_EQ(form_facebook->origin, forms[0]->origin);
1805 }
1806
1807 TEST_F(PasswordStoreMacTest, TestRemoveLoginsMultiProfile) {
1808 // Make sure that RemoveLoginsCreatedBetween does affect only the correct
1809 // profile.
1810
1811 // Add a third-party password.
1812 MockAppleKeychain::KeychainTestData keychain_data = {
1813 kSecAuthenticationTypeHTMLForm,
1814 "some.domain.com",
1815 kSecProtocolTypeHTTP,
1816 "/insecure.html",
1817 0,
1818 NULL,
1819 "20020601171500Z",
1820 "joe_user",
1821 "sekrit",
1822 false};
1823 keychain()->AddTestItem(keychain_data);
1824
1825 // Add a password through the adapter. It has the "Chrome" creator tag.
1826 // However, it's not referenced by the password database.
1827 MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain());
1828 owned_keychain_adapter.SetFindsOnlyOwnedItems(true);
1829 PasswordFormData www_form_data1 = {PasswordForm::SCHEME_HTML,
1830 "http://www.facebook.com/",
1831 "http://www.facebook.com/index.html",
1832 "login",
1833 L"username",
1834 L"password",
1835 L"submit",
1836 L"joe_user",
1837 L"sekrit",
1838 true,
1839 1};
1840 std::unique_ptr<PasswordForm> www_form =
1841 CreatePasswordFormFromDataForTesting(www_form_data1);
1842 EXPECT_TRUE(owned_keychain_adapter.AddPassword(*www_form));
1843
1844 // Add a password from the current profile.
1845 PasswordFormData www_form_data2 = {PasswordForm::SCHEME_HTML,
1846 "http://www.facebook.com/",
1847 "http://www.facebook.com/index.html",
1848 "login",
1849 L"username",
1850 L"password",
1851 L"submit",
1852 L"not_joe_user",
1853 L"12345",
1854 true,
1855 1};
1856 www_form = CreatePasswordFormFromDataForTesting(www_form_data2);
1857 store_->AddLogin(*www_form);
1858 FinishAsyncProcessing();
1859
1860 std::vector<std::unique_ptr<PasswordForm>> matching_items;
1861 EXPECT_TRUE(login_db()->GetLogins(PasswordStore::FormDigest(*www_form),
1862 &matching_items));
1863 EXPECT_EQ(1u, matching_items.size());
1864
1865 store_->RemoveLoginsCreatedBetween(base::Time(), base::Time(),
1866 base::Closure());
1867 FinishAsyncProcessing();
1868
1869 // Check the second facebook form is gone.
1870 EXPECT_TRUE(login_db()->GetLogins(PasswordStore::FormDigest(*www_form),
1871 &matching_items));
1872 EXPECT_EQ(0u, matching_items.size());
1873
1874 // Check the first facebook form is still there.
1875 std::vector<std::unique_ptr<PasswordForm>> matching_keychain_items;
1876 matching_keychain_items = owned_keychain_adapter.PasswordsFillingForm(
1877 www_form->signon_realm, www_form->scheme);
1878 ASSERT_EQ(1u, matching_keychain_items.size());
1879 EXPECT_EQ(ASCIIToUTF16("joe_user"),
1880 matching_keychain_items[0]->username_value);
1881
1882 // Check the third-party password is still there.
1883 owned_keychain_adapter.SetFindsOnlyOwnedItems(false);
1884 matching_keychain_items = owned_keychain_adapter.PasswordsFillingForm(
1885 "http://some.domain.com/insecure.html", PasswordForm::SCHEME_HTML);
1886 ASSERT_EQ(1u, matching_keychain_items.size());
1887 }
1888
1889 // Add a facebook form to the store but not to the keychain. The form is to be
1890 // implicitly deleted. However, the observers shouldn't get notified about
1891 // deletion of non-existent forms like m.facebook.com.
1892 TEST_F(PasswordStoreMacTest, SilentlyRemoveOrphanedForm) {
1893 testing::StrictMock<password_manager::MockPasswordStoreObserver>
1894 mock_observer;
1895 store()->AddObserver(&mock_observer);
1896
1897 // 1. Add a password for www.facebook.com to the LoginDatabase.
1898 PasswordFormData www_form_data = {PasswordForm::SCHEME_HTML,
1899 "http://www.facebook.com/",
1900 "http://www.facebook.com/index.html",
1901 "login",
1902 L"username",
1903 L"password",
1904 L"submit",
1905 L"joe_user",
1906 L"",
1907 true,
1908 1};
1909 std::unique_ptr<PasswordForm> www_form(
1910 CreatePasswordFormFromDataForTesting(www_form_data));
1911 EXPECT_EQ(AddChangeForForm(*www_form), login_db()->AddLogin(*www_form));
1912
1913 // 2. Get a PSL-matched password for m.facebook.com. The observer isn't
1914 // notified because the form isn't in the database.
1915 PasswordForm m_form(*www_form);
1916 m_form.signon_realm = "http://m.facebook.com";
1917 m_form.origin = GURL("http://m.facebook.com/index.html");
1918
1919 MockPasswordStoreConsumer consumer;
1920 ON_CALL(consumer, OnGetPasswordStoreResultsConstRef(_))
1921 .WillByDefault(QuitUIMessageLoop());
1922 EXPECT_CALL(mock_observer, OnLoginsChanged(_)).Times(0);
1923 // The PSL-matched form isn't returned because there is no actual password in
1924 // the keychain.
1925 EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(IsEmpty()));
1926 store_->GetLogins(PasswordStore::FormDigest(m_form), &consumer);
1927 base::RunLoop().Run();
1928 std::vector<std::unique_ptr<PasswordForm>> all_forms;
1929 EXPECT_TRUE(login_db()->GetAutofillableLogins(&all_forms));
1930 EXPECT_EQ(1u, all_forms.size());
1931 ::testing::Mock::VerifyAndClearExpectations(&mock_observer);
1932
1933 // 3. Get a password for www.facebook.com. The form is implicitly removed and
1934 // the observer is notified.
1935 password_manager::PasswordStoreChangeList list;
1936 list.push_back(password_manager::PasswordStoreChange(
1937 password_manager::PasswordStoreChange::REMOVE, *www_form));
1938 EXPECT_CALL(mock_observer, OnLoginsChanged(list));
1939 EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(IsEmpty()));
1940 store_->GetLogins(PasswordStore::FormDigest(*www_form), &consumer);
1941 base::RunLoop().Run();
1942 EXPECT_TRUE(login_db()->GetAutofillableLogins(&all_forms));
1943 EXPECT_EQ(0u, all_forms.size());
1944 }
1945
1946 // Verify that Android app passwords can be stored, retrieved, and deleted.
1947 // Regression test for http://crbug.com/455551
1948 TEST_F(PasswordStoreMacTest, StoringAndRetrievingAndroidCredentials) {
1949 PasswordForm form;
1950 form.signon_realm = "android://7x7IDboo8u9YKraUsbmVkuf1@net.rateflix.app/";
1951 form.username_value = base::UTF8ToUTF16("randomusername");
1952 form.password_value = base::UTF8ToUTF16("password");
1953
1954 VerifyCredentialLifecycle(form);
1955 }
1956
1957 // Verify that federated credentials can be stored, retrieved and deleted.
1958 TEST_F(PasswordStoreMacTest, StoringAndRetrievingFederatedCredentials) {
1959 PasswordForm form;
1960 form.signon_realm = "android://7x7IDboo8u9YKraUsbmVkuf1@net.rateflix.app/";
1961 form.federation_origin =
1962 url::Origin(GURL(password_manager::kTestingFederationUrlSpec));
1963 form.username_value = base::UTF8ToUTF16("randomusername");
1964 form.password_value = base::UTF8ToUTF16(""); // No password.
1965
1966 VerifyCredentialLifecycle(form);
1967 }
1968
1969 void CheckMigrationResult(PasswordStoreMac::MigrationResult expected_result,
1970 PasswordStoreMac::MigrationResult result) {
1971 EXPECT_EQ(expected_result, result);
1972 QuitUIMessageLoop();
1973 }
1974
1975 // Import the passwords from the Keychain to LoginDatabase.
1976 TEST_F(PasswordStoreMacTest, ImportFromKeychain) {
1977 PasswordForm form1;
1978 form1.origin = GURL("http://accounts.google.com/LoginAuth");
1979 form1.signon_realm = "http://accounts.google.com/";
1980 form1.username_value = ASCIIToUTF16("my_username");
1981 form1.password_value = ASCIIToUTF16("my_password");
1982
1983 PasswordForm form2;
1984 form2.origin = GURL("http://facebook.com/Login");
1985 form2.signon_realm = "http://facebook.com/";
1986 form2.username_value = ASCIIToUTF16("my_username");
1987 form2.password_value = ASCIIToUTF16("my_password");
1988
1989 PasswordForm blacklisted_form;
1990 blacklisted_form.origin = GURL("http://badsite.com/Login");
1991 blacklisted_form.signon_realm = "http://badsite.com/";
1992 blacklisted_form.blacklisted_by_user = true;
1993
1994 store()->AddLogin(form1);
1995 store()->AddLogin(form2);
1996 store()->AddLogin(blacklisted_form);
1997 FinishAsyncProcessing();
1998
1999 ASSERT_TRUE(base::PostTaskAndReplyWithResult(
2000 thread_->task_runner().get(), FROM_HERE,
2001 base::Bind(&PasswordStoreMac::ImportFromKeychain, login_db(), keychain()),
2002 base::Bind(&CheckMigrationResult, PasswordStoreMac::MIGRATION_OK)));
2003 FinishAsyncProcessing();
2004
2005 // The password should be stored in the database by now.
2006 std::vector<std::unique_ptr<PasswordForm>> matching_items;
2007 EXPECT_TRUE(
2008 login_db()->GetLogins(PasswordStore::FormDigest(form1), &matching_items));
2009 ASSERT_EQ(1u, matching_items.size());
2010 EXPECT_EQ(form1, *matching_items[0]);
2011
2012 EXPECT_TRUE(
2013 login_db()->GetLogins(PasswordStore::FormDigest(form2), &matching_items));
2014 ASSERT_EQ(1u, matching_items.size());
2015 EXPECT_EQ(form2, *matching_items[0]);
2016
2017 EXPECT_TRUE(login_db()->GetLogins(PasswordStore::FormDigest(blacklisted_form),
2018 &matching_items));
2019 ASSERT_EQ(1u, matching_items.size());
2020 EXPECT_EQ(blacklisted_form, *matching_items[0]);
2021
2022 // The passwords are encrypted using a key from the Keychain.
2023 EXPECT_TRUE(
2024 histogram_tester_->GetHistogramSamplesSinceCreation("OSX.Keychain.Access")
2025 ->TotalCount());
2026 histogram_tester_.reset();
2027 }
2028
2029 // Import a federated credential while the Keychain is locked.
2030 TEST_F(PasswordStoreMacTest, ImportFederatedFromLockedKeychain) {
2031 keychain()->set_locked(true);
2032 PasswordForm form1;
2033 form1.origin = GURL("http://example.com/Login");
2034 form1.signon_realm = "http://example.com/";
2035 form1.username_value = ASCIIToUTF16("my_username");
2036 form1.federation_origin = url::Origin(GURL("https://accounts.google.com/"));
2037
2038 store()->AddLogin(form1);
2039 FinishAsyncProcessing();
2040 ASSERT_TRUE(base::PostTaskAndReplyWithResult(
2041 thread_->task_runner().get(), FROM_HERE,
2042 base::Bind(&PasswordStoreMac::ImportFromKeychain, login_db(), keychain()),
2043 base::Bind(&CheckMigrationResult, PasswordStoreMac::MIGRATION_OK)));
2044 FinishAsyncProcessing();
2045
2046 std::vector<std::unique_ptr<PasswordForm>> matching_items;
2047 EXPECT_TRUE(
2048 login_db()->GetLogins(PasswordStore::FormDigest(form1), &matching_items));
2049 ASSERT_EQ(1u, matching_items.size());
2050 EXPECT_EQ(form1, *matching_items[0]);
2051 }
2052
2053 // Try to import while the Keychain is locked but the encryption key had been
2054 // read earlier.
2055 TEST_F(PasswordStoreMacTest, ImportFromLockedKeychainError) {
2056 PasswordForm form1;
2057 form1.origin = GURL("http://accounts.google.com/LoginAuth");
2058 form1.signon_realm = "http://accounts.google.com/";
2059 form1.username_value = ASCIIToUTF16("my_username");
2060 form1.password_value = ASCIIToUTF16("my_password");
2061 store()->AddLogin(form1);
2062 FinishAsyncProcessing();
2063
2064 // Add a second keychain item matching the Database entry.
2065 PasswordForm form2 = form1;
2066 form2.origin = GURL("http://accounts.google.com/Login");
2067 form2.password_value = ASCIIToUTF16("1234");
2068 MacKeychainPasswordFormAdapter adapter(keychain());
2069 EXPECT_TRUE(adapter.AddPassword(form2));
2070
2071 keychain()->set_locked(true);
2072 ASSERT_TRUE(base::PostTaskAndReplyWithResult(
2073 thread_->task_runner().get(), FROM_HERE,
2074 base::Bind(&PasswordStoreMac::ImportFromKeychain, login_db(), keychain()),
2075 base::Bind(&CheckMigrationResult, PasswordStoreMac::MIGRATION_PARTIAL)));
2076 FinishAsyncProcessing();
2077
2078 std::vector<std::unique_ptr<PasswordForm>> matching_items;
2079 EXPECT_TRUE(
2080 login_db()->GetLogins(PasswordStore::FormDigest(form1), &matching_items));
2081 EXPECT_EQ(0u, matching_items.size());
2082
2083 histogram_tester_->ExpectUniqueSample(
2084 "PasswordManager.KeychainMigration.NumPasswordsOnFailure", 1, 1);
2085 histogram_tester_->ExpectUniqueSample(
2086 "PasswordManager.KeychainMigration.NumFailedPasswords", 1, 1);
2087 // Don't test the encryption key access.
2088 histogram_tester_.reset();
2089 }
2090
2091 // Delete the Chrome-owned password from the Keychain.
2092 TEST_F(PasswordStoreMacTest, CleanUpKeychain) {
2093 MockAppleKeychain::KeychainTestData data1 = { kSecAuthenticationTypeHTMLForm,
2094 "some.domain.com", kSecProtocolTypeHTTP, NULL, 0, NULL, "20020601171500Z",
2095 "joe_user", "sekrit", false};
2096 keychain()->AddTestItem(data1);
2097
2098 MacKeychainPasswordFormAdapter keychain_adapter(keychain());
2099 PasswordFormData data2 = { PasswordForm::SCHEME_HTML, "http://web.site.com/",
2100 "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL,
2101 L"anonymous", L"knock-knock", false, 0 };
2102 keychain_adapter.AddPassword(*CreatePasswordFormFromDataForTesting(data2));
2103 std::vector<std::unique_ptr<PasswordForm>> passwords =
2104 keychain_adapter.GetAllPasswordFormPasswords();
2105 EXPECT_EQ(2u, passwords.size());
2106
2107 // Delete everyhting but only the Chrome-owned item should be affected.
2108 PasswordStoreMac::CleanUpKeychain(keychain(), passwords);
2109 passwords = keychain_adapter.GetAllPasswordFormPasswords();
2110 ASSERT_EQ(1u, passwords.size());
2111 EXPECT_EQ("http://some.domain.com/", passwords[0]->signon_realm);
2112 EXPECT_EQ(ASCIIToUTF16("sekrit"), passwords[0]->password_value);
2113 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698