diff options
author | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-01-15 01:35:45 +0000 |
---|---|---|
committer | ben@chromium.org <ben@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-01-15 01:35:45 +0000 |
commit | d330185b649cc66f60362c01cbead62e5b2545ba (patch) | |
tree | e71046177bf83524e11051db7698a0a588181eb2 /chrome/browser/password_manager | |
parent | 7236eb109ef090be1163c384584714c7d139ed98 (diff) | |
download | chromium_src-d330185b649cc66f60362c01cbead62e5b2545ba.zip chromium_src-d330185b649cc66f60362c01cbead62e5b2545ba.tar.gz chromium_src-d330185b649cc66f60362c01cbead62e5b2545ba.tar.bz2 |
Move password manager files into a subdir
Review URL: http://codereview.chromium.org/18259
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@8066 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/password_manager')
-rw-r--r-- | chrome/browser/password_manager/encryptor.cc | 65 | ||||
-rw-r--r-- | chrome/browser/password_manager/encryptor.h | 44 | ||||
-rw-r--r-- | chrome/browser/password_manager/encryptor_unittest.cc | 79 | ||||
-rw-r--r-- | chrome/browser/password_manager/ie7_password.cc | 198 | ||||
-rw-r--r-- | chrome/browser/password_manager/ie7_password.h | 47 | ||||
-rw-r--r-- | chrome/browser/password_manager/password_form_manager.cc | 514 | ||||
-rw-r--r-- | chrome/browser/password_manager/password_form_manager.h | 166 | ||||
-rw-r--r-- | chrome/browser/password_manager/password_form_manager_unittest.cc | 196 | ||||
-rw-r--r-- | chrome/browser/password_manager/password_manager.cc | 219 | ||||
-rw-r--r-- | chrome/browser/password_manager/password_manager.h | 123 |
10 files changed, 1651 insertions, 0 deletions
diff --git a/chrome/browser/password_manager/encryptor.cc b/chrome/browser/password_manager/encryptor.cc new file mode 100644 index 0000000..b5c0755 --- /dev/null +++ b/chrome/browser/password_manager/encryptor.cc @@ -0,0 +1,65 @@ +// Copyright (c) 2006-2008 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/encryptor.h" + +#include <windows.h> +#include <wincrypt.h> +#include "base/string_util.h" + +#pragma comment(lib, "crypt32.lib") + +bool Encryptor::EncryptWideString(const std::wstring& plaintext, + std::string* ciphertext) { + return EncryptString(WideToUTF8(plaintext), ciphertext); +} + +bool Encryptor::DecryptWideString(const std::string& ciphertext, + std::wstring* plaintext){ + std::string utf8; + if (!DecryptString(ciphertext, &utf8)) + return false; + + *plaintext = UTF8ToWide(utf8); + return true; +} + +bool Encryptor::EncryptString(const std::string& plaintext, + std::string* ciphertext) { + DATA_BLOB input; + input.pbData = const_cast<BYTE*>( + reinterpret_cast<const BYTE*>(plaintext.data())); + input.cbData = static_cast<DWORD>(plaintext.length()); + + DATA_BLOB output; + BOOL result = CryptProtectData(&input, L"", NULL, NULL, NULL, + 0, &output); + if (!result) + return false; + + // this does a copy + ciphertext->assign(reinterpret_cast<std::string::value_type*>(output.pbData), + output.cbData); + + LocalFree(output.pbData); + return true; +} + +bool Encryptor::DecryptString(const std::string& ciphertext, + std::string* plaintext){ + DATA_BLOB input; + input.pbData = const_cast<BYTE*>( + reinterpret_cast<const BYTE*>(ciphertext.data())); + input.cbData = static_cast<DWORD>(ciphertext.length()); + + DATA_BLOB output; + BOOL result = CryptUnprotectData(&input, NULL, NULL, NULL, NULL, + 0, &output); + if(!result) + return false; + + plaintext->assign(reinterpret_cast<char*>(output.pbData), output.cbData); + LocalFree(output.pbData); + return true; +} diff --git a/chrome/browser/password_manager/encryptor.h b/chrome/browser/password_manager/encryptor.h new file mode 100644 index 0000000..7233960 --- /dev/null +++ b/chrome/browser/password_manager/encryptor.h @@ -0,0 +1,44 @@ +// Copyright (c) 2006-2008 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. +// A class for encrypting/decrypting strings + +#ifndef CHROME_BROWSER_ENCRYPTOR_H__ +#define CHROME_BROWSER_ENCRYPTOR_H__ + +#include <string> + +#include "base/values.h" + +class Encryptor { +public: + // Encrypt a wstring. The output (second argument) is + // really an array of bytes, but we're passing it back + // as a std::string + static bool EncryptWideString(const std::wstring& plaintext, + std::string* ciphertext); + + // Decrypt an array of bytes obtained with EnctryptWideString + // back into a wstring. Note that the input (first argument) + // is a std::string, so you need to first get your (binary) + // data into a string. + static bool DecryptWideString(const std::string& ciphertext, + std::wstring* plaintext); + + // Encrypt a string. + static bool EncryptString(const std::string& plaintext, + std::string* ciphertext); + + // Decrypt an array of bytes obtained with EnctryptString + // back into a string. Note that the input (first argument) + // is a std::string, so you need to first get your (binary) + // data into a string. + static bool DecryptString(const std::string& ciphertext, + std::string* plaintext); + +private: + DISALLOW_IMPLICIT_CONSTRUCTORS(Encryptor); +}; + +#endif // CHROME_BROWSER_ENCRYPTOR_H__ + diff --git a/chrome/browser/password_manager/encryptor_unittest.cc b/chrome/browser/password_manager/encryptor_unittest.cc new file mode 100644 index 0000000..a1bbd36 --- /dev/null +++ b/chrome/browser/password_manager/encryptor_unittest.cc @@ -0,0 +1,79 @@ +// Copyright (c) 2006-2008 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/encryptor.h" + +#include <string> + +#include "base/string_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +TEST(EncryptorTest, WideEncryptionDecryption) { + std::wstring plaintext; + std::wstring result; + std::string utf8_plaintext; + std::string utf8_result; + std::string ciphertext; + + // test borderline cases (empty strings) + EXPECT_TRUE(Encryptor::EncryptWideString(plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptWideString(ciphertext, &result)); + EXPECT_EQ(plaintext, result); + + // test a simple string + plaintext = L"hello"; + EXPECT_TRUE(Encryptor::EncryptWideString(plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptWideString(ciphertext, &result)); + EXPECT_EQ(plaintext, result); + + // test unicode + std::wstring::value_type wchars[] = { 0xdbeb,0xdf1b,0x4e03,0x6708,0x8849, + 0x661f,0x671f,0x56db,0x597c,0x4e03, + 0x6708,0x56db,0x6708,0xe407,0xdbaf, + 0xdeb5,0x4ec5,0x544b,0x661f,0x671f, + 0x65e5,0x661f,0x671f,0x4e94,0xd8b1, + 0xdce1,0x7052,0x5095,0x7c0b,0xe586, 0}; + plaintext = wchars; + utf8_plaintext = WideToUTF8(plaintext); + EXPECT_EQ(plaintext, UTF8ToWide(utf8_plaintext)); + EXPECT_TRUE(Encryptor::EncryptWideString(plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptWideString(ciphertext, &result)); + EXPECT_EQ(plaintext, result); + EXPECT_TRUE(Encryptor::DecryptString(ciphertext, &utf8_result)); + EXPECT_EQ(utf8_plaintext, WideToUTF8(result)); + + EXPECT_TRUE(Encryptor::EncryptString(utf8_plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptWideString(ciphertext, &result)); + EXPECT_EQ(plaintext, result); + EXPECT_TRUE(Encryptor::DecryptString(ciphertext, &utf8_result)); + EXPECT_EQ(utf8_plaintext, WideToUTF8(result)); +} + +TEST(EncryptorTest, EncryptionDecryption) { + std::string plaintext; + std::string result; + std::string ciphertext; + + // test borderline cases (empty strings) + EXPECT_TRUE(Encryptor::EncryptString(plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptString(ciphertext, &result)); + EXPECT_EQ(plaintext, result); + + // test a simple string + plaintext = "hello"; + EXPECT_TRUE(Encryptor::EncryptString(plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptString(ciphertext, &result)); + EXPECT_EQ(plaintext, result); + + // Make sure it null terminates. + plaintext.assign("hello", 3); + EXPECT_TRUE(Encryptor::EncryptString(plaintext, &ciphertext)); + EXPECT_TRUE(Encryptor::DecryptString(ciphertext, &result)); + EXPECT_EQ(plaintext, "hel"); +} + +} // namespace + diff --git a/chrome/browser/password_manager/ie7_password.cc b/chrome/browser/password_manager/ie7_password.cc new file mode 100644 index 0000000..c448f92 --- /dev/null +++ b/chrome/browser/password_manager/ie7_password.cc @@ -0,0 +1,198 @@ +// Copyright (c) 2006-2008 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/ie7_password.h" + +#include <wincrypt.h> +#include <string> +#include <vector> + +#include "base/logging.h" +#include "base/scoped_ptr.h" +#include "base/string_util.h" + +namespace { + +// Structures that IE7/IE8 use to store a username/password. +// Some of the fields might have been incorrectly reverse engineered. +struct PreHeader { + DWORD pre_header_size; // Size of this header structure. Always 12. + DWORD header_size; // Size of the real Header: sizeof(Header) + + // item_count * sizeof(Entry); + DWORD data_size; // Size of the data referenced by the entries. +}; + +struct Header { + char wick[4]; // The string "WICK". I don't know what it means. + DWORD fixed_header_size; // The size of this structure without the entries: + // sizeof(Header). + DWORD item_count; // Number of entries. It should always be 2. One for + // the username, and one for the password. + wchar_t two_letters[2]; // Two unknown bytes. + DWORD unknown[2]; // Two unknown DWORDs. +}; + +struct Entry { + DWORD offset; // Offset where the data referenced by this entry is + // located. + FILETIME time_stamp; // Timestamp when the password got added. + DWORD string_length; // The length of the data string. +}; + +// Main data structure. +struct PasswordEntry { + PreHeader pre_header; // Contains the size of the different sections. + Header header; // Contains the number of items. + Entry entry[1]; // List of entries containing a string. The first one + // is the username, the second one if the password. +}; + +// Cleans up a crypt prov and a crypt hash. +void CleanupHashContext(HCRYPTPROV prov, HCRYPTHASH hash) { + if (hash) + CryptDestroyHash(hash); + if (prov) + CryptReleaseContext(prov, 0); +} + +} // namespace + +namespace ie7_password { + +bool GetUserPassFromData(const std::vector<unsigned char>& data, + std::wstring* username, + std::wstring* password) { + const PasswordEntry* information = + reinterpret_cast<const PasswordEntry*>(&data.front()); + + // Some expected values. If it's not what we expect we don't even try to + // understand the data. + if (information->pre_header.pre_header_size != sizeof(PreHeader)) + return false; + + if (information->header.item_count != 2) // Username and Password + return false; + + if (information->header.fixed_header_size != sizeof(Header)) + return false; + + const uint8* ptr = &data.front(); + const uint8* offset_to_data = ptr + information->pre_header.header_size + + information->pre_header.pre_header_size; + + const Entry* user_entry = information->entry; + const Entry* pass_entry = user_entry+1; + + *username = reinterpret_cast<const wchar_t*>(offset_to_data + + user_entry->offset); + *password = reinterpret_cast<const wchar_t*>(offset_to_data + + pass_entry->offset); + return true; +} + +std::wstring GetUrlHash(const std::wstring& url) { + HCRYPTPROV prov = NULL; + HCRYPTHASH hash = NULL; + + std::wstring lower_case_url = StringToLowerASCII(url); + BOOL result = CryptAcquireContext(&prov, 0, 0, PROV_RSA_FULL, 0); + if (!result) { + if (GetLastError() == NTE_BAD_KEYSET) { + // The keyset does not exist. Create one. + result = CryptAcquireContext(&prov, 0, 0, PROV_RSA_FULL, CRYPT_NEWKEYSET); + } + } + + if (!result) { + DCHECK(false); + return std::wstring(); + } + + // Initialize the hash. + if (!CryptCreateHash(prov, CALG_SHA1, 0, 0, &hash)) { + CleanupHashContext(prov, hash); + DCHECK(false); + return std::wstring(); + } + + // Add the data to the hash. + const unsigned char* buffer = + reinterpret_cast<const unsigned char*>(lower_case_url.c_str()); + DWORD data_len = static_cast<DWORD>((lower_case_url.size() + 1) * + sizeof(wchar_t)); + if (!CryptHashData(hash, buffer, data_len, 0)) { + CleanupHashContext(prov, hash); + DCHECK(false); + return std::wstring(); + } + + // Get the size of the resulting hash. + DWORD hash_len = 0; + DWORD buffer_size = sizeof(hash_len); + if (!CryptGetHashParam(hash, HP_HASHSIZE, + reinterpret_cast<unsigned char*>(&hash_len), + &buffer_size, 0)) { + CleanupHashContext(prov, hash); + DCHECK(false); + return std::wstring(); + } + + // Get the hash data. + scoped_array<unsigned char> new_buffer(new unsigned char[hash_len]); + if (!CryptGetHashParam(hash, HP_HASHVAL, new_buffer.get(), &hash_len, 0)) { + CleanupHashContext(prov, hash); + DCHECK(false); + return std::wstring(); + } + + std::wstring url_hash; + + // Transform the buffer to an hexadecimal string. + unsigned char checksum = 0; + for (DWORD i = 0; i < hash_len; ++i) { + checksum += new_buffer.get()[i]; + url_hash += StringPrintf(L"%2.2X", new_buffer.get()[i]); + } + + url_hash += StringPrintf(L"%2.2X", checksum); + + CleanupHashContext(prov, hash); + return url_hash; +} + +bool DecryptPassword(const std::wstring& url, + const std::vector<unsigned char>& data, + std::wstring* username, std::wstring* password) { + std::wstring lower_case_url = StringToLowerASCII(url); + DATA_BLOB input = {0}; + DATA_BLOB output = {0}; + DATA_BLOB url_key = {0}; + + input.pbData = const_cast<unsigned char*>(&data.front()); + input.cbData = static_cast<DWORD>((data.size()) * + sizeof(std::string::value_type)); + + url_key.pbData = reinterpret_cast<unsigned char*>( + const_cast<wchar_t*>(lower_case_url.data())); + url_key.cbData = static_cast<DWORD>((lower_case_url.size() + 1) * + sizeof(std::wstring::value_type)); + + if (CryptUnprotectData(&input, NULL, &url_key, NULL, NULL, + CRYPTPROTECT_UI_FORBIDDEN, &output)) { + // Now that we have the decrypted information, we need to understand it. + std::vector<unsigned char> decrypted_data; + decrypted_data.resize(output.cbData); + memcpy(&decrypted_data.front(), output.pbData, output.cbData); + + GetUserPassFromData(decrypted_data, username, password); + + LocalFree(output.pbData); + return true; + } + + return false; +} + +} // namespace ie7_password + diff --git a/chrome/browser/password_manager/ie7_password.h b/chrome/browser/password_manager/ie7_password.h new file mode 100644 index 0000000..c90b8e2 --- /dev/null +++ b/chrome/browser/password_manager/ie7_password.h @@ -0,0 +1,47 @@ +// Copyright (c) 2006-2008 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. + +#ifndef CHROME_BROWSER_IE7_PASSWORD_H__ +#define CHROME_BROWSER_IE7_PASSWORD_H__ + +#include <windows.h> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/time.h" + +// Contains the information read from the IE7/IE8 Storage2 key in the registry. +struct IE7PasswordInfo { + // Hash of the url. + std::wstring url_hash; + + // Encrypted data containing the username, password and some more undocumented + // fields. + std::vector<unsigned char> encrypted_data; + + // When the login was imported. + base::Time date_created; +}; + +namespace ie7_password { + +// Parses a data structure to find the password and the username. +bool GetUserPassFromData(const std::vector<unsigned char>& data, + std::wstring* username, + std::wstring* password); + +// Decrypts the username and password for a given data vector using the url as +// the key. +bool DecryptPassword(const std::wstring& url, + const std::vector<unsigned char>& data, + std::wstring* username, std::wstring* password); + +// Returns the hash of a url. +std::wstring GetUrlHash(const std::wstring& url); + +} // namespace ie7_password + +#endif // CHROME_BROWSER_IE7_PASSWORD_H__ + diff --git a/chrome/browser/password_manager/password_form_manager.cc b/chrome/browser/password_manager/password_form_manager.cc new file mode 100644 index 0000000..757dc63 --- /dev/null +++ b/chrome/browser/password_manager/password_form_manager.cc @@ -0,0 +1,514 @@ +// Copyright (c) 2006-2008 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_form_manager.h" + +#include <algorithm> + +#include "base/string_util.h" +#include "chrome/browser/password_manager/ie7_password.h" +#include "chrome/browser/password_manager/password_manager.h" +#include "chrome/browser/profile.h" +#include "webkit/glue/password_form_dom_manager.h" + +using base::Time; + +PasswordFormManager::PasswordFormManager(Profile* profile, + PasswordManager* password_manager, + const PasswordForm& observed_form, + bool ssl_valid) + : observed_form_(observed_form), + password_manager_(password_manager), + profile_(profile), + is_new_login_(true), + pending_login_query_(NULL), + preferred_match_(NULL), + best_matches_deleter_(&best_matches_), + state_(PRE_MATCHING_PHASE) { + DCHECK(profile_); + if (observed_form_.origin.is_valid()) + SplitString(observed_form_.origin.path(), '/', &form_path_tokens_); + observed_form_.ssl_valid = ssl_valid; +} + +PasswordFormManager::~PasswordFormManager() { + CancelLoginsQuery(); +} + +// TODO(timsteele): use a hash of some sort in the future? +bool PasswordFormManager::DoesManage(const PasswordForm& form) const { + if (form.scheme != PasswordForm::SCHEME_HTML) + return observed_form_.signon_realm == form.signon_realm; + + // HTML form case. + // At a minimum, username and password element must match. + if (!((form.username_element == observed_form_.username_element) && + (form.password_element == observed_form_.password_element))) { + return false; + } + + // The action URL must also match, but the form is allowed to have an empty + // action URL (See bug 1107719). + if (form.action.is_valid() && (form.action != observed_form_.action)) + return false; + + // If this is a replay of the same form in the case a user entered an invalid + // password, the origin of the new form may equal the action of the "first" + // form. + if (!((form.origin == observed_form_.origin) || + (form.origin == observed_form_.action))) { + if (form.origin.SchemeIsSecure() && + !observed_form_.origin.SchemeIsSecure()) { + // Compare origins, ignoring scheme. There is no easy way to do this + // with GURL because clearing the scheme would result in an invalid url. + // This is for some sites (such as Hotmail) that begin on an http page and + // head to https for the retry when password was invalid. + std::string::const_iterator after_scheme1 = form.origin.spec().begin() + + form.origin.scheme().length(); + std::string::const_iterator after_scheme2 = + observed_form_.origin.spec().begin() + + observed_form_.origin.scheme().length(); + return std::search(after_scheme1, + form.origin.spec().end(), + after_scheme2, + observed_form_.origin.spec().end()) + != form.origin.spec().end(); + } + return false; + } + return true; +} + +bool PasswordFormManager::IsBlacklisted() { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + if (preferred_match_ && preferred_match_->blacklisted_by_user) + return true; + return false; +} + +void PasswordFormManager::PermanentlyBlacklist() { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + + // Configure the form about to be saved for blacklist status. + pending_credentials_.preferred = true; + pending_credentials_.blacklisted_by_user = true; + pending_credentials_.username_value.clear(); + pending_credentials_.password_value.clear(); + + // Retroactively forget existing matches for this form, so we NEVER prompt or + // autofill it again. + if (!best_matches_.empty()) { + PasswordFormMap::const_iterator iter; + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::EXPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + for (iter = best_matches_.begin(); iter != best_matches_.end(); ++iter) { + // We want to remove existing matches for this form so that the exact + // origin match with |blackisted_by_user == true| is the only result that + // shows up in the future for this origin URL. However, we don't want to + // delete logins that were actually saved on a different page (hence with + // different origin URL) and just happened to match this form because of + // the scoring algorithm. See bug 1204493. + if (iter->second->origin == observed_form_.origin) + web_data_service->RemoveLogin(*iter->second); + } + } + + // Save the pending_credentials_ entry marked as blacklisted. + SaveAsNewLogin(); +} + +bool PasswordFormManager::IsNewLogin() { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + return is_new_login_; +} + +void PasswordFormManager::ProvisionallySave(const PasswordForm& credentials) { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + DCHECK(DoesManage(credentials)); + + // Make sure the important fields stay the same as the initially observed or + // autofilled ones, as they may have changed if the user experienced a login + // failure. + // Look for these credentials in the list containing auto-fill entries. + PasswordFormMap::const_iterator it = + best_matches_.find(credentials.username_value); + if (it != best_matches_.end()) { + // The user signed in with a login we autofilled. + pending_credentials_ = *it->second; + is_new_login_ = false; + // If the user selected credentials we autofilled from a PasswordForm + // that contained no action URL (IE6/7 imported passwords, for example), + // bless it with the action URL from the observed form. See bug 1107719. + if (pending_credentials_.action.is_empty()) + pending_credentials_.action = observed_form_.action; + } else { + pending_credentials_ = observed_form_; + pending_credentials_.username_value = credentials.username_value; + } + + pending_credentials_.password_value = credentials.password_value; + pending_credentials_.preferred = credentials.preferred; +} + +void PasswordFormManager::Save() { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + DCHECK(!profile_->IsOffTheRecord()); + + if (IsNewLogin()) + SaveAsNewLogin(); + else + UpdateLogin(); +} + +void PasswordFormManager::FetchMatchingLoginsFromWebDatabase() { + DCHECK_EQ(state_, PRE_MATCHING_PHASE); + DCHECK(!pending_login_query_); + state_ = MATCHING_PHASE; + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::EXPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + pending_login_query_ = web_data_service->GetLogins(observed_form_, this); +} + +void PasswordFormManager::FetchMatchingIE7LoginFromWebDatabase() { + DCHECK_EQ(state_, PRE_MATCHING_PHASE); + DCHECK(!pending_login_query_); + state_ = MATCHING_PHASE; + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::EXPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + + IE7PasswordInfo info; + std::wstring url = ASCIIToWide(observed_form_.origin.spec()); + info.url_hash = ie7_password::GetUrlHash(url); + pending_login_query_ = web_data_service->GetIE7Login(info, this); +} + +bool PasswordFormManager::HasCompletedMatching() { + return state_ == POST_MATCHING_PHASE; +} + +void PasswordFormManager::OnRequestDone(WebDataService::Handle h, + const WDTypedResult* result) { + // Get the result from the database into a usable form. + const WDResult<std::vector<PasswordForm*> >* r = + static_cast<const WDResult<std::vector<PasswordForm*> >*>(result); + std::vector<PasswordForm*> logins_result = r->GetValue(); + // Note that the result gets deleted after this call completes, but we own + // the PasswordForm objects pointed to by the result vector, thus we keep + // copies to a minimum here. + + int best_score = 0; + std::vector<PasswordForm> empties; // Empty-path matches in result set. + for (size_t i = 0; i < logins_result.size(); i++) { + if (IgnoreResult(*logins_result[i])) { + delete logins_result[i]; + continue; + } + // Score and update best matches. + int current_score = ScoreResult(*logins_result[i]); + // This check is here so we can append empty path matches in the event + // they don't score as high as others and aren't added to best_matches_. + // This is most commonly imported firefox logins. We skip blacklisted + // ones because clearly we don't want to autofill them, and secondly + // because they only mean something when we have no other matches already + // saved in Chrome - in which case they'll make it through the regular + // scoring flow below by design. Note signon_realm == origin implies empty + // path logins_result, since signon_realm is a prefix of origin for HTML + // password forms. + // TODO(timsteele): Bug 1269400. We probably should do something more + // elegant for any shorter-path match instead of explicitly handling empty + // path matches. + if ((observed_form_.scheme == PasswordForm::SCHEME_HTML) && + (observed_form_.signon_realm == logins_result[i]->origin.spec()) && + (current_score > 0) && (!logins_result[i]->blacklisted_by_user)){ + empties.push_back(*logins_result[i]); + } + + if (current_score < best_score) { + delete logins_result[i]; + continue; + } + if (current_score == best_score) { + best_matches_[logins_result[i]->username_value] = logins_result[i]; + } else if (current_score > best_score) { + best_score = current_score; + // This new login has a better score than all those up to this point + // Note 'this' owns all the PasswordForms in best_matches_. + STLDeleteValues(&best_matches_); + best_matches_.clear(); + preferred_match_ = NULL; // Don't delete, its owned by best_matches_. + best_matches_[logins_result[i]->username_value] = logins_result[i]; + } + preferred_match_ = logins_result[i]->preferred ? logins_result[i] + : preferred_match_; + } + // We're done matching now. + state_ = POST_MATCHING_PHASE; + + if (best_score <= 0) { + state_ = PRE_MATCHING_PHASE; + FetchMatchingIE7LoginFromWebDatabase(); + return; + } + + for (std::vector<PasswordForm>::const_iterator it = empties.begin(); + it != empties.end(); ++it) { + // If we don't already have a result with the same username, add the + // lower-scored empty-path match (if it had equal score it would already be + // in best_matches_). + if (best_matches_.find(it->username_value) == best_matches_.end()) + best_matches_[it->username_value] = new PasswordForm(*it); + } + + // Its possible we have at least one match but have no preferred_match_, + // because a user may have chosen to 'Forget' the preferred match. So we + // just pick the first one and whichever the user selects for submit will + // be saved as preferred. + DCHECK(!best_matches_.empty()); + if (!preferred_match_) + preferred_match_ = best_matches_.begin()->second; + + // Now we determine if the user told us to ignore this site in the past. + // If they haven't, we proceed to auto-fill. + if (!preferred_match_->blacklisted_by_user) { + password_manager_->Autofill(observed_form_, best_matches_, + preferred_match_); + } +} + +void PasswordFormManager::OnIE7RequestDone(WebDataService::Handle h, + const WDTypedResult* result) { + // Get the result from the database into a usable form. + const WDResult<IE7PasswordInfo>* r = + static_cast<const WDResult<IE7PasswordInfo>*>(result); + IE7PasswordInfo result_value = r->GetValue(); + + state_ = POST_MATCHING_PHASE; + + if (!result_value.encrypted_data.empty()) { + // We got a result. + // Delete the entry. If it's good we will add it to the real saved password + // table. + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::EXPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + web_data_service->RemoveIE7Login(result_value); + + std::wstring username; + std::wstring password; + std::wstring url = ASCIIToWide(observed_form_.origin.spec()); + if (!ie7_password::DecryptPassword(url, result_value.encrypted_data, + &username, &password)) { + return; + } + + PasswordForm* auto_fill = new PasswordForm(observed_form_); + auto_fill->username_value = username; + auto_fill->password_value = password; + auto_fill->preferred = true; + auto_fill->ssl_valid = observed_form_.origin.SchemeIsSecure(); + auto_fill->date_created = result_value.date_created; + // Add this PasswordForm to the saved password table. + web_data_service->AddLogin(*auto_fill); + + if (IgnoreResult(*auto_fill)) { + delete auto_fill; + return; + } + + best_matches_[auto_fill->username_value] = auto_fill; + preferred_match_ = auto_fill; + password_manager_->Autofill(observed_form_, best_matches_, + preferred_match_); + } +} + +void PasswordFormManager::OnWebDataServiceRequestDone(WebDataService::Handle h, + const WDTypedResult* result) { + DCHECK_EQ(state_, MATCHING_PHASE); + DCHECK_EQ(pending_login_query_, h); + DCHECK(result); + pending_login_query_ = NULL; + + if (!result) + return; + + switch (result->GetType()) { + case PASSWORD_RESULT: { + OnRequestDone(h, result); + break; + } + case PASSWORD_IE7_RESULT: { + OnIE7RequestDone(h, result); + break; + } + default: + NOTREACHED(); + } +} + +bool PasswordFormManager::IgnoreResult(const PasswordForm& form) const { + // Ignore change password forms until we have some change password + // functionality + if (observed_form_.old_password_element.length() != 0) { + return true; + } + // Don't match an invalid SSL form with one saved under secure + // circumstances. + if (form.ssl_valid && !observed_form_.ssl_valid) { + return true; + } + return false; +} + +void PasswordFormManager::SaveAsNewLogin() { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + DCHECK(IsNewLogin()); + // The new_form is being used to sign in, so it is preferred. + DCHECK(pending_credentials_.preferred); + // new_form contains the same basic data as observed_form_ (because its the + // same form), but with the newly added credentials. + + DCHECK(!profile_->IsOffTheRecord()); + + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::IMPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + pending_credentials_.date_created = Time::Now(); + web_data_service->AddLogin(pending_credentials_); +} + +void PasswordFormManager::UpdateLogin() { + DCHECK_EQ(state_, POST_MATCHING_PHASE); + DCHECK(preferred_match_); + // If we're doing an Update, its because we autofilled a form and the user + // submitted it with a possibly new password value, page security, or selected + // one of the non-preferred matches, thus requiring a swap of preferred bits. + DCHECK(!IsNewLogin() && pending_credentials_.preferred); + DCHECK(!profile_->IsOffTheRecord()); + + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::IMPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + + // Update all matches to reflect new preferred status. + PasswordFormMap::iterator iter; + for (iter = best_matches_.begin(); iter != best_matches_.end(); iter++) { + if ((iter->second->username_value != pending_credentials_.username_value) && + iter->second->preferred) { + // This wasn't the selected login but it used to be preferred. + iter->second->preferred = false; + web_data_service->UpdateLogin(*iter->second); + } + } + // Update the new preferred login. + // Note origin.spec().length > signon_realm.length implies the origin has a + // path, since signon_realm is a prefix of origin for HTML password forms. + if ((observed_form_.scheme == PasswordForm::SCHEME_HTML) && + (observed_form_.origin.spec().length() > + observed_form_.signon_realm.length()) && + (observed_form_.signon_realm == pending_credentials_.origin.spec())) { + // The user logged in successfully with one of our autofilled logins on a + // page with non-empty path, but the autofilled entry was initially saved/ + // imported with an empty path. Rather than just mark this entry preferred, + // we create a more specific copy for this exact page and leave the "master" + // unchanged. This is to prevent the case where that master login is used + // on several sites (e.g site.com/a and site.com/b) but the user actually + // has a different preference on each site. For example, on /a, he wants the + // general empty-path login so it is flagged as preferred, but on /b he logs + // in with a different saved entry - we don't want to remove the preferred + // status of the former because upon return to /a it won't be the default- + // fill match. + // TODO(timsteele): Bug 1188626 - expire the master copies. + PasswordForm copy(pending_credentials_); + copy.origin = observed_form_.origin; + copy.action = observed_form_.action; + web_data_service->AddLogin(copy); + } else { + web_data_service->UpdateLogin(pending_credentials_); + } +} + +void PasswordFormManager::CancelLoginsQuery() { + if (!pending_login_query_) + return; + WebDataService* web_data_service = + profile_->GetWebDataService(Profile::EXPLICIT_ACCESS); + if (!web_data_service) { + NOTREACHED(); + return; + } + web_data_service->CancelRequest(pending_login_query_); + pending_login_query_ = NULL; +} + +int PasswordFormManager::ScoreResult(const PasswordForm& candidate) const { + DCHECK_EQ(state_, MATCHING_PHASE); + // For scoring of candidate login data: + // The most important element that should match is the origin, followed by + // the action, the password name, the submit button name, and finally the + // username input field name. + // Exact origin match gives an addition of 32 (1 << 5) + # of matching url + // dirs. + // Partial match gives an addition of 16 (1 << 4) + # matching url dirs + // That way, a partial match cannot trump an exact match even if + // the partial one matches all other attributes (action, elements) (and + // regardless of the matching depth in the URL path). + int score = 0; + if (candidate.origin == observed_form_.origin) { + // This check is here for the most common case which + // is we have a single match in the db for the given host, + // so we don't generally need to walk the entire URL path (the else + // clause). + score += (1 << 5) + static_cast<int>(form_path_tokens_.size()); + } else { + // Walk the origin URL paths one directory at a time to see how + // deep the two match. + std::vector<std::string> candidate_path_tokens; + SplitString(candidate.origin.path(), '/', &candidate_path_tokens); + size_t depth = 0; + size_t max_dirs = std::min(form_path_tokens_.size(), + candidate_path_tokens.size()); + while ((depth < max_dirs) && (form_path_tokens_[depth] == + candidate_path_tokens[depth])) { + depth++; + score++; + } + // do we have a partial match? + score += (depth > 0) ? 1 << 4 : 0; + } + if (observed_form_.scheme == PasswordForm::SCHEME_HTML) { + if (candidate.action == observed_form_.action) + score += 1 << 3; + if (candidate.password_element == observed_form_.password_element) + score += 1 << 2; + if (candidate.submit_element == observed_form_.submit_element) + score += 1 << 1; + if (candidate.username_element == observed_form_.username_element) + score += 1 << 0; + } + + return score; +} + diff --git a/chrome/browser/password_manager/password_form_manager.h b/chrome/browser/password_manager/password_form_manager.h new file mode 100644 index 0000000..9d0d8a6 --- /dev/null +++ b/chrome/browser/password_manager/password_form_manager.h @@ -0,0 +1,166 @@ +// Copyright (c) 2006-2008 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. + +#ifndef CHROME_BROWSER_PASSWORD_FORM_MANAGER_H__ +#define CHROME_BROWSER_PASSWORD_FORM_MANAGER_H__ + +#include "chrome/common/stl_util-inl.h" +#include "chrome/browser/webdata/web_data_service.h" +#include "webkit/glue/password_form.h" + +class PasswordManager; +class Profile; + +// Per-password-form-{on-page, dialog} class responsible for interactions +// between a given form, the per-tab PasswordManager, and the web database. +class PasswordFormManager : public WebDataServiceConsumer { + public: + // web_data_service allows access to current profile's Web Data + // password_manager owns this object + // form_on_page is the form that may be submitted and could need login data. + // ssl_valid represents the security of the page containing observed_form, + // used to filter login results from database. + PasswordFormManager(Profile* profile, + PasswordManager* password_manager, + const PasswordForm& observed_form, + bool ssl_valid); + virtual ~PasswordFormManager(); + + // Compare basic data of observed_form_ with argument. + bool DoesManage(const PasswordForm& form) const; + + // Retrieves potential matching logins from the database. + void FetchMatchingLoginsFromWebDatabase(); + void FetchMatchingIE7LoginFromWebDatabase(); + + // Simple state-check to verify whether this object as received a callback + // from the web database and completed its matching phase. Note that the + // callback in question occurs on the same (and only) main thread from which + // instances of this class are ever used, but it is required since it is + // conceivable that a user (or ui test) could attempt to submit a login + // prompt before the callback has occured, which would InvokeLater a call to + // PasswordManager::ProvisionallySave, which would interact with this object + // before the db has had time to answer with matching password entries. + // This is intended to be a one-time check; if the return value is false the + // expectation is caller will give up. This clearly won't work if you put it + // in a loop and wait for matching to complete; you're (supposed to be) on + // the same thread! + bool HasCompletedMatching(); + + // Determines if the user opted to 'never remember' passwords for this form. + bool IsBlacklisted(); + + // Used by PasswordManager to determine whether or not to display + // a SavePasswordBar when given the green light to save the PasswordForm + // managed by this. + bool IsNewLogin(); + + // WebDataServiceConsumer implementation. If matches were found + // (in *result), this is where we determine we need to autofill. + virtual void OnWebDataServiceRequestDone(WebDataService::Handle h, + const WDTypedResult* result); + + // Determines if we need to autofill given the results of the query. + void OnRequestDone(WebDataService::Handle h, const WDTypedResult* result); + + // Determines if we need to autofill given the results of the query in the + // ie7_password table. + void OnIE7RequestDone(WebDataService::Handle h, const WDTypedResult* result); + + // A user opted to 'never remember' passwords for this form. + // Blacklist it so that from now on when it is seen we ignore it. + void PermanentlyBlacklist(); + + // If the user has submitted observed_form_, provisionally hold on to + // the submitted credentials until we are told by PasswordManager whether + // or not the login was successful. + void ProvisionallySave(const PasswordForm& credentials); + + // Handles save-as-new or update of the form managed by this manager. + // Note the basic data of updated_credentials must match that of + // observed_form_ (e.g DoesManage(pending_credentials_) == true). + void Save(); + + private: + friend class PasswordFormManagerTest; + // Called by destructor to ensure if this object is deleted, no potential + // outstanding callbacks can call OnWebDataServiceRequestDone. + void CancelLoginsQuery(); + + // Helper for OnWebDataServiceRequestDone to determine whether or not + // the given result form is worth scoring. + bool IgnoreResult(const PasswordForm& form) const; + + // Helper for Save in the case that best_matches.size() == 0, meaning + // we have no prior record of this form/username/password and the user + // has opted to 'Save Password'. + void SaveAsNewLogin(); + + // Helper for OnWebDataServiceRequestDone to score an individual result + // against the observed_form_. + int ScoreResult(const PasswordForm& form) const; + + // Helper for Save in the case that best_matches.size() > 0, meaning + // we have at least one match for this form/username/password. This + // Updates the form managed by this object, as well as any matching forms + // that now need to have preferred bit changed, since updated_credentials + // is now implicitly 'preferred'. + void UpdateLogin(); + + // Set of PasswordForms from the DB that best match the form + // being managed by this. Use a map instead of vector, because we most + // frequently require lookups by username value in IsNewLogin. + PasswordFormMap best_matches_; + + // Cleans up when best_matches_ goes out of scope. + STLValueDeleter<PasswordFormMap> best_matches_deleter_; + + // The PasswordForm from the page or dialog managed by this. + PasswordForm observed_form_; + + // The origin url path of observed_form_ tokenized, for convenience when + // scoring. + std::vector<std::string> form_path_tokens_; + + // Stores updated credentials when the form was submitted but success is + // still unknown. + PasswordForm pending_credentials_; + + // Whether pending_credentials_ stores a new login or is an update + // to an existing one. + bool is_new_login_; + + // PasswordManager owning this. + const PasswordManager* const password_manager_; + + // Handle to any pending WebDataService::GetLogins query. + WebDataService::Handle pending_login_query_; + + // Convenience pointer to entry in best_matches_ that is marked + // as preferred. This is only allowed to be null if there are no best matches + // at all, since there will always be one preferred login when there are + // multiple matches (when first saved, a login is marked preferred). + const PasswordForm* preferred_match_; + + typedef enum { + PRE_MATCHING_PHASE, // Have not yet invoked a GetLogins query to find + // matching login information from DB. + MATCHING_PHASE, // We've made a GetLogins request, but + // haven't received or finished processing result. + POST_MATCHING_PHASE // We've queried the DB and processed matching + // login results. + } PasswordFormManagerState; + + // State of matching process, used to verify that we don't call methods + // assuming we've already processed the web data request for matching logins, + // when we actually haven't. + PasswordFormManagerState state_; + + // The profile from which we get the WebDataService. + Profile* profile_; + + DISALLOW_EVIL_CONSTRUCTORS(PasswordFormManager); +}; +#endif // CHROME_BROWSER_PASSWORD_FORM_MANAGER_H__ + diff --git a/chrome/browser/password_manager/password_form_manager_unittest.cc b/chrome/browser/password_manager/password_form_manager_unittest.cc new file mode 100644 index 0000000..ffcee20 --- /dev/null +++ b/chrome/browser/password_manager/password_form_manager_unittest.cc @@ -0,0 +1,196 @@ +// Copyright (c) 2006-2008 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 "testing/gtest/include/gtest/gtest.h" + +#include "chrome/browser/password_manager/password_form_manager.h" +#include "chrome/browser/password_manager/password_manager.h" +#include "chrome/browser/profile_manager.h" +#include "chrome/test/testing_profile.h" +#include "webkit/glue/password_form.h" + +class PasswordFormManagerTest : public testing::Test { + public: + PasswordFormManagerTest() { + } + virtual void SetUp() { + observed_form_.origin = GURL(L"http://www.google.com/a/LoginAuth"); + observed_form_.action = GURL(L"http://www.google.com/a/Login"); + observed_form_.username_element = L"Email"; + observed_form_.password_element = L"Passwd"; + observed_form_.submit_element = L"signIn"; + observed_form_.signon_realm = "http://www.google.com"; + + saved_match_ = observed_form_; + saved_match_.origin = GURL(L"http://www.google.com/a/ServiceLoginAuth"); + saved_match_.action = GURL(L"http://www.google.com/a/ServiceLogin"); + saved_match_.preferred = true; + saved_match_.username_value = L"test@gmail.com"; + saved_match_.password_value = L"test1"; + profile_ = new TestingProfile(); + } + + virtual void TearDown() { + delete profile_; + } + + PasswordForm* GetPendingCredentials(PasswordFormManager* p) { + return &p->pending_credentials_; + } + + void SimulateMatchingPhase(PasswordFormManager* p, bool find_match) { + // Roll up the state to mock out the matching phase. + p->state_ = PasswordFormManager::POST_MATCHING_PHASE; + if (!find_match) + return; + + PasswordForm* match = new PasswordForm(saved_match_); + // Heap-allocated form is owned by p. + p->best_matches_[match->username_value] = match; + p->preferred_match_ = match; + } + + bool IgnoredResult(PasswordFormManager* p, PasswordForm* form) { + return p->IgnoreResult(*form); + } + + Profile* profile() { return profile_; } + + PasswordForm* observed_form() { return &observed_form_; } + PasswordForm* saved_match() { return &saved_match_; } + + private: + PasswordForm observed_form_; + PasswordForm saved_match_; + std::wstring test_dir_; + Profile* profile_; +}; + +TEST_F(PasswordFormManagerTest, TestNewLogin) { + PasswordFormManager* manager = new PasswordFormManager( + profile(), NULL, *observed_form(), false); + SimulateMatchingPhase(manager, false); + // User submits credentials for the observed form. + PasswordForm credentials = *observed_form(); + credentials.username_value = saved_match()->username_value; + credentials.password_value = saved_match()->password_value; + credentials.preferred = true; + manager->ProvisionallySave(credentials); + + // Successful login. The PasswordManager would instruct PasswordFormManager + // to save, which should know this is a new login. + EXPECT_TRUE(manager->IsNewLogin()); + // Make sure the credentials that would be submitted on successful login + // are going to match the stored entry in the db. + EXPECT_EQ(observed_form()->origin.spec(), + GetPendingCredentials(manager)->origin.spec()); + EXPECT_EQ(observed_form()->signon_realm, + GetPendingCredentials(manager)->signon_realm); + EXPECT_TRUE(GetPendingCredentials(manager)->preferred); + EXPECT_EQ(saved_match()->password_value, + GetPendingCredentials(manager)->password_value); + EXPECT_EQ(saved_match()->username_value, + GetPendingCredentials(manager)->username_value); + + // Now, suppose the user re-visits the site and wants to save an additional + // login for the site with a new username. In this case, the matching phase + // will yield the previously saved login. + SimulateMatchingPhase(manager, true); + // Set up the new login. + std::wstring new_user = L"newuser"; + std::wstring new_pass = L"newpass"; + credentials.username_value = new_user; + credentials.password_value = new_pass; + manager->ProvisionallySave(credentials); + + // Again, the PasswordFormManager should know this is still a new login. + EXPECT_TRUE(manager->IsNewLogin()); + // And make sure everything squares up again. + EXPECT_EQ(observed_form()->origin.spec(), + GetPendingCredentials(manager)->origin.spec()); + EXPECT_EQ(observed_form()->signon_realm, + GetPendingCredentials(manager)->signon_realm); + EXPECT_TRUE(GetPendingCredentials(manager)->preferred); + EXPECT_EQ(new_pass, + GetPendingCredentials(manager)->password_value); + EXPECT_EQ(new_user, + GetPendingCredentials(manager)->username_value); + // Done. + delete manager; +} + +TEST_F(PasswordFormManagerTest, TestUpdatePassword) { + // Create a PasswordFormManager with observed_form, as if we just + // saw this form and need to find matching logins. + PasswordFormManager* manager = new PasswordFormManager( + profile(), NULL, *observed_form(), false); + SimulateMatchingPhase(manager, true); + + // User submits credentials for the observed form using a username previously + // stored, but a new password. Note that the observed form may have different + // origin URL (as it does in this case) than the saved_match, but we want to + // make sure the updated password is reflected in saved_match, because that is + // what we autofilled. + std::wstring new_pass = L"newpassword"; + PasswordForm credentials = *observed_form(); + credentials.username_value = saved_match()->username_value; + credentials.password_value = new_pass; + credentials.preferred = true; + manager->ProvisionallySave(credentials); + + // Successful login. The PasswordManager would instruct PasswordFormManager + // to save, and since this is an update, it should know not to save as a new + // login. + EXPECT_FALSE(manager->IsNewLogin()); + + // Make sure the credentials that would be submitted on successful login + // are going to match the stored entry in the db. (This verifies correct + // behaviour for bug 1074420). + EXPECT_EQ(GetPendingCredentials(manager)->origin.spec(), + saved_match()->origin.spec()); + EXPECT_EQ(GetPendingCredentials(manager)->signon_realm, + saved_match()->signon_realm); + EXPECT_TRUE(GetPendingCredentials(manager)->preferred); + EXPECT_EQ(new_pass, + GetPendingCredentials(manager)->password_value); + // Done. + delete manager; +} + +TEST_F(PasswordFormManagerTest, TestIgnoreResult) { + PasswordFormManager* manager = new PasswordFormManager( + profile(), NULL, *observed_form(), false); + // Make sure we don't match a PasswordForm if it was originally saved on + // an SSL-valid page and we are now on a page with invalid certificate. + saved_match()->ssl_valid = true; + EXPECT_TRUE(IgnoredResult(manager, saved_match())); + + saved_match()->ssl_valid = false; + // Different paths for action / origin are okay. + saved_match()->action = GURL(L"http://www.google.com/b/Login"); + saved_match()->origin = GURL(L"http://www.google.com/foo"); + EXPECT_FALSE(IgnoredResult(manager, saved_match())); + + // Done. + delete manager; +} + +TEST_F(PasswordFormManagerTest, TestEmptyAction) { + scoped_ptr<PasswordFormManager> manager(new PasswordFormManager( + profile(), NULL, *observed_form(), false)); + + saved_match()->action = GURL(); + SimulateMatchingPhase(manager.get(), true); + // User logs in with the autofilled username / password from saved_match. + PasswordForm login = *observed_form(); + login.username_value = saved_match()->username_value; + login.password_value = saved_match()->password_value; + manager->ProvisionallySave(login); + EXPECT_FALSE(manager->IsNewLogin()); + // We bless our saved PasswordForm entry with the action URL of the + // observed form. + EXPECT_EQ(observed_form()->action, + GetPendingCredentials(manager.get())->action); +} + diff --git a/chrome/browser/password_manager/password_manager.cc b/chrome/browser/password_manager/password_manager.cc new file mode 100644 index 0000000..4187151 --- /dev/null +++ b/chrome/browser/password_manager/password_manager.cc @@ -0,0 +1,219 @@ +// Copyright (c) 2006-2008 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_manager.h" + +#include "base/string_util.h" +#include "chrome/app/theme/theme_resources.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/tab_contents/web_contents.h" +#include "chrome/common/l10n_util.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/pref_service.h" +#include "chrome/common/resource_bundle.h" +#include "chrome/common/stl_util-inl.h" + +#include "chromium_strings.h" +#include "generated_resources.h" + +// static +void PasswordManager::RegisterUserPrefs(PrefService* prefs) { + prefs->RegisterBooleanPref(prefs::kPasswordManagerEnabled, true); +} + +PasswordManager::PasswordManager(WebContents* web_contents) + : ConfirmInfoBarDelegate(web_contents), + web_contents_(web_contents), + observer_(NULL), + login_managers_deleter_(&pending_login_managers_) { + password_manager_enabled_.Init(prefs::kPasswordManagerEnabled, + web_contents->profile()->GetPrefs(), NULL); +} + +PasswordManager::~PasswordManager() { + // Remove any InfoBars we may be showing. + web_contents_->RemoveInfoBar(this); +} + +void PasswordManager::ProvisionallySavePassword(PasswordForm form) { + if (!web_contents_->controller() || !web_contents_->profile() || + web_contents_->profile()->IsOffTheRecord() || !*password_manager_enabled_) + return; + + // No password to save? Then don't. + if (form.password_value.empty()) + return; + + LoginManagers::iterator iter; + PasswordFormManager* manager = NULL; + for (iter = pending_login_managers_.begin(); + iter != pending_login_managers_.end(); iter++) { + if ((*iter)->DoesManage(form)) { + manager = *iter; + break; + } + } + // If we didn't find a manager, this means a form was submitted without + // first loading the page containing the form. Don't offer to save + // passwords in this case. + if (!manager) + return; + + // If we found a manager but it didn't finish matching yet, the user has + // tried to submit credentials before we had time to even find matching + // results for the given form and autofill. If this is the case, we just + // give up. + if (!manager->HasCompletedMatching()) + return; + + // Also get out of here if the user told us to 'never remember' passwords for + // this form. + if (manager->IsBlacklisted()) + return; + + form.ssl_valid = form.origin.SchemeIsSecure() && + !web_contents_->controller()->ssl_manager()-> + ProcessedSSLErrorFromRequest(); + form.preferred = true; + manager->ProvisionallySave(form); + provisional_save_manager_.reset(manager); + pending_login_managers_.erase(iter); + // We don't care about the rest of the forms on the page now that one + // was selected. + STLDeleteElements(&pending_login_managers_); +} + +void PasswordManager::DidNavigate() { + // As long as this navigation isn't due to a currently pending + // password form submit, we're ready to reset and move on. + if (!provisional_save_manager_.get() && !pending_login_managers_.empty()) + STLDeleteElements(&pending_login_managers_); +} + +void PasswordManager::ClearProvisionalSave() { + provisional_save_manager_.reset(); +} + +void PasswordManager::DidStopLoading() { + if (!provisional_save_manager_.get()) + return; + + DCHECK(!web_contents_->profile()->IsOffTheRecord()); + DCHECK(!provisional_save_manager_->IsBlacklisted()); + + if (!web_contents_->profile() || + !web_contents_->profile()->GetWebDataService(Profile::IMPLICIT_ACCESS)) + return; + if (!web_contents_->controller()) + return; + + if (provisional_save_manager_->IsNewLogin()) { + web_contents_->AddInfoBar(this); + pending_decision_manager_.reset(provisional_save_manager_.release()); + } else { + // If the save is not a new username entry, then we just want to save this + // data (since the user already has related data saved), so don't prompt. + provisional_save_manager_->Save(); + provisional_save_manager_.reset(); + } +} + +void PasswordManager::PasswordFormsSeen(const std::vector<PasswordForm>& forms) { + if (!web_contents_->profile() || + !web_contents_->profile()->GetWebDataService(Profile::EXPLICIT_ACCESS)) + return; + if (!web_contents_->controller()) + return; + if (!*password_manager_enabled_) + return; + + // Ask the SSLManager for current security. + bool had_ssl_error = web_contents_->controller()->ssl_manager()-> + ProcessedSSLErrorFromRequest(); + + std::vector<PasswordForm>::const_iterator iter; + for (iter = forms.begin(); iter != forms.end(); iter++) { + if (provisional_save_manager_.get() && + provisional_save_manager_->DoesManage(*iter)) { + // The form trying to be saved has immediately re-appeared. Assume + // login failure and abort this save. Fallback to pending login state + // since the user may try again. + pending_login_managers_.push_back(provisional_save_manager_.release()); + // Don't delete the login managers since the user may try again + // and we want to be able to save in that case. + break; + } else { + bool ssl_valid = iter->origin.SchemeIsSecure() && !had_ssl_error; + PasswordFormManager* manager = + new PasswordFormManager(web_contents_->profile(), + this, *iter, ssl_valid); + pending_login_managers_.push_back(manager); + manager->FetchMatchingLoginsFromWebDatabase(); + } + } +} + +void PasswordManager::Autofill(const PasswordForm& form_for_autofill, + const PasswordFormMap& best_matches, + const PasswordForm* const preferred_match) const { + DCHECK(web_contents_); + DCHECK(preferred_match); + switch (form_for_autofill.scheme) { + case PasswordForm::SCHEME_HTML: { + // Note the check above is required because the observer_ for a non-HTML + // schemed password form may have been freed, so we need to distinguish. + bool action_mismatch = form_for_autofill.action.GetWithEmptyPath() != + preferred_match->action.GetWithEmptyPath(); + scoped_ptr<PasswordFormDomManager::FillData> fill_data( + PasswordFormDomManager::CreateFillData(form_for_autofill, + best_matches, preferred_match, + action_mismatch)); + web_contents_->render_view_host()->FillPasswordForm(*fill_data); + return; + } + default: + if (observer_) + observer_->OnAutofillDataAvailable(preferred_match->username_value, + preferred_match->password_value); + } +} + +// PasswordManager, ConfirmInfoBarDelegate implementation: --------------------- + +void PasswordManager::InfoBarClosed() { + pending_decision_manager_.reset(NULL); +} + +std::wstring PasswordManager::GetMessageText() const { + return l10n_util::GetString(IDS_PASSWORD_MANAGER_SAVE_PASSWORD_PROMPT); +} + +SkBitmap* PasswordManager::GetIcon() const { + return ResourceBundle::GetSharedInstance().GetBitmapNamed( + IDR_INFOBAR_SAVE_PASSWORD); +} + +int PasswordManager::GetButtons() const { + return BUTTON_OK | BUTTON_CANCEL; +} + +std::wstring PasswordManager::GetButtonLabel(InfoBarButton button) const { + if (button == BUTTON_OK) + return l10n_util::GetString(IDS_PASSWORD_MANAGER_SAVE_BUTTON); + if (button == BUTTON_CANCEL) + return l10n_util::GetString(IDS_PASSWORD_MANAGER_BLACKLIST_BUTTON); + NOTREACHED(); + return std::wstring(); +} + +bool PasswordManager::Accept() { + pending_decision_manager_->Save(); + return true; +} + +bool PasswordManager::Cancel() { + pending_decision_manager_->PermanentlyBlacklist(); + return true; +} + diff --git a/chrome/browser/password_manager/password_manager.h b/chrome/browser/password_manager/password_manager.h new file mode 100644 index 0000000..74c5554 --- /dev/null +++ b/chrome/browser/password_manager/password_manager.h @@ -0,0 +1,123 @@ +// Copyright (c) 2006-2008 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. + +#ifndef CHROME_BROWSER_PASSWORD_MANAGER_H__ +#define CHROME_BROWSER_PASSWORD_MANAGER_H__ + +#include "base/scoped_ptr.h" +#include "chrome/browser/password_manager/password_form_manager.h" +#include "chrome/browser/tab_contents/infobar_delegate.h" +#include "chrome/browser/views/login_view.h" +#include "chrome/common/pref_member.h" +#include "chrome/common/stl_util-inl.h" +#include "webkit/glue/password_form.h" +#include "webkit/glue/password_form_dom_manager.h" + +class PrefService; +class WebContents; + +// Per-tab password manager. Handles creation and management of UI elements, +// receiving password form data from the renderer and managing the password +// database through the WebDataService. The PasswordManager is a LoginModel +// for purposes of supporting HTTP authentication dialogs. +class PasswordManager : public views::LoginModel, + public ConfirmInfoBarDelegate { + public: + static void RegisterUserPrefs(PrefService* prefs); + + explicit PasswordManager(WebContents* web_contents); + ~PasswordManager(); + + // Called by a PasswordFormManager when it decides a form can be autofilled + // on the page. + void Autofill(const PasswordForm& form_for_autofill, + const PasswordFormMap& best_matches, + const PasswordForm* const preferred_match) const; + + // Notification that the user navigated away from the current page. + // Unless this is a password form submission, for our purposes this + // means we're done with the current page, so we can clean-up. + void DidNavigate(); + + // Show a prompt to save submitted password if it is a new username for + // the form, or else just update the stored value. + void DidStopLoading(); + + // Notifies the password manager that password forms were parsed on the page. + void PasswordFormsSeen(const std::vector<PasswordForm>& forms); + + // When a form is submitted, we prepare to save the password but wait + // until we decide the user has successfully logged in. This is step 1 + // of 2 (see SavePassword). + void ProvisionallySavePassword(PasswordForm form); + + // Clear any pending saves + void ClearProvisionalSave(); + + // LoginModel implementation. + virtual void SetObserver(views::LoginModelObserver* observer) { + observer_ = observer; + } + + private: + // Overridden from ConfirmInfoBarDelegate: + virtual void InfoBarClosed(); + virtual std::wstring GetMessageText() const; + virtual SkBitmap* GetIcon() const; + virtual int GetButtons() const; + virtual std::wstring GetButtonLabel(InfoBarButton button) const; + virtual bool Accept(); + virtual bool Cancel(); + + // Note about how a PasswordFormManager can transition from + // pending_login_managers_ to {provisional_save, pending_decision_}manager_. + // + // 1. form "seen" + // | new + // | ___ pending_decision + // pending_login -- form submit --> provisional_save ___/ + // ^ | \___ (update DB) + // | fail + // |-----------<------<---------| !new + // + // When a form is "seen" on a page, a PasswordFormManager is created + // and stored in this collection until user navigates away from page. + typedef std::vector<PasswordFormManager*> LoginManagers; + LoginManagers pending_login_managers_; + + // Deleter for pending_login_managers_ when PasswordManager is deleted (e.g + // tab closes) on a page with a password form, thus containing login managers. + STLElementDeleter<LoginManagers> login_managers_deleter_; + + // When the user submits a password/credential, this contains the + // PasswordFormManager for the form in question until we deem the login + // attempt to have succeeded (as in valid credentials). If it fails, we + // send the PasswordFormManager back to the pending_login_managers_ set. + // Scoped in case PasswordManager gets deleted (e.g tab closes) between the + // time a user submits a login form and gets to the next page. + scoped_ptr<PasswordFormManager> provisional_save_manager_; + + // After a successful *new* login attempt, we take the PasswordFormManager in + // provisional_save_manager_ and move it here while the user makes up their + // mind with the "save password" infobar. Note if the login is one we already + // know about, the end of the line is provisional_save_manager_ because we + // just update it on success and so such forms never end up in + // pending_decision_manager_. + scoped_ptr<PasswordFormManager> pending_decision_manager_; + + // The containing WebContents + WebContents* web_contents_; + + // The LoginModelObserver (i.e LoginView) requiring autofill. + views::LoginModelObserver* observer_; + + // Set to false to disable the password manager (will no longer fill + // passwords or ask you if you want to save passwords). + BooleanPrefMember password_manager_enabled_; + + DISALLOW_EVIL_CONSTRUCTORS(PasswordManager); +}; + +#endif + |