| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/password_manager/password_store_mac.h" |
| |
| #include "base/basictypes.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/scoped_observer.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/test/histogram_tester.h" |
| #include "base/thread_task_runner_handle.h" |
| #include "chrome/browser/password_manager/password_store_mac_internal.h" |
| #include "chrome/common/chrome_paths.h" |
| #include "components/os_crypt/os_crypt.h" |
| #include "components/password_manager/core/browser/login_database.h" |
| #include "components/password_manager/core/browser/password_manager_test_utils.h" |
| #include "components/password_manager/core/browser/password_store_consumer.h" |
| #include "content/public/test/test_browser_thread.h" |
| #include "content/public/test/test_utils.h" |
| #include "crypto/mock_apple_keychain.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using autofill::PasswordForm; |
| using base::ASCIIToUTF16; |
| using base::WideToUTF16; |
| using content::BrowserThread; |
| using crypto::MockAppleKeychain; |
| using internal_keychain_helpers::FormsMatchForMerge; |
| using internal_keychain_helpers::STRICT_FORM_MATCH; |
| using password_manager::CreatePasswordFormFromDataForTesting; |
| using password_manager::LoginDatabase; |
| using password_manager::PasswordFormData; |
| using password_manager::PasswordStore; |
| using password_manager::PasswordStoreChange; |
| using password_manager::PasswordStoreChangeList; |
| using password_manager::PasswordStoreConsumer; |
| using testing::_; |
| using testing::DoAll; |
| using testing::Invoke; |
| using testing::IsEmpty; |
| using testing::SizeIs; |
| using testing::WithArg; |
| |
| namespace { |
| |
| ACTION(QuitUIMessageLoop) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| base::MessageLoop::current()->Quit(); |
| } |
| |
| // From the mock's argument #0 of type const std::vector<PasswordForm*>& takes |
| // the first form and copies it to the form pointed to by |target_form_ptr|. |
| ACTION_P(SaveACopyOfFirstForm, target_form_ptr) { |
| ASSERT_FALSE(arg0.empty()); |
| *target_form_ptr = *arg0[0]; |
| } |
| |
| void Noop() { |
| } |
| |
| class MockPasswordStoreConsumer : public PasswordStoreConsumer { |
| public: |
| MOCK_METHOD1(OnGetPasswordStoreResultsConstRef, |
| void(const std::vector<PasswordForm*>&)); |
| |
| // GMock cannot mock methods with move-only args. |
| void OnGetPasswordStoreResults(ScopedVector<PasswordForm> results) override { |
| OnGetPasswordStoreResultsConstRef(results.get()); |
| } |
| }; |
| |
| class MockPasswordStoreObserver : public PasswordStore::Observer { |
| public: |
| MOCK_METHOD1(OnLoginsChanged, |
| void(const password_manager::PasswordStoreChangeList& changes)); |
| }; |
| |
| // A LoginDatabase that simulates an Init() method that takes a long time. |
| class SlowToInitLoginDatabase : public password_manager::LoginDatabase { |
| public: |
| // Creates an instance whose Init() method will block until |event| is |
| // signaled. |event| must outlive |this|. |
| SlowToInitLoginDatabase(const base::FilePath& db_path, |
| base::WaitableEvent* event) |
| : password_manager::LoginDatabase(db_path), event_(event) {} |
| ~SlowToInitLoginDatabase() override {} |
| |
| // LoginDatabase: |
| bool Init() override { |
| event_->Wait(); |
| return password_manager::LoginDatabase::Init(); |
| } |
| |
| private: |
| base::WaitableEvent* event_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SlowToInitLoginDatabase); |
| }; |
| |
| #pragma mark - |
| |
| // Macro to simplify calling CheckFormsAgainstExpectations with a useful label. |
| #define CHECK_FORMS(forms, expectations, i) \ |
| CheckFormsAgainstExpectations(forms, expectations, #forms, i) |
| |
| // Ensures that the data in |forms| match |expectations|, causing test failures |
| // for any discrepencies. |
| // TODO(stuartmorgan): This is current order-dependent; ideally it shouldn't |
| // matter if |forms| and |expectations| are scrambled. |
| void CheckFormsAgainstExpectations( |
| const std::vector<PasswordForm*>& forms, |
| const std::vector<PasswordFormData*>& expectations, |
| |
| const char* forms_label, unsigned int test_number) { |
| EXPECT_EQ(expectations.size(), forms.size()) << forms_label << " in test " |
| << test_number; |
| if (expectations.size() != forms.size()) |
| return; |
| |
| for (unsigned int i = 0; i < expectations.size(); ++i) { |
| SCOPED_TRACE(testing::Message() << forms_label << " in test " << test_number |
| << ", item " << i); |
| PasswordForm* form = forms[i]; |
| PasswordFormData* expectation = expectations[i]; |
| EXPECT_EQ(expectation->scheme, form->scheme); |
| EXPECT_EQ(std::string(expectation->signon_realm), form->signon_realm); |
| EXPECT_EQ(GURL(expectation->origin), form->origin); |
| EXPECT_EQ(GURL(expectation->action), form->action); |
| EXPECT_EQ(WideToUTF16(expectation->submit_element), form->submit_element); |
| EXPECT_EQ(WideToUTF16(expectation->username_element), |
| form->username_element); |
| EXPECT_EQ(WideToUTF16(expectation->password_element), |
| form->password_element); |
| if (expectation->username_value) { |
| EXPECT_EQ(WideToUTF16(expectation->username_value), form->username_value); |
| EXPECT_EQ(WideToUTF16(expectation->username_value), form->display_name); |
| EXPECT_TRUE(form->skip_zero_click); |
| if (expectation->password_value && |
| wcscmp(expectation->password_value, |
| password_manager::kTestingFederatedLoginMarker) == 0) { |
| EXPECT_TRUE(form->password_value.empty()); |
| EXPECT_EQ(GURL(password_manager::kTestingFederationUrlSpec), |
| form->federation_url); |
| } else { |
| EXPECT_EQ(WideToUTF16(expectation->password_value), |
| form->password_value); |
| EXPECT_TRUE(form->federation_url.is_empty()); |
| } |
| } else { |
| EXPECT_TRUE(form->blacklisted_by_user); |
| } |
| EXPECT_EQ(expectation->preferred, form->preferred); |
| EXPECT_EQ(expectation->ssl_valid, form->ssl_valid); |
| EXPECT_DOUBLE_EQ(expectation->creation_time, |
| form->date_created.ToDoubleT()); |
| base::Time created = base::Time::FromDoubleT(expectation->creation_time); |
| EXPECT_EQ( |
| created + base::TimeDelta::FromDays( |
| password_manager::kTestingDaysAfterPasswordsAreSynced), |
| form->date_synced); |
| EXPECT_EQ(GURL(password_manager::kTestingAvatarUrlSpec), form->avatar_url); |
| } |
| } |
| |
| PasswordStoreChangeList AddChangeForForm(const PasswordForm& form) { |
| return PasswordStoreChangeList( |
| 1, PasswordStoreChange(PasswordStoreChange::ADD, form)); |
| } |
| |
| } // namespace |
| |
| #pragma mark - |
| |
| class PasswordStoreMacInternalsTest : public testing::Test { |
| public: |
| void SetUp() override { |
| MockAppleKeychain::KeychainTestData test_data[] = { |
| // Basic HTML form. |
| {kSecAuthenticationTypeHTMLForm, |
| "some.domain.com", |
| kSecProtocolTypeHTTP, |
| NULL, |
| 0, |
| NULL, |
| "20020601171500Z", |
| "joe_user", |
| "sekrit", |
| false}, |
| // HTML form with path. |
| {kSecAuthenticationTypeHTMLForm, |
| "some.domain.com", |
| kSecProtocolTypeHTTP, |
| "/insecure.html", |
| 0, |
| NULL, |
| "19991231235959Z", |
| "joe_user", |
| "sekrit", |
| false}, |
| // Secure HTML form with path. |
| {kSecAuthenticationTypeHTMLForm, |
| "some.domain.com", |
| kSecProtocolTypeHTTPS, |
| "/secure.html", |
| 0, |
| NULL, |
| "20100908070605Z", |
| "secure_user", |
| "password", |
| false}, |
| // True negative item. |
| {kSecAuthenticationTypeHTMLForm, |
| "dont.remember.com", |
| kSecProtocolTypeHTTP, |
| NULL, |
| 0, |
| NULL, |
| "20000101000000Z", |
| "", |
| "", |
| true}, |
| // De-facto negative item, type one. |
| {kSecAuthenticationTypeHTMLForm, |
| "dont.remember.com", |
| kSecProtocolTypeHTTP, |
| NULL, |
| 0, |
| NULL, |
| "20000101000000Z", |
| "Password Not Stored", |
| "", |
| false}, |
| // De-facto negative item, type two. |
| {kSecAuthenticationTypeHTMLForm, |
| "dont.remember.com", |
| kSecProtocolTypeHTTPS, |
| NULL, |
| 0, |
| NULL, |
| "20000101000000Z", |
| "Password Not Stored", |
| " ", |
| false}, |
| // HTTP auth basic, with port and path. |
| {kSecAuthenticationTypeHTTPBasic, |
| "some.domain.com", |
| kSecProtocolTypeHTTP, |
| "/insecure.html", |
| 4567, |
| "low_security", |
| "19980330100000Z", |
| "basic_auth_user", |
| "basic", |
| false}, |
| // HTTP auth digest, secure. |
| {kSecAuthenticationTypeHTTPDigest, |
| "some.domain.com", |
| kSecProtocolTypeHTTPS, |
| NULL, |
| 0, |
| "high_security", |
| "19980330100000Z", |
| "digest_auth_user", |
| "digest", |
| false}, |
| // An FTP password with an invalid date, for edge-case testing. |
| {kSecAuthenticationTypeDefault, |
| "a.server.com", |
| kSecProtocolTypeFTP, |
| NULL, |
| 0, |
| NULL, |
| "20010203040", |
| "abc", |
| "123", |
| false}, |
| // Password for an Android application. |
| {kSecAuthenticationTypeHTMLForm, |
| "android://hash@com.domain.some/", |
| kSecProtocolTypeHTTPS, |
| "", |
| 0, |
| NULL, |
| "20150515141312Z", |
| "joe_user", |
| "secret", |
| false}, |
| }; |
| |
| keychain_ = new MockAppleKeychain(); |
| |
| for (unsigned int i = 0; i < arraysize(test_data); ++i) { |
| keychain_->AddTestItem(test_data[i]); |
| } |
| } |
| |
| void TearDown() override { |
| ExpectCreatesAndFreesBalanced(); |
| ExpectCreatorCodesSet(); |
| delete keychain_; |
| } |
| |
| protected: |
| // Causes a test failure unless everything returned from keychain_'s |
| // ItemCopyAttributesAndData, SearchCreateFromAttributes, and SearchCopyNext |
| // was correctly freed. |
| void ExpectCreatesAndFreesBalanced() { |
| EXPECT_EQ(0, keychain_->UnfreedSearchCount()); |
| EXPECT_EQ(0, keychain_->UnfreedKeychainItemCount()); |
| EXPECT_EQ(0, keychain_->UnfreedAttributeDataCount()); |
| } |
| |
| // Causes a test failure unless any Keychain items added during the test have |
| // their creator code set. |
| void ExpectCreatorCodesSet() { |
| EXPECT_TRUE(keychain_->CreatorCodesSetForAddedItems()); |
| } |
| |
| MockAppleKeychain* keychain_; |
| }; |
| |
| #pragma mark - |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestKeychainToFormTranslation) { |
| typedef struct { |
| const PasswordForm::Scheme scheme; |
| const char* signon_realm; |
| const char* origin; |
| const wchar_t* username; // Set to NULL to check for a blacklist entry. |
| const wchar_t* password; |
| const bool ssl_valid; |
| const int creation_year; |
| const int creation_month; |
| const int creation_day; |
| const int creation_hour; |
| const int creation_minute; |
| const int creation_second; |
| } TestExpectations; |
| |
| TestExpectations expected[] = { |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", L"joe_user", L"sekrit", false, |
| 2002, 6, 1, 17, 15, 0 }, |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/insecure.html", L"joe_user", L"sekrit", false, |
| 1999, 12, 31, 23, 59, 59 }, |
| { PasswordForm::SCHEME_HTML, "https://some.domain.com/", |
| "https://some.domain.com/secure.html", L"secure_user", L"password", true, |
| 2010, 9, 8, 7, 6, 5 }, |
| { PasswordForm::SCHEME_HTML, "http://dont.remember.com/", |
| "http://dont.remember.com/", NULL, NULL, false, |
| 2000, 1, 1, 0, 0, 0 }, |
| { PasswordForm::SCHEME_HTML, "http://dont.remember.com/", |
| "http://dont.remember.com/", NULL, NULL, false, |
| 2000, 1, 1, 0, 0, 0 }, |
| { PasswordForm::SCHEME_HTML, "https://dont.remember.com/", |
| "https://dont.remember.com/", NULL, NULL, true, |
| 2000, 1, 1, 0, 0, 0 }, |
| { PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security", |
| "http://some.domain.com:4567/insecure.html", L"basic_auth_user", L"basic", |
| false, 1998, 03, 30, 10, 00, 00 }, |
| { PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security", |
| "https://some.domain.com/", L"digest_auth_user", L"digest", true, |
| 1998, 3, 30, 10, 0, 0 }, |
| // This one gives us an invalid date, which we will treat as a "NULL" date |
| // which is 1601. |
| { PasswordForm::SCHEME_OTHER, "http://a.server.com/", |
| "http://a.server.com/", L"abc", L"123", false, |
| 1601, 1, 1, 0, 0, 0 }, |
| { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| "", L"joe_user", L"secret", true, |
| 2015, 5, 15, 14, 13, 12 }, |
| }; |
| |
| for (unsigned int i = 0; i < arraysize(expected); ++i) { |
| // Create our fake KeychainItemRef; see MockAppleKeychain docs. |
| SecKeychainItemRef keychain_item = |
| reinterpret_cast<SecKeychainItemRef>(i + 1); |
| PasswordForm form; |
| bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, keychain_item, &form, true); |
| |
| EXPECT_TRUE(parsed) << "In iteration " << i; |
| |
| EXPECT_EQ(expected[i].scheme, form.scheme) << "In iteration " << i; |
| EXPECT_EQ(GURL(expected[i].origin), form.origin) << "In iteration " << i; |
| EXPECT_EQ(expected[i].ssl_valid, form.ssl_valid) << "In iteration " << i; |
| EXPECT_EQ(std::string(expected[i].signon_realm), form.signon_realm) |
| << "In iteration " << i; |
| if (expected[i].username) { |
| EXPECT_EQ(WideToUTF16(expected[i].username), form.username_value) |
| << "In iteration " << i; |
| EXPECT_EQ(WideToUTF16(expected[i].password), form.password_value) |
| << "In iteration " << i; |
| EXPECT_FALSE(form.blacklisted_by_user) << "In iteration " << i; |
| } else { |
| EXPECT_TRUE(form.blacklisted_by_user) << "In iteration " << i; |
| } |
| base::Time::Exploded exploded_time; |
| form.date_created.UTCExplode(&exploded_time); |
| EXPECT_EQ(expected[i].creation_year, exploded_time.year) |
| << "In iteration " << i; |
| EXPECT_EQ(expected[i].creation_month, exploded_time.month) |
| << "In iteration " << i; |
| EXPECT_EQ(expected[i].creation_day, exploded_time.day_of_month) |
| << "In iteration " << i; |
| EXPECT_EQ(expected[i].creation_hour, exploded_time.hour) |
| << "In iteration " << i; |
| EXPECT_EQ(expected[i].creation_minute, exploded_time.minute) |
| << "In iteration " << i; |
| EXPECT_EQ(expected[i].creation_second, exploded_time.second) |
| << "In iteration " << i; |
| } |
| |
| { |
| // Use an invalid ref, to make sure errors are reported. |
| SecKeychainItemRef keychain_item = reinterpret_cast<SecKeychainItemRef>(99); |
| PasswordForm form; |
| bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, keychain_item, &form, true); |
| EXPECT_FALSE(parsed); |
| } |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestKeychainSearch) { |
| struct TestDataAndExpectation { |
| const PasswordFormData data; |
| const size_t expected_fill_matches; |
| const size_t expected_merge_matches; |
| }; |
| // Most fields are left blank because we don't care about them for searching. |
| /* clang-format off */ |
| TestDataAndExpectation test_data[] = { |
| // An HTML form we've seen. |
| { { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| NULL, NULL, NULL, NULL, NULL, L"joe_user", NULL, false, false, 0 }, |
| 2, 2 }, |
| { { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| NULL, NULL, NULL, NULL, NULL, L"wrong_user", NULL, false, false, 0 }, |
| 2, 0 }, |
| // An HTML form we haven't seen |
| { { PasswordForm::SCHEME_HTML, "http://www.unseendomain.com/", |
| NULL, NULL, NULL, NULL, NULL, L"joe_user", NULL, false, false, 0 }, |
| 0, 0 }, |
| // Basic auth that should match. |
| { { PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security", |
| NULL, NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, false, false, |
| 0 }, |
| 1, 1 }, |
| // Basic auth with the wrong port. |
| { { PasswordForm::SCHEME_BASIC, "http://some.domain.com:1111/low_security", |
| NULL, NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, false, false, |
| 0 }, |
| 0, 0 }, |
| // Digest auth we've saved under https, visited with http. |
| { { PasswordForm::SCHEME_DIGEST, "http://some.domain.com/high_security", |
| NULL, NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, false, false, |
| 0 }, |
| 0, 0 }, |
| // Digest auth that should match. |
| { { PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security", |
| NULL, NULL, NULL, NULL, NULL, L"wrong_user", NULL, false, true, 0 }, |
| 1, 0 }, |
| // Digest auth with the wrong domain. |
| { { PasswordForm::SCHEME_DIGEST, "https://some.domain.com/other_domain", |
| NULL, NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, false, true, |
| 0 }, |
| 0, 0 }, |
| // Android credentials (both legacy ones with origin, and without). |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| "android://hash@com.domain.some/", NULL, NULL, NULL, NULL, L"joe_user", |
| NULL, false, true, 0 }, |
| 1, 1 }, |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| NULL, NULL, NULL, NULL, NULL, L"joe_user", NULL, false, true, 0 }, |
| 1, 1 }, |
| // Federated logins do not have a corresponding Keychain entry, and should |
| // not match the username/password stored for the same application. Note |
| // that it will match for filling, however, because that part does not know |
| // that it is a federated login. |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| NULL, NULL, NULL, NULL, NULL, L"joe_user", |
| password_manager::kTestingFederatedLoginMarker, false, true, 0 }, |
| 1, 0 }, |
| /// Garbage forms should have no matches. |
| { { PasswordForm::SCHEME_HTML, "foo/bar/baz", |
| NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 0, 0 }, |
| }; |
| /* clang-format on */ |
| |
| MacKeychainPasswordFormAdapter keychain_adapter(keychain_); |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(true); |
| for (unsigned int i = 0; i < arraysize(test_data); ++i) { |
| scoped_ptr<PasswordForm> query_form = |
| CreatePasswordFormFromDataForTesting(test_data[i].data); |
| |
| // Check matches treating the form as a fill target. |
| ScopedVector<autofill::PasswordForm> matching_items = |
| keychain_adapter.PasswordsFillingForm(query_form->signon_realm, |
| query_form->scheme); |
| EXPECT_EQ(test_data[i].expected_fill_matches, matching_items.size()); |
| |
| // Check matches treating the form as a merging target. |
| EXPECT_EQ(test_data[i].expected_merge_matches > 0, |
| keychain_adapter.HasPasswordsMergeableWithForm(*query_form)); |
| std::vector<SecKeychainItemRef> keychain_items; |
| std::vector<internal_keychain_helpers::ItemFormPair> item_form_pairs = |
| internal_keychain_helpers:: |
| ExtractAllKeychainItemAttributesIntoPasswordForms(&keychain_items, |
| *keychain_); |
| matching_items = |
| internal_keychain_helpers::ExtractPasswordsMergeableWithForm( |
| *keychain_, item_form_pairs, *query_form); |
| EXPECT_EQ(test_data[i].expected_merge_matches, matching_items.size()); |
| STLDeleteContainerPairSecondPointers(item_form_pairs.begin(), |
| item_form_pairs.end()); |
| for (std::vector<SecKeychainItemRef>::iterator i = keychain_items.begin(); |
| i != keychain_items.end(); ++i) { |
| keychain_->Free(*i); |
| } |
| |
| // None of the pre-seeded items are owned by us, so none should match an |
| // owned-passwords-only search. |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| query_form->signon_realm, query_form->scheme); |
| EXPECT_EQ(0U, matching_items.size()); |
| } |
| } |
| |
| // Changes just the origin path of |form|. |
| static void SetPasswordFormPath(PasswordForm* form, const char* path) { |
| GURL::Replacements replacement; |
| std::string new_value(path); |
| replacement.SetPathStr(new_value); |
| form->origin = form->origin.ReplaceComponents(replacement); |
| } |
| |
| // Changes just the signon_realm port of |form|. |
| static void SetPasswordFormPort(PasswordForm* form, const char* port) { |
| GURL::Replacements replacement; |
| std::string new_value(port); |
| replacement.SetPortStr(new_value); |
| GURL signon_gurl = GURL(form->signon_realm); |
| form->signon_realm = signon_gurl.ReplaceComponents(replacement).spec(); |
| } |
| |
| // Changes just the signon_ream auth realm of |form|. |
| static void SetPasswordFormRealm(PasswordForm* form, const char* realm) { |
| GURL::Replacements replacement; |
| std::string new_value(realm); |
| replacement.SetPathStr(new_value); |
| GURL signon_gurl = GURL(form->signon_realm); |
| form->signon_realm = signon_gurl.ReplaceComponents(replacement).spec(); |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestKeychainExactSearch) { |
| MacKeychainPasswordFormAdapter keychain_adapter(keychain_); |
| |
| PasswordFormData base_form_data[] = { |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/insecure.html", |
| NULL, NULL, NULL, NULL, L"joe_user", NULL, true, false, 0 }, |
| { PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security", |
| "http://some.domain.com:4567/insecure.html", |
| NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, true, false, 0 }, |
| { PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security", |
| "https://some.domain.com", |
| NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, true, true, 0 }, |
| }; |
| |
| for (unsigned int i = 0; i < arraysize(base_form_data); ++i) { |
| // Create a base form and make sure we find a match. |
| scoped_ptr<PasswordForm> base_form = |
| CreatePasswordFormFromDataForTesting(base_form_data[i]); |
| EXPECT_TRUE(keychain_adapter.HasPasswordsMergeableWithForm(*base_form)); |
| EXPECT_TRUE(keychain_adapter.HasPasswordExactlyMatchingForm(*base_form)); |
| |
| // Make sure that the matching isn't looser than it should be by checking |
| // that slightly altered forms don't match. |
| ScopedVector<autofill::PasswordForm> modified_forms; |
| |
| modified_forms.push_back(new PasswordForm(*base_form)); |
| modified_forms.back()->username_value = ASCIIToUTF16("wrong_user"); |
| |
| modified_forms.push_back(new PasswordForm(*base_form)); |
| SetPasswordFormPath(modified_forms.back(), "elsewhere.html"); |
| |
| modified_forms.push_back(new PasswordForm(*base_form)); |
| modified_forms.back()->scheme = PasswordForm::SCHEME_OTHER; |
| |
| modified_forms.push_back(new PasswordForm(*base_form)); |
| SetPasswordFormPort(modified_forms.back(), "1234"); |
| |
| modified_forms.push_back(new PasswordForm(*base_form)); |
| modified_forms.back()->blacklisted_by_user = true; |
| |
| if (base_form->scheme == PasswordForm::SCHEME_BASIC || |
| base_form->scheme == PasswordForm::SCHEME_DIGEST) { |
| modified_forms.push_back(new PasswordForm(*base_form)); |
| SetPasswordFormRealm(modified_forms.back(), "incorrect"); |
| } |
| |
| for (unsigned int j = 0; j < modified_forms.size(); ++j) { |
| bool match = keychain_adapter.HasPasswordExactlyMatchingForm( |
| *modified_forms[j]); |
| EXPECT_FALSE(match) << "In modified version " << j |
| << " of base form " << i; |
| } |
| } |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestKeychainAdd) { |
| struct TestDataAndExpectation { |
| PasswordFormData data; |
| bool should_succeed; |
| }; |
| /* clang-format off */ |
| TestDataAndExpectation test_data[] = { |
| // Test a variety of scheme/port/protocol/path variations. |
| { { PasswordForm::SCHEME_HTML, "http://web.site.com/", |
| "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL, |
| L"anonymous", L"knock-knock", false, false, 0 }, true }, |
| { { PasswordForm::SCHEME_HTML, "https://web.site.com/", |
| "https://web.site.com/", NULL, NULL, NULL, NULL, |
| L"admin", L"p4ssw0rd", false, false, 0 }, true }, |
| { { PasswordForm::SCHEME_BASIC, "http://a.site.com:2222/therealm", |
| "http://a.site.com:2222/", NULL, NULL, NULL, NULL, |
| L"username", L"password", false, false, 0 }, true }, |
| { { PasswordForm::SCHEME_DIGEST, "https://digest.site.com/differentrealm", |
| "https://digest.site.com/secure.html", NULL, NULL, NULL, NULL, |
| L"testname", L"testpass", false, false, 0 }, true }, |
| // Test that Android credentials can be stored. Also check the legacy form |
| // when |origin| was still filled with the Android URI (and not left empty). |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.example.alpha/", |
| "", NULL, NULL, NULL, NULL, |
| L"joe_user", L"password", false, true, 0 }, true }, |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.example.beta/", |
| "android://hash@com.example.beta/", NULL, NULL, NULL, NULL, |
| L"jane_user", L"password2", false, true, 0 }, true }, |
| // Make sure that garbage forms are rejected. |
| { { PasswordForm::SCHEME_HTML, "gobbledygook", |
| "gobbledygook", NULL, NULL, NULL, NULL, |
| L"anonymous", L"knock-knock", false, false, 0 }, false }, |
| // Test that failing to update a duplicate (forced using the magic failure |
| // password; see MockAppleKeychain::ItemModifyAttributesAndData) is |
| // reported. |
| { { PasswordForm::SCHEME_HTML, "http://some.domain.com", |
| "http://some.domain.com/insecure.html", NULL, NULL, NULL, NULL, |
| L"joe_user", L"fail_me", false, false, 0 }, false }, |
| }; |
| /* clang-format on */ |
| |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(true); |
| |
| for (unsigned int i = 0; i < arraysize(test_data); ++i) { |
| scoped_ptr<PasswordForm> in_form = |
| CreatePasswordFormFromDataForTesting(test_data[i].data); |
| bool add_succeeded = owned_keychain_adapter.AddPassword(*in_form); |
| EXPECT_EQ(test_data[i].should_succeed, add_succeeded); |
| if (add_succeeded) { |
| EXPECT_TRUE(owned_keychain_adapter.HasPasswordsMergeableWithForm( |
| *in_form)); |
| EXPECT_TRUE(owned_keychain_adapter.HasPasswordExactlyMatchingForm( |
| *in_form)); |
| } |
| } |
| |
| // Test that adding duplicate item updates the existing item. |
| // TODO(engedy): Add a test to verify that updating Android credentials work. |
| // See: https://crbug.com/476851. |
| { |
| PasswordFormData data = { |
| PasswordForm::SCHEME_HTML, "http://some.domain.com", |
| "http://some.domain.com/insecure.html", NULL, |
| NULL, NULL, NULL, L"joe_user", L"updated_password", false, false, 0 |
| }; |
| scoped_ptr<PasswordForm> update_form = |
| CreatePasswordFormFromDataForTesting(data); |
| MacKeychainPasswordFormAdapter keychain_adapter(keychain_); |
| EXPECT_TRUE(keychain_adapter.AddPassword(*update_form)); |
| SecKeychainItemRef keychain_item = reinterpret_cast<SecKeychainItemRef>(2); |
| PasswordForm stored_form; |
| internal_keychain_helpers::FillPasswordFormFromKeychainItem(*keychain_, |
| keychain_item, |
| &stored_form, |
| true); |
| EXPECT_EQ(update_form->password_value, stored_form.password_value); |
| } |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestKeychainRemove) { |
| struct TestDataAndExpectation { |
| PasswordFormData data; |
| bool should_succeed; |
| }; |
| /* clang-format off */ |
| TestDataAndExpectation test_data[] = { |
| // Test deletion of an item that we add. |
| { { PasswordForm::SCHEME_HTML, "http://web.site.com/", |
| "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL, |
| L"anonymous", L"knock-knock", false, false, 0 }, true }, |
| // Test that Android credentials can be removed. Also check the legacy case |
| // when |origin| was still filled with the Android URI (and not left empty). |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.example.alpha/", |
| "", NULL, NULL, NULL, NULL, |
| L"joe_user", L"secret", false, true, 0 }, true }, |
| { { PasswordForm::SCHEME_HTML, "android://hash@com.example.beta/", |
| "android://hash@com.example.beta/", NULL, NULL, NULL, NULL, |
| L"jane_user", L"secret", false, true, 0 }, true }, |
| // Make sure we don't delete items we don't own. |
| { { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/insecure.html", NULL, NULL, NULL, NULL, |
| L"joe_user", NULL, true, false, 0 }, false }, |
| }; |
| /* clang-format on */ |
| |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(true); |
| |
| // Add our test items (except the last one) so that we can delete them. |
| for (unsigned int i = 0; i + 1 < arraysize(test_data); ++i) { |
| scoped_ptr<PasswordForm> add_form = |
| CreatePasswordFormFromDataForTesting(test_data[i].data); |
| EXPECT_TRUE(owned_keychain_adapter.AddPassword(*add_form)); |
| } |
| |
| for (unsigned int i = 0; i < arraysize(test_data); ++i) { |
| scoped_ptr<PasswordForm> form = |
| CreatePasswordFormFromDataForTesting(test_data[i].data); |
| EXPECT_EQ(test_data[i].should_succeed, |
| owned_keychain_adapter.RemovePassword(*form)); |
| |
| MacKeychainPasswordFormAdapter keychain_adapter(keychain_); |
| bool match = keychain_adapter.HasPasswordExactlyMatchingForm(*form); |
| EXPECT_EQ(test_data[i].should_succeed, !match); |
| } |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestFormMatch) { |
| PasswordForm base_form; |
| base_form.signon_realm = std::string("http://some.domain.com/"); |
| base_form.origin = GURL("http://some.domain.com/page.html"); |
| base_form.username_value = ASCIIToUTF16("joe_user"); |
| |
| { |
| // Check that everything unimportant can be changed. |
| PasswordForm different_form(base_form); |
| different_form.username_element = ASCIIToUTF16("username"); |
| different_form.submit_element = ASCIIToUTF16("submit"); |
| different_form.username_element = ASCIIToUTF16("password"); |
| different_form.password_value = ASCIIToUTF16("sekrit"); |
| different_form.action = GURL("http://some.domain.com/action.cgi"); |
| different_form.ssl_valid = true; |
| different_form.preferred = true; |
| different_form.date_created = base::Time::Now(); |
| EXPECT_TRUE( |
| FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH)); |
| |
| // Check that path differences don't prevent a match. |
| base_form.origin = GURL("http://some.domain.com/other_page.html"); |
| EXPECT_TRUE( |
| FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH)); |
| } |
| |
| // Check that any one primary key changing is enough to prevent matching. |
| { |
| PasswordForm different_form(base_form); |
| different_form.scheme = PasswordForm::SCHEME_DIGEST; |
| EXPECT_FALSE( |
| FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH)); |
| } |
| { |
| PasswordForm different_form(base_form); |
| different_form.signon_realm = std::string("http://some.domain.com:8080/"); |
| EXPECT_FALSE( |
| FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH)); |
| } |
| { |
| PasswordForm different_form(base_form); |
| different_form.username_value = ASCIIToUTF16("john.doe"); |
| EXPECT_FALSE( |
| FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH)); |
| } |
| { |
| PasswordForm different_form(base_form); |
| different_form.blacklisted_by_user = true; |
| EXPECT_FALSE( |
| FormsMatchForMerge(base_form, different_form, STRICT_FORM_MATCH)); |
| } |
| |
| // Blacklist forms should *never* match for merging, even when identical |
| // (and certainly not when only one is a blacklist entry). |
| { |
| PasswordForm form_a(base_form); |
| form_a.blacklisted_by_user = true; |
| PasswordForm form_b(form_a); |
| EXPECT_FALSE(FormsMatchForMerge(form_a, form_b, STRICT_FORM_MATCH)); |
| } |
| |
| // Federated login forms should never match for merging either. |
| { |
| PasswordForm form_b(base_form); |
| form_b.federation_url = GURL(password_manager::kTestingFederationUrlSpec); |
| EXPECT_FALSE(FormsMatchForMerge(base_form, form_b, STRICT_FORM_MATCH)); |
| EXPECT_FALSE(FormsMatchForMerge(form_b, base_form, STRICT_FORM_MATCH)); |
| EXPECT_FALSE(FormsMatchForMerge(form_b, form_b, STRICT_FORM_MATCH)); |
| } |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestFormMerge) { |
| // Set up a bunch of test data to use in varying combinations. |
| /* clang-format off */ |
| PasswordFormData keychain_user_1 = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", "", L"", L"", L"", L"joe_user", L"sekrit", |
| false, false, 1010101010 }; |
| PasswordFormData keychain_user_1_with_path = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "", L"", L"", L"", L"joe_user", L"otherpassword", |
| false, false, 1010101010 }; |
| PasswordFormData keychain_user_2 = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", "", L"", L"", L"", L"john.doe", L"sesame", |
| false, false, 958739876 }; |
| PasswordFormData keychain_blacklist = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", "", L"", L"", L"", NULL, NULL, |
| false, false, 1010101010 }; |
| PasswordFormData keychain_android = |
| { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| "", "", L"", L"", L"", L"joe_user", L"secret", |
| false, true, 1234567890 }; |
| |
| PasswordFormData db_user_1 = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", "http://some.domain.com/action.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"", |
| true, false, 1212121212 }; |
| PasswordFormData db_user_1_with_path = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "http://some.domain.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"", |
| true, false, 1234567890 }; |
| PasswordFormData db_user_3_with_path = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "http://some.domain.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"second-account", L"", |
| true, false, 1240000000 }; |
| PasswordFormData database_blacklist_with_path = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/path.html", "http://some.domain.com/action.cgi", |
| L"submit", L"username", L"password", NULL, NULL, |
| true, false, 1212121212 }; |
| PasswordFormData db_android = |
| { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| "android://hash@com.domain.some/", "", L"", L"", L"", L"joe_user", L"", |
| false, true, 1234567890 }; |
| PasswordFormData db_federated = |
| { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| "android://hash@com.domain.some/", "", L"", L"", L"", L"joe_user", |
| password_manager::kTestingFederatedLoginMarker, |
| false, true, 3434343434 }; |
| |
| PasswordFormData merged_user_1 = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", "http://some.domain.com/action.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"sekrit", |
| true, false, 1212121212 }; |
| PasswordFormData merged_user_1_with_db_path = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "http://some.domain.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"sekrit", |
| true, false, 1234567890 }; |
| PasswordFormData merged_user_1_with_both_paths = |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "http://some.domain.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"otherpassword", |
| true, false, 1234567890 }; |
| PasswordFormData merged_android = |
| { PasswordForm::SCHEME_HTML, "android://hash@com.domain.some/", |
| "android://hash@com.domain.some/", "", L"", L"", L"", L"joe_user", |
| L"secret", false, true, 1234567890 }; |
| /* clang-format on */ |
| |
| // Build up the big multi-dimensional array of data sets that will actually |
| // drive the test. Use vectors rather than arrays so that initialization is |
| // simple. |
| enum { |
| KEYCHAIN_INPUT = 0, |
| DATABASE_INPUT, |
| MERGE_OUTPUT, |
| KEYCHAIN_OUTPUT, |
| DATABASE_OUTPUT, |
| MERGE_IO_ARRAY_COUNT // termination marker |
| }; |
| const unsigned int kTestCount = 5; |
| std::vector< std::vector< std::vector<PasswordFormData*> > > test_data( |
| MERGE_IO_ARRAY_COUNT, std::vector< std::vector<PasswordFormData*> >( |
| kTestCount, std::vector<PasswordFormData*>())); |
| unsigned int current_test = 0; |
| |
| // Test a merge with a few accounts in both systems, with partial overlap. |
| CHECK(current_test < kTestCount); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_2); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_user_1); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_user_1_with_path); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_user_3_with_path); |
| test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1); |
| test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1_with_db_path); |
| test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_user_2); |
| test_data[DATABASE_OUTPUT][current_test].push_back(&db_user_3_with_path); |
| |
| // Test a merge where Chrome has a blacklist entry, and the keychain has |
| // a stored account. |
| ++current_test; |
| CHECK(current_test < kTestCount); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1); |
| test_data[DATABASE_INPUT][current_test].push_back( |
| &database_blacklist_with_path); |
| // We expect both to be present because a blacklist could be specific to a |
| // subpath, and we want access to the password on other paths. |
| test_data[MERGE_OUTPUT][current_test].push_back( |
| &database_blacklist_with_path); |
| test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_user_1); |
| |
| // Test a merge where Chrome has an account, and Keychain has a blacklist |
| // (from another browser) and the Chrome password data. |
| ++current_test; |
| CHECK(current_test < kTestCount); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_blacklist); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_user_1); |
| test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1); |
| test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_blacklist); |
| |
| // Test that matches are done using exact path when possible. |
| ++current_test; |
| CHECK(current_test < kTestCount); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_user_1_with_path); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_user_1); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_user_1_with_path); |
| test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1); |
| test_data[MERGE_OUTPUT][current_test].push_back( |
| &merged_user_1_with_both_paths); |
| |
| // Test that Android credentails are matched correctly and that federated |
| // credentials are not tried to be matched with a Keychain item. |
| ++current_test; |
| CHECK(current_test < kTestCount); |
| test_data[KEYCHAIN_INPUT][current_test].push_back(&keychain_android); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_federated); |
| test_data[DATABASE_INPUT][current_test].push_back(&db_android); |
| test_data[MERGE_OUTPUT][current_test].push_back(&db_federated); |
| test_data[MERGE_OUTPUT][current_test].push_back(&merged_android); |
| |
| for (unsigned int test_case = 0; test_case <= current_test; ++test_case) { |
| ScopedVector<autofill::PasswordForm> keychain_forms; |
| for (std::vector<PasswordFormData*>::iterator i = |
| test_data[KEYCHAIN_INPUT][test_case].begin(); |
| i != test_data[KEYCHAIN_INPUT][test_case].end(); ++i) { |
| keychain_forms.push_back( |
| CreatePasswordFormFromDataForTesting(*(*i)).release()); |
| } |
| ScopedVector<autofill::PasswordForm> database_forms; |
| for (std::vector<PasswordFormData*>::iterator i = |
| test_data[DATABASE_INPUT][test_case].begin(); |
| i != test_data[DATABASE_INPUT][test_case].end(); ++i) { |
| database_forms.push_back( |
| CreatePasswordFormFromDataForTesting(*(*i)).release()); |
| } |
| |
| ScopedVector<autofill::PasswordForm> merged_forms; |
| internal_keychain_helpers::MergePasswordForms(&keychain_forms, |
| &database_forms, |
| &merged_forms); |
| |
| CHECK_FORMS(keychain_forms.get(), test_data[KEYCHAIN_OUTPUT][test_case], |
| test_case); |
| CHECK_FORMS(database_forms.get(), test_data[DATABASE_OUTPUT][test_case], |
| test_case); |
| CHECK_FORMS(merged_forms.get(), test_data[MERGE_OUTPUT][test_case], |
| test_case); |
| } |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestPasswordBulkLookup) { |
| PasswordFormData db_data[] = { |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/", "http://some.domain.com/action.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"", |
| true, false, 1212121212 }, |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "http://some.domain.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"", |
| true, false, 1234567890 }, |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/page.html", |
| "http://some.domain.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"second-account", L"", |
| true, false, 1240000000 }, |
| { PasswordForm::SCHEME_HTML, "http://dont.remember.com/", |
| "http://dont.remember.com/", |
| "http://dont.remember.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"", |
| true, false, 1240000000 }, |
| { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/path.html", "http://some.domain.com/action.cgi", |
| L"submit", L"username", L"password", NULL, NULL, |
| true, false, 1212121212 }, |
| }; |
| ScopedVector<autofill::PasswordForm> database_forms; |
| for (unsigned int i = 0; i < arraysize(db_data); ++i) { |
| database_forms.push_back( |
| CreatePasswordFormFromDataForTesting(db_data[i]).release()); |
| } |
| ScopedVector<autofill::PasswordForm> merged_forms; |
| internal_keychain_helpers::GetPasswordsForForms(*keychain_, &database_forms, |
| &merged_forms); |
| EXPECT_EQ(2U, database_forms.size()); |
| ASSERT_EQ(3U, merged_forms.size()); |
| EXPECT_EQ(ASCIIToUTF16("sekrit"), merged_forms[0]->password_value); |
| EXPECT_EQ(ASCIIToUTF16("sekrit"), merged_forms[1]->password_value); |
| EXPECT_TRUE(merged_forms[2]->blacklisted_by_user); |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestBlacklistedFiltering) { |
| PasswordFormData db_data[] = { |
| { PasswordForm::SCHEME_HTML, "http://dont.remember.com/", |
| "http://dont.remember.com/", |
| "http://dont.remember.com/handlepage.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"non_empty_password", |
| true, false, 1240000000 }, |
| { PasswordForm::SCHEME_HTML, "https://dont.remember.com/", |
| "https://dont.remember.com/", |
| "https://dont.remember.com/handlepage_secure.cgi", |
| L"submit", L"username", L"password", L"joe_user", L"non_empty_password", |
| true, false, 1240000000 }, |
| }; |
| ScopedVector<autofill::PasswordForm> database_forms; |
| for (unsigned int i = 0; i < arraysize(db_data); ++i) { |
| database_forms.push_back( |
| CreatePasswordFormFromDataForTesting(db_data[i]).release()); |
| } |
| ScopedVector<autofill::PasswordForm> merged_forms; |
| internal_keychain_helpers::GetPasswordsForForms(*keychain_, &database_forms, |
| &merged_forms); |
| EXPECT_EQ(2U, database_forms.size()); |
| ASSERT_EQ(0U, merged_forms.size()); |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestFillPasswordFormFromKeychainItem) { |
| // When |extract_password_data| is false, the password field must be empty, |
| // and |blacklisted_by_user| must be false. |
| SecKeychainItemRef keychain_item = reinterpret_cast<SecKeychainItemRef>(1); |
| PasswordForm form_without_extracted_password; |
| bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, |
| keychain_item, |
| &form_without_extracted_password, |
| false); // Do not extract password. |
| EXPECT_TRUE(parsed); |
| ASSERT_TRUE(form_without_extracted_password.password_value.empty()); |
| ASSERT_FALSE(form_without_extracted_password.blacklisted_by_user); |
| |
| // When |extract_password_data| is true and the keychain entry has a non-empty |
| // password, the password field must be non-empty, and the value of |
| // |blacklisted_by_user| must be false. |
| keychain_item = reinterpret_cast<SecKeychainItemRef>(1); |
| PasswordForm form_with_extracted_password; |
| parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, |
| keychain_item, |
| &form_with_extracted_password, |
| true); // Extract password. |
| EXPECT_TRUE(parsed); |
| ASSERT_EQ(ASCIIToUTF16("sekrit"), |
| form_with_extracted_password.password_value); |
| ASSERT_FALSE(form_with_extracted_password.blacklisted_by_user); |
| |
| // When |extract_password_data| is true and the keychain entry has an empty |
| // username and password (""), the password field must be empty, and the value |
| // of |blacklisted_by_user| must be true. |
| keychain_item = reinterpret_cast<SecKeychainItemRef>(4); |
| PasswordForm negative_form; |
| parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, |
| keychain_item, |
| &negative_form, |
| true); // Extract password. |
| EXPECT_TRUE(parsed); |
| ASSERT_TRUE(negative_form.username_value.empty()); |
| ASSERT_TRUE(negative_form.password_value.empty()); |
| ASSERT_TRUE(negative_form.blacklisted_by_user); |
| |
| // When |extract_password_data| is true and the keychain entry has an empty |
| // password (""), the password field must be empty (""), and the value of |
| // |blacklisted_by_user| must be true. |
| keychain_item = reinterpret_cast<SecKeychainItemRef>(5); |
| PasswordForm form_with_empty_password_a; |
| parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, |
| keychain_item, |
| &form_with_empty_password_a, |
| true); // Extract password. |
| EXPECT_TRUE(parsed); |
| ASSERT_TRUE(form_with_empty_password_a.password_value.empty()); |
| ASSERT_TRUE(form_with_empty_password_a.blacklisted_by_user); |
| |
| // When |extract_password_data| is true and the keychain entry has a single |
| // space password (" "), the password field must be a single space (" "), and |
| // the value of |blacklisted_by_user| must be true. |
| keychain_item = reinterpret_cast<SecKeychainItemRef>(6); |
| PasswordForm form_with_empty_password_b; |
| parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( |
| *keychain_, |
| keychain_item, |
| &form_with_empty_password_b, |
| true); // Extract password. |
| EXPECT_TRUE(parsed); |
| ASSERT_EQ(ASCIIToUTF16(" "), |
| form_with_empty_password_b.password_value); |
| ASSERT_TRUE(form_with_empty_password_b.blacklisted_by_user); |
| } |
| |
| TEST_F(PasswordStoreMacInternalsTest, TestPasswordGetAll) { |
| MacKeychainPasswordFormAdapter keychain_adapter(keychain_); |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain_); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(true); |
| |
| // Add a few passwords of various types so that we own some. |
| PasswordFormData owned_password_data[] = { |
| { PasswordForm::SCHEME_HTML, "http://web.site.com/", |
| "http://web.site.com/path/to/page.html", NULL, NULL, NULL, NULL, |
| L"anonymous", L"knock-knock", false, false, 0 }, |
| { PasswordForm::SCHEME_BASIC, "http://a.site.com:2222/therealm", |
| "http://a.site.com:2222/", NULL, NULL, NULL, NULL, |
| L"username", L"password", false, false, 0 }, |
| { PasswordForm::SCHEME_DIGEST, "https://digest.site.com/differentrealm", |
| "https://digest.site.com/secure.html", NULL, NULL, NULL, NULL, |
| L"testname", L"testpass", false, false, 0 }, |
| }; |
| for (unsigned int i = 0; i < arraysize(owned_password_data); ++i) { |
| scoped_ptr<PasswordForm> form = |
| CreatePasswordFormFromDataForTesting(owned_password_data[i]); |
| owned_keychain_adapter.AddPassword(*form); |
| } |
| |
| ScopedVector<autofill::PasswordForm> all_passwords = |
| keychain_adapter.GetAllPasswordFormPasswords(); |
| EXPECT_EQ(9 + arraysize(owned_password_data), all_passwords.size()); |
| |
| ScopedVector<autofill::PasswordForm> owned_passwords = |
| owned_keychain_adapter.GetAllPasswordFormPasswords(); |
| EXPECT_EQ(arraysize(owned_password_data), owned_passwords.size()); |
| } |
| |
| #pragma mark - |
| |
| class PasswordStoreMacTest : public testing::Test { |
| public: |
| PasswordStoreMacTest() : ui_thread_(BrowserThread::UI, &message_loop_) {} |
| |
| void SetUp() override { |
| ASSERT_TRUE(db_dir_.CreateUniqueTempDir()); |
| histogram_tester_.reset(new base::HistogramTester); |
| |
| // Ensure that LoginDatabase will use the mock keychain if it needs to |
| // encrypt/decrypt a password. |
| OSCrypt::UseMockKeychain(true); |
| login_db_.reset( |
| new password_manager::LoginDatabase(test_login_db_file_path())); |
| thread_.reset(new base::Thread("Chrome_PasswordStore_Thread")); |
| ASSERT_TRUE(thread_->Start()); |
| ASSERT_TRUE(thread_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&PasswordStoreMacTest::InitLoginDatabase, |
| base::Unretained(login_db_.get())))); |
| CreateAndInitPasswordStore(login_db_.get()); |
| // Make sure deferred initialization is performed before some tests start |
| // accessing the |login_db| directly. |
| FinishAsyncProcessing(); |
| } |
| |
| void TearDown() override { |
| ClosePasswordStore(); |
| thread_.reset(); |
| login_db_.reset(); |
| // Whatever a test did, PasswordStoreMac stores only empty password values |
| // in LoginDatabase. The empty valus do not require encryption and therefore |
| // OSCrypt shouldn't call the Keychain. The histogram doesn't cover the |
| // internet passwords. |
| if (histogram_tester_) { |
| scoped_ptr<base::HistogramSamples> samples = |
| histogram_tester_->GetHistogramSamplesSinceCreation( |
| "OSX.Keychain.Access"); |
| EXPECT_TRUE(!samples || samples->TotalCount() == 0); |
| } |
| } |
| |
| static void InitLoginDatabase(password_manager::LoginDatabase* login_db) { |
| ASSERT_TRUE(login_db->Init()); |
| } |
| |
| void CreateAndInitPasswordStore(password_manager::LoginDatabase* login_db) { |
| store_ = new PasswordStoreMac( |
| base::ThreadTaskRunnerHandle::Get(), nullptr, |
| make_scoped_ptr<AppleKeychain>(new MockAppleKeychain)); |
| ASSERT_TRUE(thread_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&PasswordStoreMac::InitWithTaskRunner, store_, |
| thread_->task_runner()))); |
| |
| ASSERT_TRUE(thread_->task_runner()->PostTask( |
| FROM_HERE, base::Bind(&PasswordStoreMac::set_login_metadata_db, store_, |
| base::Unretained(login_db)))); |
| } |
| |
| void ClosePasswordStore() { |
| if (!store_) |
| return; |
| |
| store_->Shutdown(); |
| store_ = nullptr; |
| } |
| |
| // Verifies that the given |form| can be properly stored so that it can be |
| // retrieved by FillMatchingLogins() and GetAutofillableLogins(), and then it |
| // can be properly removed. |
| void VerifyCredentialLifecycle(const PasswordForm& form) { |
| // Run everything twice to make sure no garbage is left behind that would |
| // prevent storing the form a second time. |
| for (size_t iteration = 0; iteration < 2; ++iteration) { |
| SCOPED_TRACE(testing::Message("Iteration: ") << iteration); |
| |
| MockPasswordStoreConsumer mock_consumer; |
| EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef(IsEmpty())) |
| .WillOnce(QuitUIMessageLoop()); |
| store()->GetAutofillableLogins(&mock_consumer); |
| base::MessageLoop::current()->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_consumer); |
| |
| store()->AddLogin(form); |
| FinishAsyncProcessing(); |
| |
| PasswordForm returned_form; |
| EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef(SizeIs(1u))) |
| .WillOnce( |
| DoAll(SaveACopyOfFirstForm(&returned_form), QuitUIMessageLoop())); |
| |
| // The query operations will also do some housekeeping: they will remove |
| // dangling credentials in the LoginDatabase without a matching Keychain |
| // item when one is expected. If the logic that stores the Keychain item |
| // is incorrect, this will wipe the newly added form before the second |
| // query. |
| store()->GetAutofillableLogins(&mock_consumer); |
| base::MessageLoop::current()->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_consumer); |
| EXPECT_EQ(form, returned_form); |
| |
| PasswordForm query_form = form; |
| query_form.password_value.clear(); |
| query_form.username_value.clear(); |
| EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef(SizeIs(1u))) |
| .WillOnce( |
| DoAll(SaveACopyOfFirstForm(&returned_form), QuitUIMessageLoop())); |
| store()->GetLogins(query_form, PasswordStore::ALLOW_PROMPT, |
| &mock_consumer); |
| base::MessageLoop::current()->Run(); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_consumer); |
| EXPECT_EQ(form, returned_form); |
| |
| store()->RemoveLogin(form); |
| } |
| } |
| |
| base::FilePath test_login_db_file_path() const { |
| return db_dir_.path().Append(FILE_PATH_LITERAL("login.db")); |
| } |
| |
| password_manager::LoginDatabase* login_db() const { |
| return store_->login_metadata_db(); |
| } |
| |
| MockAppleKeychain* keychain() { |
| return static_cast<MockAppleKeychain*>(store_->keychain()); |
| } |
| |
| void FinishAsyncProcessing() { |
| scoped_refptr<content::MessageLoopRunner> runner = |
| new content::MessageLoopRunner; |
| ASSERT_TRUE(thread_->task_runner()->PostTaskAndReply( |
| FROM_HERE, base::Bind(&Noop), runner->QuitClosure())); |
| runner->Run(); |
| } |
| |
| PasswordStoreMac* store() { return store_.get(); } |
| |
| protected: |
| base::MessageLoopForUI message_loop_; |
| content::TestBrowserThread ui_thread_; |
| // Thread that the synchronous methods are run on. |
| scoped_ptr<base::Thread> thread_; |
| |
| base::ScopedTempDir db_dir_; |
| scoped_ptr<password_manager::LoginDatabase> login_db_; |
| scoped_refptr<PasswordStoreMac> store_; |
| scoped_ptr<base::HistogramTester> histogram_tester_; |
| }; |
| |
| TEST_F(PasswordStoreMacTest, TestStoreUpdate) { |
| // Insert a password into both the database and the keychain. |
| // This is done manually, rather than through store_->AddLogin, because the |
| // Mock Keychain isn't smart enough to be able to support update generically, |
| // so some.domain.com triggers special handling to test it that make inserting |
| // fail. |
| PasswordFormData joint_data = { |
| PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/insecure.html", "login.cgi", |
| L"username", L"password", L"submit", L"joe_user", L"sekrit", true, false, 1 |
| }; |
| scoped_ptr<PasswordForm> joint_form = |
| CreatePasswordFormFromDataForTesting(joint_data); |
| EXPECT_EQ(AddChangeForForm(*joint_form), login_db()->AddLogin(*joint_form)); |
| MockAppleKeychain::KeychainTestData joint_keychain_data = { |
| kSecAuthenticationTypeHTMLForm, "some.domain.com", |
| kSecProtocolTypeHTTP, "/insecure.html", 0, NULL, "20020601171500Z", |
| "joe_user", "sekrit", false }; |
| keychain()->AddTestItem(joint_keychain_data); |
| |
| // Insert a password into the keychain only. |
| MockAppleKeychain::KeychainTestData keychain_only_data = { |
| kSecAuthenticationTypeHTMLForm, "keychain.only.com", |
| kSecProtocolTypeHTTP, NULL, 0, NULL, "20020601171500Z", |
| "keychain", "only", false |
| }; |
| keychain()->AddTestItem(keychain_only_data); |
| |
| struct UpdateData { |
| PasswordFormData form_data; |
| const char* password; // NULL indicates no entry should be present. |
| }; |
| |
| // Make a series of update calls. |
| UpdateData updates[] = { |
| // Update the keychain+db passwords (the normal password update case). |
| { { PasswordForm::SCHEME_HTML, "http://some.domain.com/", |
| "http://some.domain.com/insecure.html", "login.cgi", |
| L"username", L"password", L"submit", L"joe_user", L"53krit", |
| true, false, 2 }, |
| "53krit", |
| }, |
| // Update the keychain-only password; this simulates the initial use of a |
| // password stored by another browsers. |
| { { PasswordForm::SCHEME_HTML, "http://keychain.only.com/", |
| "http://keychain.only.com/login.html", "login.cgi", |
| L"username", L"password", L"submit", L"keychain", L"only", |
| true, false, 2 }, |
| "only", |
| }, |
| // Update a password that doesn't exist in either location. This tests the |
| // case where a form is filled, then the stored login is removed, then the |
| // form is submitted. |
| { { PasswordForm::SCHEME_HTML, "http://different.com/", |
| "http://different.com/index.html", "login.cgi", |
| L"username", L"password", L"submit", L"abc", L"123", |
| true, false, 2 }, |
| NULL, |
| }, |
| }; |
| for (unsigned int i = 0; i < arraysize(updates); ++i) { |
| scoped_ptr<PasswordForm> form = |
| CreatePasswordFormFromDataForTesting(updates[i].form_data); |
| store_->UpdateLogin(*form); |
| } |
| |
| FinishAsyncProcessing(); |
| |
| MacKeychainPasswordFormAdapter keychain_adapter(keychain()); |
| for (unsigned int i = 0; i < arraysize(updates); ++i) { |
| scoped_ptr<PasswordForm> query_form = |
| CreatePasswordFormFromDataForTesting(updates[i].form_data); |
| |
| ScopedVector<autofill::PasswordForm> matching_items = |
| keychain_adapter.PasswordsFillingForm(query_form->signon_realm, |
| query_form->scheme); |
| if (updates[i].password) { |
| EXPECT_GT(matching_items.size(), 0U) << "iteration " << i; |
| if (matching_items.size() >= 1) |
| EXPECT_EQ(ASCIIToUTF16(updates[i].password), |
| matching_items[0]->password_value) << "iteration " << i; |
| } else { |
| EXPECT_EQ(0U, matching_items.size()) << "iteration " << i; |
| } |
| |
| EXPECT_TRUE(login_db()->GetLogins(*query_form, &matching_items)); |
| EXPECT_EQ(updates[i].password ? 1U : 0U, matching_items.size()) |
| << "iteration " << i; |
| } |
| } |
| |
| TEST_F(PasswordStoreMacTest, TestDBKeychainAssociation) { |
| // Tests that association between the keychain and login database parts of a |
| // password added by fuzzy (PSL) matching works. |
| // 1. Add a password for www.facebook.com |
| // 2. Get a password for m.facebook.com. This fuzzy matches and returns the |
| // www.facebook.com password. |
| // 3. Add the returned password for m.facebook.com. |
| // 4. Remove both passwords. |
| // -> check: that both are gone from the login DB and the keychain |
| // This test should in particular ensure that we don't keep passwords in the |
| // keychain just before we think we still have other (fuzzy-)matching entries |
| // for them in the login database. (For example, here if we deleted the |
| // www.facebook.com password from the login database, we should not be blocked |
| // from deleting it from the keystore just becaus the m.facebook.com password |
| // fuzzy-matches the www.facebook.com one.) |
| |
| // 1. Add a password for www.facebook.com |
| PasswordFormData www_form_data = { |
| PasswordForm::SCHEME_HTML, "http://www.facebook.com/", |
| "http://www.facebook.com/index.html", "login", |
| L"username", L"password", L"submit", L"joe_user", L"sekrit", true, false, 1 |
| }; |
| scoped_ptr<PasswordForm> www_form = |
| CreatePasswordFormFromDataForTesting(www_form_data); |
| EXPECT_EQ(AddChangeForForm(*www_form), login_db()->AddLogin(*www_form)); |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain()); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(true); |
| owned_keychain_adapter.AddPassword(*www_form); |
| |
| // 2. Get a password for m.facebook.com. |
| PasswordForm m_form(*www_form); |
| m_form.signon_realm = "http://m.facebook.com"; |
| m_form.origin = GURL("http://m.facebook.com/index.html"); |
| |
| MockPasswordStoreConsumer consumer; |
| store_->GetLogins(m_form, PasswordStore::ALLOW_PROMPT, &consumer); |
| PasswordForm returned_form; |
| EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(SizeIs(1u))) |
| .WillOnce( |
| DoAll(SaveACopyOfFirstForm(&returned_form), QuitUIMessageLoop())); |
| base::MessageLoop::current()->Run(); |
| |
| // 3. Add the returned password for m.facebook.com. |
| EXPECT_EQ(AddChangeForForm(returned_form), |
| login_db()->AddLogin(returned_form)); |
| owned_keychain_adapter.AddPassword(m_form); |
| |
| // 4. Remove both passwords. |
| store_->RemoveLogin(*www_form); |
| store_->RemoveLogin(m_form); |
| FinishAsyncProcessing(); |
| |
| // No trace of www.facebook.com. |
| ScopedVector<autofill::PasswordForm> matching_items = |
| owned_keychain_adapter.PasswordsFillingForm(www_form->signon_realm, |
| www_form->scheme); |
| EXPECT_EQ(0u, matching_items.size()); |
| EXPECT_TRUE(login_db()->GetLogins(*www_form, &matching_items)); |
| EXPECT_EQ(0u, matching_items.size()); |
| // No trace of m.facebook.com. |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| m_form.signon_realm, m_form.scheme); |
| EXPECT_EQ(0u, matching_items.size()); |
| EXPECT_TRUE(login_db()->GetLogins(m_form, &matching_items)); |
| EXPECT_EQ(0u, matching_items.size()); |
| } |
| |
| namespace { |
| |
| class PasswordsChangeObserver : |
| public password_manager::PasswordStore::Observer { |
| public: |
| PasswordsChangeObserver(PasswordStoreMac* store) : observer_(this) { |
| observer_.Add(store); |
| } |
| |
| void WaitAndVerify(PasswordStoreMacTest* test) { |
| test->FinishAsyncProcessing(); |
| ::testing::Mock::VerifyAndClearExpectations(this); |
| } |
| |
| // password_manager::PasswordStore::Observer: |
| MOCK_METHOD1(OnLoginsChanged, |
| void(const password_manager::PasswordStoreChangeList& changes)); |
| |
| private: |
| ScopedObserver<password_manager::PasswordStore, |
| PasswordsChangeObserver> observer_; |
| }; |
| |
| password_manager::PasswordStoreChangeList GetAddChangeList( |
| const PasswordForm& form) { |
| password_manager::PasswordStoreChange change( |
| password_manager::PasswordStoreChange::ADD, form); |
| return password_manager::PasswordStoreChangeList(1, change); |
| } |
| |
| // Tests RemoveLoginsCreatedBetween or RemoveLoginsSyncedBetween depending on |
| // |check_created|. |
| void CheckRemoveLoginsBetween(PasswordStoreMacTest* test, bool check_created) { |
| PasswordFormData www_form_data_facebook = { |
| PasswordForm::SCHEME_HTML, "http://www.facebook.com/", |
| "http://www.facebook.com/index.html", "login", L"submit", L"username", |
| L"password", L"joe_user", L"sekrit", true, false, 0 }; |
| // The old form doesn't have elements names. |
| PasswordFormData www_form_data_facebook_old = { |
| PasswordForm::SCHEME_HTML, "http://www.facebook.com/", |
| "http://www.facebook.com/index.html", "login", L"", L"", |
| L"", L"joe_user", L"oldsekrit", true, false, 0 }; |
| PasswordFormData www_form_data_other = { |
| PasswordForm::SCHEME_HTML, "http://different.com/", |
| "http://different.com/index.html", "login", L"submit", L"username", |
| L"password", L"different_joe_user", L"sekrit", true, false, 0 }; |
| scoped_ptr<PasswordForm> form_facebook = |
| CreatePasswordFormFromDataForTesting(www_form_data_facebook); |
| scoped_ptr<PasswordForm> form_facebook_old = |
| CreatePasswordFormFromDataForTesting(www_form_data_facebook_old); |
| scoped_ptr<PasswordForm> form_other = |
| CreatePasswordFormFromDataForTesting(www_form_data_other); |
| base::Time now = base::Time::Now(); |
| // TODO(vasilii): remove the next line once crbug/374132 is fixed. |
| now = base::Time::FromTimeT(now.ToTimeT()); |
| base::Time next_day = now + base::TimeDelta::FromDays(1); |
| if (check_created) { |
| form_facebook_old->date_created = now; |
| form_facebook->date_created = next_day; |
| form_other->date_created = next_day; |
| } else { |
| form_facebook_old->date_synced = now; |
| form_facebook->date_synced = next_day; |
| form_other->date_synced = next_day; |
| } |
| |
| PasswordsChangeObserver observer(test->store()); |
| test->store()->AddLogin(*form_facebook_old); |
| test->store()->AddLogin(*form_facebook); |
| test->store()->AddLogin(*form_other); |
| EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_facebook_old))); |
| EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_facebook))); |
| EXPECT_CALL(observer, OnLoginsChanged(GetAddChangeList(*form_other))); |
| observer.WaitAndVerify(test); |
| |
| // Check the keychain content. |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(test->keychain()); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(false); |
| ScopedVector<PasswordForm> matching_items( |
| owned_keychain_adapter.PasswordsFillingForm(form_facebook->signon_realm, |
| form_facebook->scheme)); |
| EXPECT_EQ(1u, matching_items.size()); |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| form_other->signon_realm, form_other->scheme); |
| EXPECT_EQ(1u, matching_items.size()); |
| |
| // Remove facebook. |
| if (check_created) { |
| test->store()->RemoveLoginsCreatedBetween(base::Time(), next_day, |
| base::Closure()); |
| } else { |
| test->store()->RemoveLoginsSyncedBetween(base::Time(), next_day); |
| } |
| password_manager::PasswordStoreChangeList list; |
| form_facebook_old->password_value.clear(); |
| form_facebook->password_value.clear(); |
| list.push_back(password_manager::PasswordStoreChange( |
| password_manager::PasswordStoreChange::REMOVE, *form_facebook_old)); |
| list.push_back(password_manager::PasswordStoreChange( |
| password_manager::PasswordStoreChange::REMOVE, *form_facebook)); |
| EXPECT_CALL(observer, OnLoginsChanged(list)); |
| list.clear(); |
| observer.WaitAndVerify(test); |
| |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| form_facebook->signon_realm, form_facebook->scheme); |
| EXPECT_EQ(0u, matching_items.size()); |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| form_other->signon_realm, form_other->scheme); |
| EXPECT_EQ(1u, matching_items.size()); |
| |
| // Remove form_other. |
| if (check_created) { |
| test->store()->RemoveLoginsCreatedBetween(next_day, base::Time(), |
| base::Closure()); |
| } else { |
| test->store()->RemoveLoginsSyncedBetween(next_day, base::Time()); |
| } |
| form_other->password_value.clear(); |
| list.push_back(password_manager::PasswordStoreChange( |
| password_manager::PasswordStoreChange::REMOVE, *form_other)); |
| EXPECT_CALL(observer, OnLoginsChanged(list)); |
| observer.WaitAndVerify(test); |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| form_other->signon_realm, form_other->scheme); |
| EXPECT_EQ(0u, matching_items.size()); |
| } |
| |
| } // namespace |
| |
| TEST_F(PasswordStoreMacTest, TestRemoveLoginsCreatedBetween) { |
| CheckRemoveLoginsBetween(this, true); |
| } |
| |
| TEST_F(PasswordStoreMacTest, TestRemoveLoginsSyncedBetween) { |
| CheckRemoveLoginsBetween(this, false); |
| } |
| |
| TEST_F(PasswordStoreMacTest, TestRemoveLoginsMultiProfile) { |
| // Make sure that RemoveLoginsCreatedBetween does affect only the correct |
| // profile. |
| |
| // Add a third-party password. |
| MockAppleKeychain::KeychainTestData keychain_data = { |
| kSecAuthenticationTypeHTMLForm, "some.domain.com", |
| kSecProtocolTypeHTTP, "/insecure.html", 0, NULL, "20020601171500Z", |
| "joe_user", "sekrit", false }; |
| keychain()->AddTestItem(keychain_data); |
| |
| // Add a password through the adapter. It has the "Chrome" creator tag. |
| // However, it's not referenced by the password database. |
| MacKeychainPasswordFormAdapter owned_keychain_adapter(keychain()); |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(true); |
| PasswordFormData www_form_data1 = { |
| PasswordForm::SCHEME_HTML, "http://www.facebook.com/", |
| "http://www.facebook.com/index.html", "login", L"username", L"password", |
| L"submit", L"joe_user", L"sekrit", true, false, 1 }; |
| scoped_ptr<PasswordForm> www_form = |
| CreatePasswordFormFromDataForTesting(www_form_data1); |
| EXPECT_TRUE(owned_keychain_adapter.AddPassword(*www_form)); |
| |
| // Add a password from the current profile. |
| PasswordFormData www_form_data2 = { |
| PasswordForm::SCHEME_HTML, "http://www.facebook.com/", |
| "http://www.facebook.com/index.html", "login", L"username", L"password", |
| L"submit", L"not_joe_user", L"12345", true, false, 1 }; |
| www_form = CreatePasswordFormFromDataForTesting(www_form_data2); |
| store_->AddLogin(*www_form); |
| FinishAsyncProcessing(); |
| |
| ScopedVector<PasswordForm> matching_items; |
| EXPECT_TRUE(login_db()->GetLogins(*www_form, &matching_items)); |
| EXPECT_EQ(1u, matching_items.size()); |
| |
| store_->RemoveLoginsCreatedBetween(base::Time(), base::Time(), |
| base::Closure()); |
| FinishAsyncProcessing(); |
| |
| // Check the second facebook form is gone. |
| EXPECT_TRUE(login_db()->GetLogins(*www_form, &matching_items)); |
| EXPECT_EQ(0u, matching_items.size()); |
| |
| // Check the first facebook form is still there. |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| www_form->signon_realm, www_form->scheme); |
| ASSERT_EQ(1u, matching_items.size()); |
| EXPECT_EQ(ASCIIToUTF16("joe_user"), matching_items[0]->username_value); |
| |
| // Check the third-party password is still there. |
| owned_keychain_adapter.SetFindsOnlyOwnedItems(false); |
| matching_items = owned_keychain_adapter.PasswordsFillingForm( |
| "http://some.domain.com/insecure.html", PasswordForm::SCHEME_HTML); |
| ASSERT_EQ(1u, matching_items.size()); |
| } |
| |
| // Add a facebook form to the store but not to the keychain. The form is to be |
| // implicitly deleted. However, the observers shouldn't get notified about |
| // deletion of non-existent forms like m.facebook.com. |
| TEST_F(PasswordStoreMacTest, SilentlyRemoveOrphanedForm) { |
| testing::StrictMock<MockPasswordStoreObserver> mock_observer; |
| store()->AddObserver(&mock_observer); |
| |
| // 1. Add a password for www.facebook.com to the LoginDatabase. |
| PasswordFormData www_form_data = { |
| PasswordForm::SCHEME_HTML, "http://www.facebook.com/", |
| "http://www.facebook.com/index.html", "login", |
| L"username", L"password", L"submit", L"joe_user", L"", true, false, 1 |
| }; |
| scoped_ptr<PasswordForm> www_form( |
| CreatePasswordFormFromDataForTesting(www_form_data)); |
| EXPECT_EQ(AddChangeForForm(*www_form), login_db()->AddLogin(*www_form)); |
| |
| // 2. Get a PSL-matched password for m.facebook.com. The observer isn't |
| // notified because the form isn't in the database. |
| PasswordForm m_form(*www_form); |
| m_form.signon_realm = "http://m.facebook.com"; |
| m_form.origin = GURL("http://m.facebook.com/index.html"); |
| |
| MockPasswordStoreConsumer consumer; |
| ON_CALL(consumer, OnGetPasswordStoreResultsConstRef(_)) |
| .WillByDefault(QuitUIMessageLoop()); |
| EXPECT_CALL(mock_observer, OnLoginsChanged(_)).Times(0); |
| // The PSL-matched form isn't returned because there is no actual password in |
| // the keychain. |
| EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(IsEmpty())); |
| store_->GetLogins(m_form, PasswordStore::ALLOW_PROMPT, &consumer); |
| base::MessageLoop::current()->Run(); |
| ScopedVector<autofill::PasswordForm> all_forms; |
| EXPECT_TRUE(login_db()->GetAutofillableLogins(&all_forms)); |
| EXPECT_EQ(1u, all_forms.size()); |
| ::testing::Mock::VerifyAndClearExpectations(&mock_observer); |
| |
| // 3. Get a password for www.facebook.com. The form is implicitly removed and |
| // the observer is notified. |
| password_manager::PasswordStoreChangeList list; |
| list.push_back(password_manager::PasswordStoreChange( |
| password_manager::PasswordStoreChange::REMOVE, *www_form)); |
| EXPECT_CALL(mock_observer, OnLoginsChanged(list)); |
| EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(IsEmpty())); |
| store_->GetLogins(*www_form, PasswordStore::ALLOW_PROMPT, &consumer); |
| base::MessageLoop::current()->Run(); |
| EXPECT_TRUE(login_db()->GetAutofillableLogins(&all_forms)); |
| EXPECT_EQ(0u, all_forms.size()); |
| } |
| |
| // Verify that Android app passwords can be stored, retrieved, and deleted. |
| // Regression test for http://crbug.com/455551 |
| TEST_F(PasswordStoreMacTest, StoringAndRetrievingAndroidCredentials) { |
| PasswordForm form; |
| form.signon_realm = "android://7x7IDboo8u9YKraUsbmVkuf1@net.rateflix.app/"; |
| form.username_value = base::UTF8ToUTF16("randomusername"); |
| form.password_value = base::UTF8ToUTF16("password"); |
| |
| VerifyCredentialLifecycle(form); |
| } |
| |
| // Verify that federated credentials can be stored, retrieved and deleted. |
| TEST_F(PasswordStoreMacTest, StoringAndRetrievingFederatedCredentials) { |
| PasswordForm form; |
| form.signon_realm = "android://7x7IDboo8u9YKraUsbmVkuf1@net.rateflix.app/"; |
| form.federation_url = GURL(password_manager::kTestingFederationUrlSpec); |
| form.username_value = base::UTF8ToUTF16("randomusername"); |
| form.password_value = base::UTF8ToUTF16(""); // No password. |
| |
| VerifyCredentialLifecycle(form); |
| } |
| |
| void CheckMigrationResult(PasswordStoreMac::MigrationResult expected_result, |
| PasswordStoreMac::MigrationResult result) { |
| EXPECT_EQ(expected_result, result); |
| QuitUIMessageLoop(); |
| } |
| |
| // Import the passwords from the Keychain to LoginDatabase. |
| TEST_F(PasswordStoreMacTest, ImportFromKeychain) { |
| PasswordForm form1; |
| form1.origin = GURL("http://accounts.google.com/LoginAuth"); |
| form1.signon_realm = "http://accounts.google.com/"; |
| form1.username_value = ASCIIToUTF16("my_username"); |
| form1.password_value = ASCIIToUTF16("my_password"); |
| |
| PasswordForm form2; |
| form2.origin = GURL("http://facebook.com/Login"); |
| form2.signon_realm = "http://facebook.com/"; |
| form2.username_value = ASCIIToUTF16("my_username"); |
| form2.password_value = ASCIIToUTF16("my_password"); |
| |
| PasswordForm blacklisted_form; |
| blacklisted_form.origin = GURL("http://badsite.com/Login"); |
| blacklisted_form.signon_realm = "http://badsite.com/"; |
| blacklisted_form.blacklisted_by_user = true; |
| |
| store()->AddLogin(form1); |
| store()->AddLogin(form2); |
| store()->AddLogin(blacklisted_form); |
| FinishAsyncProcessing(); |
| |
| ASSERT_TRUE(base::PostTaskAndReplyWithResult( |
| thread_->task_runner().get(), FROM_HERE, |
| base::Bind(&PasswordStoreMac::ImportFromKeychain, store()), |
| base::Bind(&CheckMigrationResult, PasswordStoreMac::MIGRATION_OK))); |
| FinishAsyncProcessing(); |
| |
| // The password should be stored in the database by now. |
| ScopedVector<PasswordForm> matching_items; |
| EXPECT_TRUE(login_db()->GetLogins(form1, &matching_items)); |
| ASSERT_EQ(1u, matching_items.size()); |
| EXPECT_EQ(form1, *matching_items[0]); |
| |
| EXPECT_TRUE(login_db()->GetLogins(form2, &matching_items)); |
| ASSERT_EQ(1u, matching_items.size()); |
| EXPECT_EQ(form2, *matching_items[0]); |
| |
| EXPECT_TRUE(login_db()->GetLogins(blacklisted_form, &matching_items)); |
| ASSERT_EQ(1u, matching_items.size()); |
| EXPECT_EQ(blacklisted_form, *matching_items[0]); |
| |
| // The passwords are encrypted using a key from the Keychain. |
| EXPECT_TRUE(histogram_tester_->GetHistogramSamplesSinceCreation( |
| "OSX.Keychain.Access")->TotalCount()); |
| histogram_tester_.reset(); |
| } |
| |
| // Import a federated credential while the Keychain is locked. |
| TEST_F(PasswordStoreMacTest, ImportFederatedFromLockedKeychain) { |
| keychain()->set_locked(true); |
| PasswordForm form1; |
| form1.origin = GURL("http://example.com/Login"); |
| form1.signon_realm = "http://example.com/"; |
| form1.username_value = ASCIIToUTF16("my_username"); |
| form1.federation_url = GURL("https://accounts.google.com/"); |
| |
| store()->AddLogin(form1); |
| FinishAsyncProcessing(); |
| ASSERT_TRUE(base::PostTaskAndReplyWithResult( |
| thread_->task_runner().get(), FROM_HERE, |
| base::Bind(&PasswordStoreMac::ImportFromKeychain, store()), |
| base::Bind(&CheckMigrationResult, PasswordStoreMac::MIGRATION_OK))); |
| FinishAsyncProcessing(); |
| |
| ScopedVector<PasswordForm> matching_items; |
| EXPECT_TRUE(login_db()->GetLogins(form1, &matching_items)); |
| ASSERT_EQ(1u, matching_items.size()); |
| EXPECT_EQ(form1, *matching_items[0]); |
| } |
| |
| // Try to import while the Keychain is locked but the encryption key had been |
| // read earlier. |
| TEST_F(PasswordStoreMacTest, ImportFromLockedKeychainError) { |
| PasswordForm form1; |
| form1.origin = GURL("http://accounts.google.com/LoginAuth"); |
| form1.signon_realm = "http://accounts.google.com/"; |
| form1.username_value = ASCIIToUTF16("my_username"); |
| form1.password_value = ASCIIToUTF16("my_password"); |
| store()->AddLogin(form1); |
| FinishAsyncProcessing(); |
| |
| // Add a second keychain item matching the Database entry. |
| PasswordForm form2 = form1; |
| form2.origin = GURL("http://accounts.google.com/Login"); |
| form2.password_value = ASCIIToUTF16("1234"); |
| MacKeychainPasswordFormAdapter adapter(keychain()); |
| EXPECT_TRUE(adapter.AddPassword(form2)); |
| |
| keychain()->set_locked(true); |
| ASSERT_TRUE(base::PostTaskAndReplyWithResult( |
| thread_->task_runner().get(), FROM_HERE, |
| base::Bind(&PasswordStoreMac::ImportFromKeychain, store()), |
| base::Bind(&CheckMigrationResult, PasswordStoreMac::KEYCHAIN_BLOCKED))); |
| FinishAsyncProcessing(); |
| |
| ScopedVector<PasswordForm> matching_items; |
| EXPECT_TRUE(login_db()->GetLogins(form1, &matching_items)); |
| ASSERT_EQ(1u, matching_items.size()); |
| EXPECT_EQ(base::string16(), matching_items[0]->password_value); |
| |
| histogram_tester_->ExpectUniqueSample( |
| "PasswordManager.KeychainMigration.NumPasswordsOnFailure", 1, 1); |
| histogram_tester_->ExpectUniqueSample( |
| "PasswordManager.KeychainMigration.NumFailedPasswords", 1, 1); |
| histogram_tester_->ExpectUniqueSample( |
| "PasswordManager.KeychainMigration.NumChromeOwnedInaccessiblePasswords", |
| 2, 1); |
| // Don't test the encryption key access. |
| histogram_tester_.reset(); |
| } |