diff options
author | stuartmorgan@chromium.org <stuartmorgan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-26 20:17:50 +0000 |
---|---|---|
committer | stuartmorgan@chromium.org <stuartmorgan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-05-26 20:17:50 +0000 |
commit | 32d0ef5d82ef2e972635245107825e776072dc82 (patch) | |
tree | 3424f929290716ae4899fea7c961f985579547c2 /chrome/browser/password_manager | |
parent | 61c03cb2e0c5ddaddf8351862e500610c22fdb70 (diff) | |
download | chromium_src-32d0ef5d82ef2e972635245107825e776072dc82.zip chromium_src-32d0ef5d82ef2e972635245107825e776072dc82.tar.gz chromium_src-32d0ef5d82ef2e972635245107825e776072dc82.tar.bz2 |
First phase of Mac Keychain integration. For the overall plan, see the design doc linked from the bug. This implements the ability to read existing Keychain passwords, but no storage/updating yet, and no adding of our own metadata as passwords are used; all that will be done in follow-up patches.
This also includes a Keychain wrapper for the functionality necessary so far, and a mock Keychain sufficient to unit-test essentially all of the code.
This patch deliberately excludes the step of instantiating a PasswordStoreMac and hooking it up to the profile, because a bug in autocomplete itself prevents passwords we load from actually being used, and we don't want to trigger Keychain UI for passwords that will be ignored. So this won't be used in builds yet, but it will be unit tested.
BUG=11745
TEST=none (no user-visible effect yet)
Review URL: http://codereview.chromium.org/115658
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@16897 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/password_manager')
4 files changed, 1144 insertions, 0 deletions
diff --git a/chrome/browser/password_manager/password_store_mac.cc b/chrome/browser/password_manager/password_store_mac.cc new file mode 100644 index 0000000..50a06e6 --- /dev/null +++ b/chrome/browser/password_manager/password_store_mac.cc @@ -0,0 +1,333 @@ +// Copyright (c) 2009 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 "chrome/browser/password_manager/password_store_mac_internal.h" + +#include <CoreServices/CoreServices.h> +#include <string> +#include <vector> + +#include "base/logging.h" +#include "base/string_util.h" +#include "base/time.h" +#include "chrome/browser/keychain_mac.h" + +namespace internal_keychain_helpers { + +// TODO(stuartmorgan): signon_realm for proxies is not yet supported. +bool ExtractSignonRealmComponents(const std::string& signon_realm, + std::string* server, int* port, + bool* is_secure, + std::string* security_domain) { + // The signon_realm will be the Origin portion of a URL for an HTML form, + // and the same but with the security domain as a path for HTTP auth. + GURL realm_as_url(signon_realm); + if (!realm_as_url.is_valid()) { + return false; + } + + if (server) + *server = realm_as_url.host(); + if (is_secure) + *is_secure = realm_as_url.SchemeIsSecure(); + if (port) + *port = realm_as_url.has_port() ? atoi(realm_as_url.port().c_str()) : 0; + if (security_domain) { + // Strip the leading '/' off of the path to get the security domain. + *security_domain = realm_as_url.path().substr(1); + } + return true; +} + +GURL URLFromComponents(bool is_secure, const std::string& host, int port, + const std::string& path) { + GURL::Replacements url_components; + std::string scheme(is_secure ? "https" : "http"); + url_components.SetSchemeStr(scheme); + url_components.SetHostStr(host); + std::string port_string; // Must remain in scope until after we do replacing. + if (port != kAnyPort) { + std::ostringstream port_stringstream; + port_stringstream << port; + port_string = port_stringstream.str(); + url_components.SetPortStr(port_string); + } + url_components.SetPathStr(path); + + GURL url("http://dummy.com"); // ReplaceComponents needs a valid URL. + return url.ReplaceComponents(url_components); +} + +// The time string is of the form "yyyyMMddHHmmss'Z", in UTC time. +bool TimeFromKeychainTimeString(const char* time_string_bytes, + unsigned int byte_length, + base::Time* time) { + DCHECK(time); + + char* time_string = static_cast<char*>(malloc(byte_length + 1)); + memcpy(time_string, time_string_bytes, byte_length); + time_string[byte_length] = '\0'; + base::Time::Exploded exploded_time; + bzero(&exploded_time, sizeof(exploded_time)); + int assignments = sscanf(time_string, "%4d%2d%2d%2d%2d%2dZ", + &exploded_time.year, &exploded_time.month, + &exploded_time.day_of_month, &exploded_time.hour, + &exploded_time.minute, &exploded_time.second); + free(time_string); + + if (assignments == 6) { + *time = base::Time::FromUTCExploded(exploded_time); + return true; + } + return false; +} + +SecAuthenticationType AuthTypeForScheme(PasswordForm::Scheme scheme) { + switch (scheme) { + case PasswordForm::SCHEME_HTML: return kSecAuthenticationTypeHTMLForm; + case PasswordForm::SCHEME_BASIC: return kSecAuthenticationTypeHTTPBasic; + case PasswordForm::SCHEME_DIGEST: return kSecAuthenticationTypeHTTPDigest; + case PasswordForm::SCHEME_OTHER: return kSecAuthenticationTypeDefault; + } + NOTREACHED(); + return kSecAuthenticationTypeDefault; +} + +PasswordForm::Scheme SchemeForAuthType(SecAuthenticationType auth_type) { + switch (auth_type) { + case kSecAuthenticationTypeHTMLForm: return PasswordForm::SCHEME_HTML; + case kSecAuthenticationTypeHTTPBasic: return PasswordForm::SCHEME_BASIC; + case kSecAuthenticationTypeHTTPDigest: return PasswordForm::SCHEME_DIGEST; + default: return PasswordForm::SCHEME_OTHER; + } +} + +// Searches |keychain| for all items usable for the given signon_realm, and +// puts them in |items|. The caller is responsible for calling keychain->Free +// on each of them when it is finished with them. +void FindMatchingKeychainItems(const MacKeychain& keychain, + const std::string& signon_realm, + PasswordForm::Scheme scheme, + std::vector<SecKeychainItemRef>* items) { + // Construct a keychain search based on the signon_realm and scheme. + std::string server; + std::string security_domain; + int port; + bool is_secure; + if (!internal_keychain_helpers::ExtractSignonRealmComponents( + signon_realm, &server, &port, &is_secure, &security_domain)) { + // TODO(stuartmorgan): Proxies will currently fail here, since their + // signon_realm is not a URL. We need to detect the proxy case and handle + // it specially. + return; + } + + const char* server_c_str = server.c_str(); + UInt32 port_uint = port; + SecProtocolType protocol = is_secure ? kSecProtocolTypeHTTPS + : kSecProtocolTypeHTTP; + SecAuthenticationType auth_type = + internal_keychain_helpers::AuthTypeForScheme(scheme); + const char* security_domain_c_str = security_domain.c_str(); + + // kSecSecurityDomainItemAttr must be last, so that it can be "removed" when + // not applicable. + SecKeychainAttribute attributes[] = { + { kSecServerItemAttr, strlen(server_c_str), + const_cast<void*>(reinterpret_cast<const void*>(server_c_str)) }, + { kSecPortItemAttr, sizeof(port_uint), static_cast<void*>(&port_uint) }, + { kSecProtocolItemAttr, sizeof(protocol), static_cast<void*>(&protocol) }, + { kSecAuthenticationTypeItemAttr, sizeof(auth_type), + static_cast<void*>(&auth_type) }, + { kSecSecurityDomainItemAttr, strlen(security_domain_c_str), + const_cast<void*>(reinterpret_cast<const void*>(security_domain_c_str)) } + }; + SecKeychainAttributeList search_attributes = { arraysize(attributes), + attributes }; + // For HTML forms, we don't want the security domain to be part of the + // search, so trim that off of the attribute list. + if (scheme == PasswordForm::SCHEME_HTML) { + search_attributes.count -= 1; + } + + SecKeychainSearchRef keychain_search = NULL; + OSStatus result = keychain.SearchCreateFromAttributes( + NULL, kSecInternetPasswordItemClass, &search_attributes, + &keychain_search); + + if (result != noErr) { + LOG(ERROR) << "Keychain lookup failed for " << server << " with error " + << result; + return; + } + + SecKeychainItemRef keychain_item; + while (keychain.SearchCopyNext(keychain_search, &keychain_item) == noErr) { + // Consumer is responsible for deleting the forms when they are done. + items->push_back(keychain_item); + } + + keychain.Free(keychain_search); +} + +bool FillPasswordFormFromKeychainItem(const MacKeychain& keychain, + const SecKeychainItemRef& keychain_item, + PasswordForm* form) { + DCHECK(form); + + SecKeychainAttributeInfo attrInfo; + UInt32 tags[] = { kSecAccountItemAttr, + kSecServerItemAttr, + kSecPortItemAttr, + kSecPathItemAttr, + kSecProtocolItemAttr, + kSecAuthenticationTypeItemAttr, + kSecSecurityDomainItemAttr, + kSecCreationDateItemAttr, + kSecNegativeItemAttr }; + attrInfo.count = arraysize(tags); + attrInfo.tag = tags; + attrInfo.format = NULL; + + SecKeychainAttributeList *attrList; + UInt32 password_length; + void* password_data; + OSStatus result = keychain.ItemCopyAttributesAndData(keychain_item, &attrInfo, + NULL, &attrList, + &password_length, + &password_data); + + if (result != noErr) { + // We don't log errSecAuthFailed because that just means that the user + // chose not to allow us access to the item. + if (result != errSecAuthFailed) { + LOG(ERROR) << "Keychain data load failed: " << result; + } + return false; + } + + UTF8ToWide(static_cast<const char *>(password_data), password_length, + &(form->password_value)); + + int port = kAnyPort; + std::string server; + std::string security_domain; + std::string path; + for (unsigned int i = 0; i < attrList->count; i++) { + SecKeychainAttribute attr = attrList->attr[i]; + if (!attr.data) { + continue; + } + switch (attr.tag) { + case kSecAccountItemAttr: + UTF8ToWide(static_cast<const char *>(attr.data), attr.length, + &(form->username_value)); + break; + case kSecServerItemAttr: + server.assign(static_cast<const char *>(attr.data), attr.length); + break; + case kSecPortItemAttr: + port = *(static_cast<UInt32*>(attr.data)); + break; + case kSecPathItemAttr: + path.assign(static_cast<const char *>(attr.data), attr.length); + break; + case kSecProtocolItemAttr: + { + SecProtocolType protocol = *(static_cast<SecProtocolType*>(attr.data)); + // TODO(stuartmorgan): Handle proxy types + form->ssl_valid = (protocol == kSecProtocolTypeHTTPS); + break; + } + case kSecAuthenticationTypeItemAttr: + { + SecAuthenticationType auth_type = + *(static_cast<SecAuthenticationType*>(attr.data)); + form->scheme = internal_keychain_helpers::SchemeForAuthType(auth_type); + break; + } + case kSecSecurityDomainItemAttr: + security_domain.assign(static_cast<const char *>(attr.data), + attr.length); + break; + case kSecCreationDateItemAttr: + // The only way to get a date out of Keychain is as a string. Really. + // (The docs claim it's an int, but the header is correct.) + internal_keychain_helpers::TimeFromKeychainTimeString( + static_cast<char*>(attr.data), attr.length, &form->date_created); + break; + case kSecNegativeItemAttr: + Boolean negative_item = *(static_cast<Boolean*>(attr.data)); + if (negative_item) { + form->blacklisted_by_user = true; + } + break; + } + } + keychain.ItemFreeAttributesAndData(attrList, password_data); + + // kSecNegativeItemAttr doesn't seem to actually be in widespread use. In + // practice, other browsers seem to use a "" or " " password (and a special + // user name) to indicated blacklist entries. + if (form->password_value.empty() || form->password_value == L" ") { + form->blacklisted_by_user = true; + } + + form->origin = internal_keychain_helpers::URLFromComponents(form->ssl_valid, + server, port, + path); + // TODO(stuartmorgan): Handle proxies, which need a different signon_realm + // format. + form->signon_realm = form->origin.GetOrigin().spec(); + if (form->scheme != PasswordForm::SCHEME_HTML) { + form->signon_realm.append(security_domain); + } + return true; +} + +} // internal_keychain_helpers + + +PasswordStoreMac::PasswordStoreMac(MacKeychain* keychain) + : keychain_(keychain) { + DCHECK(keychain_.get()); +} + +void PasswordStoreMac::AddLoginImpl(const PasswordForm& form) { + NOTIMPLEMENTED(); +} + +void PasswordStoreMac::UpdateLoginImpl(const PasswordForm& form) { + NOTIMPLEMENTED(); +} + +void PasswordStoreMac::RemoveLoginImpl(const PasswordForm& form) { + NOTIMPLEMENTED(); +} + +void PasswordStoreMac::GetLoginsImpl(GetLoginsRequest* request) { + std::vector<SecKeychainItemRef> keychain_items; + + internal_keychain_helpers::FindMatchingKeychainItems( + *keychain_, request->form.signon_realm, request->form.scheme, + &keychain_items); + + std::vector<PasswordForm*> forms; + + for (std::vector<SecKeychainItemRef>::iterator i = keychain_items.begin(); + i != keychain_items.end(); ++i) { + // Consumer is responsible for deleting the forms when they are done... + PasswordForm* form = new PasswordForm(); + SecKeychainItemRef keychain_item = *i; + if (internal_keychain_helpers::FillPasswordFormFromKeychainItem( + *keychain_, keychain_item, form)) { + forms.push_back(form); + } + // ... but we need to clean up the keychain item. + keychain_->Free(keychain_item); + } + + NotifyConsumer(request, forms); +} diff --git a/chrome/browser/password_manager/password_store_mac.h b/chrome/browser/password_manager/password_store_mac.h new file mode 100644 index 0000000..61bce2d --- /dev/null +++ b/chrome/browser/password_manager/password_store_mac.h @@ -0,0 +1,30 @@ +// Copyright (c) 2009 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_PASSWORD_STORE_MAC_H_ +#define CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_H_ + +#include "base/scoped_ptr.h" +#include "chrome/browser/password_manager/password_store.h" + +class MacKeychain; + +class PasswordStoreMac : public PasswordStore { + public: + // Takes ownership of |keychain|, which must not be NULL. + explicit PasswordStoreMac(MacKeychain* keychain); + virtual ~PasswordStoreMac() {} + + private: + void AddLoginImpl(const PasswordForm& form); + void UpdateLoginImpl(const PasswordForm& form); + void RemoveLoginImpl(const PasswordForm& form); + void GetLoginsImpl(GetLoginsRequest* request); + + scoped_ptr<MacKeychain> keychain_; + + DISALLOW_COPY_AND_ASSIGN(PasswordStoreMac); +}; + +#endif // CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_H_ diff --git a/chrome/browser/password_manager/password_store_mac_internal.h b/chrome/browser/password_manager/password_store_mac_internal.h new file mode 100644 index 0000000..250a15fc --- /dev/null +++ b/chrome/browser/password_manager/password_store_mac_internal.h @@ -0,0 +1,71 @@ +// Copyright (c) 2009 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_PASSWORD_STORE_MAC_INTERNAL_H_ +#define CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_INTERNAL_H_ + +#include <Security/Security.h> + +#include <string> +#include <vector> + +#include "base/time.h" +#include "chrome/browser/keychain_mac.h" + +namespace internal_keychain_helpers { + +// Takes a PasswordForm's signon_realm and parses it into its component parts, +// which are returned though the appropriate out parameters. +// Returns true if it can be successfully parsed, in which case all out params +// that are non-NULL will be set. If there is no port, port will be 0. +// If the return value is false, the state of the our params is undefined. +bool ExtractSignonRealmComponents(const std::string& signon_realm, + std::string* server, int* port, + bool* is_secure, + std::string* security_domain); + +// Returns a URL built from the given components. To create a URL without a +// port, pass kAnyPort for the |port| parameter. +GURL URLFromComponents(bool is_secure, const std::string& host, int port, + const std::string& path); + +// Converts a Keychain time string to a Time object, returning true if +// time_string_bytes was parsable. If the return value is false, the value of +// |time| is unchanged. +bool TimeFromKeychainTimeString(const char* time_string_bytes, + unsigned int byte_length, + base::Time* time); + +// Returns the Keychain SecAuthenticationType type corresponding to |scheme|. +SecAuthenticationType AuthTypeForScheme(PasswordForm::Scheme scheme); + +// Returns the PasswordForm Scheme corresponding to |auth_type|. +PasswordForm::Scheme SchemeForAuthType(SecAuthenticationType auth_type); + +// Searches |keychain| for all items usable for the given signon_realm, and +// puts them in |items|. The caller is responsible for calling keychain->Free +// on each of them when it is finished with them. +void FindMatchingKeychainItems(const MacKeychain& keychain, + const std::string& signon_realm, + PasswordForm::Scheme scheme, + std::vector<SecKeychainItemRef>* items); + +// Sets the fields of |form| based on the keychain data from |keychain_item|. +// Fields that can't be determined from |keychain_item| will be unchanged. +// +// IMPORTANT: This function can cause the OS to trigger UI (to allow access to +// the keychain item if we aren't trusted for the item), and block until the UI +// is dismissed. +// +// If excessive prompting for access to other applications' keychain items +// becomes an issue, the password storage API will need to be refactored to +// allow the password to be retrieved later (accessing other fields doesn't +// require authorization). +bool FillPasswordFormFromKeychainItem(const MacKeychain& keychain, + const SecKeychainItemRef& keychain_item, + PasswordForm* form); + +} // internal_keychain_helpers + +#endif // CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_INTERNAL_H_ diff --git a/chrome/browser/password_manager/password_store_mac_unittest.cc b/chrome/browser/password_manager/password_store_mac_unittest.cc new file mode 100644 index 0000000..d268ae7f3 --- /dev/null +++ b/chrome/browser/password_manager/password_store_mac_unittest.cc @@ -0,0 +1,710 @@ +// Copyright (c) 2009 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 "base/basictypes.h" +#include "chrome/browser/password_manager/password_store_mac.h" +#include "chrome/browser/password_manager/password_store_mac_internal.h" + +#pragma mark Mock Keychain + +// TODO(stuartmorgan): Replace this with gMock. You know, once we have it. +// The basic idea of this mock is that it has a static array of data to use +// for ItemCopyAttributesAndData, and SecKeychainItemRef values are just indexes +// into that array (offset by 1 to prevent problems with client null-checking +// refs), cast to pointers. +class MockKeychain : public MacKeychain { + public: + MockKeychain(); + virtual ~MockKeychain(); + virtual OSStatus ItemCopyAttributesAndData( + SecKeychainItemRef itemRef, SecKeychainAttributeInfo *info, + SecItemClass *itemClass, SecKeychainAttributeList **attrList, + UInt32 *length, void **outData) const; + virtual OSStatus ItemFreeAttributesAndData(SecKeychainAttributeList *attrList, + void *data) const; + virtual OSStatus SearchCreateFromAttributes( + CFTypeRef keychainOrArray, SecItemClass itemClass, + const SecKeychainAttributeList *attrList, + SecKeychainSearchRef *searchRef) const; + virtual OSStatus SearchCopyNext(SecKeychainSearchRef searchRef, + SecKeychainItemRef *itemRef) const; + virtual void Free(CFTypeRef ref) const; + + // Causes a test failure unless everything returned from + // ItemCopyAttributesAndData, SearchCreateFromAttributes, and SearchCopyNext + // was correctly freed. + void ExpectCreatesAndFreesBalanced(); + + private: + // Sets the data and length of |tag| in the item-th test item based on + // |value|. The null-terminator will not be included; the Keychain Services + // docs don't indicate whether it is or not, so clients should not assume + // that it will be. + void SetTestDataString(int item, UInt32 tag, const char* value); + // Sets the data of the corresponding attribute of the item-th test item to + // |value|. Assumes that the space has alread been allocated, and the length + // set. + void SetTestDataPort(int item, UInt32 value); + void SetTestDataProtocol(int item, SecProtocolType value); + void SetTestDataAuthType(int item, SecAuthenticationType value); + void SetTestDataNegativeItem(int item, Boolean value); + // Sets the password for the item-th test item. As with SetTestDataString, + // the data will not be null-terminated. + void SetTestDataPassword(int item, const char* value); + + // Returns the index of |tag| in |attribute_list|, or -1 if it's not found. + static int IndexForTag(const SecKeychainAttributeList& attribute_list, + UInt32 tag); + + static const int kDummySearchRef = 1000; + + typedef struct { + void* data; + UInt32 length; + } KeychainPasswordData; + + SecKeychainAttributeList* keychain_attr_list_; + KeychainPasswordData* keychain_data_; + unsigned int item_count_; + + // Tracks the items that should be returned in subsequent calls to + // SearchCopyNext, based on the last call to SearchCreateFromAttributes. + // We can't handle multiple active searches, since we don't track the search + // ref we return, but we don't need to for our mocking. + mutable std::vector<unsigned int> remaining_search_results_; + + // Track copies and releases to make sure they balance. Really these should + // be maps to track per item, but this should be good enough to catch + // real mistakes. + mutable int search_copy_count_; + mutable int keychain_item_copy_count_; + mutable int attribute_data_copy_count_; +}; + +MockKeychain::MockKeychain() + : search_copy_count_(0), keychain_item_copy_count_(0), + attribute_data_copy_count_(0) { + UInt32 tags[] = { kSecAccountItemAttr, + kSecServerItemAttr, + kSecPortItemAttr, + kSecPathItemAttr, + kSecProtocolItemAttr, + kSecAuthenticationTypeItemAttr, + kSecSecurityDomainItemAttr, + kSecCreationDateItemAttr, + kSecNegativeItemAttr }; + + // Create the test keychain data to return from ItemCopyAttributesAndData, + // and set up everything that's consistent across all the items. + item_count_ = 8; + keychain_attr_list_ = static_cast<SecKeychainAttributeList*>( + calloc(item_count_, sizeof(SecKeychainAttributeList))); + keychain_data_ = static_cast<KeychainPasswordData*>( + calloc(item_count_, sizeof(KeychainPasswordData))); + for (unsigned int i = 0; i < item_count_; ++i) { + keychain_attr_list_[i].count = arraysize(tags); + keychain_attr_list_[i].attr = static_cast<SecKeychainAttribute*>( + calloc(keychain_attr_list_[i].count, sizeof(SecKeychainAttribute))); + for (unsigned int j = 0; j < keychain_attr_list_[i].count; ++j) { + keychain_attr_list_[i].attr[j].tag = tags[j]; + size_t data_size = 0; + switch (tags[j]) { + case kSecPortItemAttr: + data_size = sizeof(UInt32); + break; + case kSecProtocolItemAttr: + data_size = sizeof(SecProtocolType); + break; + case kSecAuthenticationTypeItemAttr: + data_size = sizeof(SecAuthenticationType); + break; + case kSecNegativeItemAttr: + data_size = sizeof(Boolean); + break; + } + if (data_size > 0) { + keychain_attr_list_[i].attr[j].length = data_size; + keychain_attr_list_[i].attr[j].data = calloc(1, data_size); + } + } + } + + // Basic HTML form. + unsigned int item = 0; + CHECK(item < item_count_); + SetTestDataString(item, kSecAccountItemAttr, "joe_user"); + SetTestDataString(item, kSecServerItemAttr, "some.domain.com"); + SetTestDataProtocol(item, kSecProtocolTypeHTTP); + SetTestDataAuthType(item, kSecAuthenticationTypeHTMLForm); + SetTestDataString(item, kSecCreationDateItemAttr, "20020601171500Z"); + SetTestDataPassword(item, "sekrit"); + + // HTML form with path. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecAccountItemAttr, "joe_user"); + SetTestDataString(item, kSecServerItemAttr, "some.domain.com"); + SetTestDataString(item, kSecPathItemAttr, "insecure.html"); + SetTestDataProtocol(item, kSecProtocolTypeHTTP); + SetTestDataAuthType(item, kSecAuthenticationTypeHTMLForm); + SetTestDataString(item, kSecCreationDateItemAttr, "19991231235959Z"); + SetTestDataPassword(item, "sekrit"); + + // Secure HTML form with path. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecAccountItemAttr, "secure_user"); + SetTestDataString(item, kSecServerItemAttr, "some.domain.com"); + SetTestDataString(item, kSecPathItemAttr, "secure.html"); + SetTestDataProtocol(item, kSecProtocolTypeHTTPS); + SetTestDataAuthType(item, kSecAuthenticationTypeHTMLForm); + SetTestDataString(item, kSecCreationDateItemAttr, "20100908070605Z"); + SetTestDataPassword(item, "password"); + + // True negative item. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecServerItemAttr, "dont.remember.com"); + SetTestDataProtocol(item, kSecProtocolTypeHTTP); + SetTestDataAuthType(item, kSecAuthenticationTypeHTMLForm); + SetTestDataString(item, kSecCreationDateItemAttr, "20000101000000Z"); + SetTestDataNegativeItem(item, true); + + // De-facto negative item, type one. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecAccountItemAttr, "Password Not Stored"); + SetTestDataString(item, kSecServerItemAttr, "dont.remember.com"); + SetTestDataProtocol(item, kSecProtocolTypeHTTP); + SetTestDataAuthType(item, kSecAuthenticationTypeHTMLForm); + SetTestDataString(item, kSecCreationDateItemAttr, "20000101000000Z"); + SetTestDataPassword(item, ""); + + // De-facto negative item, type two. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecServerItemAttr, "dont.remember.com"); + SetTestDataProtocol(item, kSecProtocolTypeHTTPS); + SetTestDataAuthType(item, kSecAuthenticationTypeHTMLForm); + SetTestDataString(item, kSecCreationDateItemAttr, "20000101000000Z"); + SetTestDataPassword(item, " "); + + // HTTP auth basic, with port and path. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecAccountItemAttr, "basic_auth_user"); + SetTestDataString(item, kSecServerItemAttr, "some.domain.com"); + SetTestDataString(item, kSecSecurityDomainItemAttr, "low_security"); + SetTestDataString(item, kSecPathItemAttr, "insecure.html"); + SetTestDataProtocol(item, kSecProtocolTypeHTTP); + SetTestDataPort(item, 4567); + SetTestDataAuthType(item, kSecAuthenticationTypeHTTPBasic); + SetTestDataString(item, kSecCreationDateItemAttr, "19980330100000Z"); + SetTestDataPassword(item, "basic"); + + // HTTP auth digest, secure. + ++item; + CHECK(item < item_count_); + SetTestDataString(item, kSecAccountItemAttr, "digest_auth_user"); + SetTestDataString(item, kSecServerItemAttr, "some.domain.com"); + SetTestDataString(item, kSecSecurityDomainItemAttr, "high_security"); + SetTestDataProtocol(item, kSecProtocolTypeHTTPS); + SetTestDataAuthType(item, kSecAuthenticationTypeHTTPDigest); + SetTestDataString(item, kSecCreationDateItemAttr, "19980330100000Z"); + SetTestDataPassword(item, "digest"); +} + +MockKeychain::~MockKeychain() { + for (unsigned int i = 0; i < item_count_; ++i) { + for (unsigned int j = 0; j < keychain_attr_list_[i].count; ++j) { + if (keychain_attr_list_[i].attr[j].data) { + free(keychain_attr_list_[i].attr[j].data); + } + } + free(keychain_attr_list_[i].attr); + if (keychain_data_[i].data) { + free(keychain_data_[i].data); + } + } + free(keychain_attr_list_); + free(keychain_data_); +} + +void MockKeychain::ExpectCreatesAndFreesBalanced() { + EXPECT_EQ(0, search_copy_count_); + EXPECT_EQ(0, keychain_item_copy_count_); + EXPECT_EQ(0, attribute_data_copy_count_); +} + +int MockKeychain::IndexForTag(const SecKeychainAttributeList& attribute_list, + UInt32 tag) { + for (unsigned int i = 0; i < attribute_list.count; ++i) { + if (attribute_list.attr[i].tag == tag) { + return i; + } + } + DCHECK(false); + return -1; +} + +void MockKeychain::SetTestDataString(int item, UInt32 tag, const char* value) { + int attribute_index = IndexForTag(keychain_attr_list_[item], tag); + size_t data_size = strlen(value); + keychain_attr_list_[item].attr[attribute_index].length = data_size; + if (data_size > 0) { + keychain_attr_list_[item].attr[attribute_index].data = malloc(data_size); + // Use memcpy rather than str*cpy because we are deliberately omitting the + // null-terminator (see method declaration comment). + CHECK(keychain_attr_list_[item].attr[attribute_index].data); + memcpy(keychain_attr_list_[item].attr[attribute_index].data, value, + data_size); + } else { + keychain_attr_list_[item].attr[attribute_index].data = NULL; + } +} + +void MockKeychain::SetTestDataPort(int item, UInt32 value) { + int attribute_index = IndexForTag(keychain_attr_list_[item], + kSecPortItemAttr); + void* data = keychain_attr_list_[item].attr[attribute_index].data; + *(static_cast<UInt32*>(data)) = value; +} + +void MockKeychain::SetTestDataProtocol(int item, SecProtocolType value) { + int attribute_index = IndexForTag(keychain_attr_list_[item], + kSecProtocolItemAttr); + void* data = keychain_attr_list_[item].attr[attribute_index].data; + *(static_cast<SecProtocolType*>(data)) = value; +} + +void MockKeychain::SetTestDataAuthType(int item, SecAuthenticationType value) { + int attribute_index = IndexForTag(keychain_attr_list_[item], + kSecAuthenticationTypeItemAttr); + void* data = keychain_attr_list_[item].attr[attribute_index].data; + *(static_cast<SecAuthenticationType*>(data)) = value; +} + +void MockKeychain::SetTestDataNegativeItem(int item, Boolean value) { + int attribute_index = IndexForTag(keychain_attr_list_[item], + kSecNegativeItemAttr); + void* data = keychain_attr_list_[item].attr[attribute_index].data; + *(static_cast<Boolean*>(data)) = value; +} + +void MockKeychain::SetTestDataPassword(int item, const char* value) { + size_t data_size = strlen(value); + keychain_data_[item].length = data_size; + if (data_size > 0) { + keychain_data_[item].data = malloc(data_size); + // Use memcpy rather than str*cpy because we are deliberately omitting the + // null-terminator (see method declaration comment). + memcpy(keychain_data_[item].data, value, data_size); + } else { + keychain_data_[item].data = NULL; + } +} + +OSStatus MockKeychain::ItemCopyAttributesAndData( + SecKeychainItemRef itemRef, SecKeychainAttributeInfo *info, + SecItemClass *itemClass, SecKeychainAttributeList **attrList, + UInt32 *length, void **outData) const { + DCHECK(itemRef); + unsigned int item_index = reinterpret_cast<unsigned int>(itemRef) - 1; + if (item_index >= item_count_) { + return errSecInvalidItemRef; + } + + DCHECK(!itemClass); // itemClass not implemented in the Mock. + if (attrList) { + *attrList = &(keychain_attr_list_[item_index]); + } + if (outData) { + *outData = keychain_data_[item_index].data; + DCHECK(length); + *length = keychain_data_[item_index].length; + } + + ++attribute_data_copy_count_; + return noErr; +} + +OSStatus MockKeychain::ItemFreeAttributesAndData( + SecKeychainAttributeList *attrList, + void *data) const { + --attribute_data_copy_count_; + return noErr; +} + +OSStatus MockKeychain::SearchCreateFromAttributes( + CFTypeRef keychainOrArray, SecItemClass itemClass, + const SecKeychainAttributeList *attrList, + SecKeychainSearchRef *searchRef) const { + // Figure out which of our mock items matches, and set up the array we'll use + // to generate results out of SearchCopyNext. + remaining_search_results_.clear(); + for (unsigned int mock_item = 0; mock_item < item_count_; ++mock_item) { + bool mock_item_matches = true; + for (UInt32 search_attr = 0; search_attr < attrList->count; ++search_attr) { + int mock_attr = IndexForTag(keychain_attr_list_[mock_item], + attrList->attr[search_attr].tag); + SecKeychainAttribute* mock_attribute = + &(keychain_attr_list_[mock_item].attr[mock_attr]); + if (mock_attribute->length != attrList->attr[search_attr].length || + memcmp(mock_attribute->data, attrList->attr[search_attr].data, + attrList->attr[search_attr].length) != 0) { + mock_item_matches = false; + break; + } + } + if (mock_item_matches) { + remaining_search_results_.push_back(mock_item); + } + } + + DCHECK(searchRef); + *searchRef = reinterpret_cast<SecKeychainSearchRef>(kDummySearchRef); + ++search_copy_count_; + return noErr; +} + +OSStatus MockKeychain::SearchCopyNext(SecKeychainSearchRef searchRef, + SecKeychainItemRef *itemRef) const { + if (remaining_search_results_.empty()) { + return errSecItemNotFound; + } + unsigned int index = remaining_search_results_.front(); + remaining_search_results_.erase(remaining_search_results_.begin()); + *itemRef = reinterpret_cast<SecKeychainItemRef>(index + 1); + ++keychain_item_copy_count_; + return noErr; +} + +void MockKeychain::Free(CFTypeRef ref) const { + if (!ref) { + return; + } + + if (reinterpret_cast<int>(ref) == kDummySearchRef) { + --search_copy_count_; + } else { + --keychain_item_copy_count_; + } +} + +#pragma mark Unit Tests +//////////////////////////////////////////////////////////////////////////////// + +TEST(PasswordStoreMacTest, TestSignonRealmParsing) { + typedef struct { + const char* signon_realm; + const bool expected_parsed; + const char* expected_server; + const bool expected_is_secure; + const int expected_port; + const char* expected_security_domain; + } TestData; + + TestData test_data[] = { + // HTML form signon realms. + { "http://www.domain.com/", + true, "www.domain.com", false, 0, "" }, + { "https://foo.org:9999/", + true, "foo.org", true, 9999, "" }, + // HTTP auth signon realms. + { "http://httpauth.com:8080/lowsecurity", + true, "httpauth.com", false, 8080, "lowsecurity" }, + { "https://httpauth.com/highsecurity", + true, "httpauth.com", true, 0 , "highsecurity" }, + // Bogus realms + { "blahblahblah", + false, false, "", 0, "" }, + { "foo/bar/baz", + false, false, "", 0, "" }, + }; + + for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { + std::string server; + std::string security_domain; + bool is_secure = false; + int port = -1; + bool parsed = internal_keychain_helpers::ExtractSignonRealmComponents( + std::string(test_data[i].signon_realm), &server, &port, &is_secure, + &security_domain); + EXPECT_EQ(test_data[i].expected_parsed, parsed) << "In iteration " << i; + + if (!parsed) { + continue; // If parse failed, out params are undefined. + } + EXPECT_EQ(std::string(test_data[i].expected_server), server) + << "In iteration " << i; + EXPECT_EQ(std::string(test_data[i].expected_security_domain), + security_domain) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_is_secure, is_secure) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_port, port) << "In iteration " << i; + } + + // NULLs are allowed for out params. + bool parsed = internal_keychain_helpers::ExtractSignonRealmComponents( + std::string("http://foo.bar.com:1234/baz"), NULL, NULL, NULL, NULL); + EXPECT_TRUE(parsed); +} + +TEST(PasswordStoreMacTest, TestURLConstruction) { + std::string host("exampledomain.com"); + std::string path("/path/to/page.html"); + + GURL full_url = internal_keychain_helpers::URLFromComponents(false, host, + 1234, path); + EXPECT_TRUE(full_url.is_valid()); + EXPECT_EQ(GURL("http://exampledomain.com:1234/path/to/page.html"), full_url); + + GURL simple_secure_url = internal_keychain_helpers::URLFromComponents( + true, host, 0, std::string("")); + EXPECT_TRUE(simple_secure_url.is_valid()); + EXPECT_EQ(GURL("https://exampledomain.com/"), simple_secure_url); +} + +TEST(PasswordStoreMacTest, TestKeychainTime) { + typedef struct { + const char* time_string; + const bool expected_parsed; + const int expected_year; + const int expected_month; + const int expected_day; + const int expected_hour; + const int expected_minute; + const int expected_second; + } TestData; + + TestData test_data[] = { + // HTML form signon realms. + { "19980330100000Z", true, 1998, 3, 30, 10, 0, 0 }, + { "19991231235959Z", true, 1999, 12, 31, 23, 59, 59 }, + { "20000101000000Z", true, 2000, 1, 1, 0, 0, 0 }, + { "20011112012843Z", true, 2001, 11, 12, 1, 28, 43 }, + { "20020601171530Z", true, 2002, 6, 1, 17, 15, 30 }, + { "20100908070605Z", true, 2010, 9, 8, 7, 6, 5 }, + { "20010203040", false, 0, 0, 0, 0, 0, 0 }, + }; + + for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { + base::Time time; + bool parsed = internal_keychain_helpers::TimeFromKeychainTimeString( + test_data[i].time_string, strlen(test_data[i].time_string), &time); + EXPECT_EQ(test_data[i].expected_parsed, parsed) << "In iteration " << i; + if (!parsed) { + continue; + } + + base::Time::Exploded exploded_time; + time.UTCExplode(&exploded_time); + EXPECT_EQ(test_data[i].expected_year, exploded_time.year) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_month, exploded_time.month) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_day, exploded_time.day_of_month) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_hour, exploded_time.hour) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_minute, exploded_time.minute) + << "In iteration " << i; + EXPECT_EQ(test_data[i].expected_second, exploded_time.second) + << "In iteration " << i; + } +} + +TEST(PasswordStoreMacTest, TestAuthTypeSchemeTranslation) { + // Our defined types should round-trip correctly. + SecAuthenticationType auth_types[] = { kSecAuthenticationTypeHTMLForm, + kSecAuthenticationTypeHTTPBasic, + kSecAuthenticationTypeHTTPDigest }; + const int auth_count = sizeof(auth_types) / sizeof(SecAuthenticationType); + for (int i = 0; i < auth_count; ++i) { + SecAuthenticationType round_tripped_auth_type = + internal_keychain_helpers::AuthTypeForScheme( + internal_keychain_helpers::SchemeForAuthType(auth_types[i])); + EXPECT_EQ(auth_types[i], round_tripped_auth_type); + } + // Anything else should become SCHEME_OTHER and come back as Default. + PasswordForm::Scheme scheme_for_other_auth_type = + internal_keychain_helpers::SchemeForAuthType(kSecAuthenticationTypeNTLM); + SecAuthenticationType round_tripped_other_auth_type = + internal_keychain_helpers::AuthTypeForScheme(scheme_for_other_auth_type); + EXPECT_EQ(PasswordForm::SCHEME_OTHER, scheme_for_other_auth_type); + EXPECT_EQ(kSecAuthenticationTypeDefault, round_tripped_other_auth_type); +} + +TEST(PasswordStoreMacTest, 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 }, + }; + + MockKeychain mock_keychain; + + for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(expected); ++i) { + // Create our fake KeychainItemRef; see MockKeychain docs. + SecKeychainItemRef keychain_item = + reinterpret_cast<SecKeychainItemRef>(i + 1); + PasswordForm form; + bool parsed = internal_keychain_helpers::FillPasswordFormFromKeychainItem( + mock_keychain, keychain_item, &form); + + EXPECT_TRUE(parsed) << "In iteration " << i; + mock_keychain.ExpectCreatesAndFreesBalanced(); + + 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(std::wstring(expected[i].username), form.username_value) + << "In iteration " << i; + EXPECT_EQ(std::wstring(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( + mock_keychain, keychain_item, &form); + mock_keychain.ExpectCreatesAndFreesBalanced(); + EXPECT_FALSE(parsed); + } +} + +static void FreeKeychainItems(const MacKeychain& keychain, + std::vector<SecKeychainItemRef>* items) { + for (std::vector<SecKeychainItemRef>::iterator i = items->begin(); + i != items->end(); ++i) { + keychain.Free(*i); + } + items->clear(); +} + +TEST(PasswordStoreMacTest, TestKeychainSearch) { + MockKeychain mock_keychain; + + { // An HTML form we've seen. + std::vector<SecKeychainItemRef> matching_items; + internal_keychain_helpers::FindMatchingKeychainItems( + mock_keychain, std::string("http://some.domain.com/"), + PasswordForm::SCHEME_HTML, &matching_items); + EXPECT_EQ(static_cast<size_t>(2), matching_items.size()); + FreeKeychainItems(mock_keychain, &matching_items); + mock_keychain.ExpectCreatesAndFreesBalanced(); + } + + { // An HTML form we haven't seen + std::vector<SecKeychainItemRef> matching_items; + internal_keychain_helpers::FindMatchingKeychainItems( + mock_keychain, std::string("http://www.unseendomain.com/"), + PasswordForm::SCHEME_HTML, &matching_items); + EXPECT_EQ(static_cast<size_t>(0), matching_items.size()); + FreeKeychainItems(mock_keychain, &matching_items); + mock_keychain.ExpectCreatesAndFreesBalanced(); + } + + { // Basic auth that should match. + std::vector<SecKeychainItemRef> matching_items; + internal_keychain_helpers::FindMatchingKeychainItems( + mock_keychain, std::string("http://some.domain.com:4567/low_security"), + PasswordForm::SCHEME_BASIC, &matching_items); + EXPECT_EQ(static_cast<size_t>(1), matching_items.size()); + FreeKeychainItems(mock_keychain, &matching_items); + mock_keychain.ExpectCreatesAndFreesBalanced(); + } + + { // Basic auth with the wrong port. + std::vector<SecKeychainItemRef> matching_items; + internal_keychain_helpers::FindMatchingKeychainItems( + mock_keychain, std::string("http://some.domain.com:1111/low_security"), + PasswordForm::SCHEME_BASIC, &matching_items); + EXPECT_EQ(static_cast<size_t>(0), matching_items.size()); + FreeKeychainItems(mock_keychain, &matching_items); + mock_keychain.ExpectCreatesAndFreesBalanced(); + } + + { // Digest auth we've saved under https, visited with http. + std::vector<SecKeychainItemRef> matching_items; + internal_keychain_helpers::FindMatchingKeychainItems( + mock_keychain, std::string("http://some.domain.com/high_security"), + PasswordForm::SCHEME_DIGEST, &matching_items); + EXPECT_EQ(static_cast<size_t>(0), matching_items.size()); + FreeKeychainItems(mock_keychain, &matching_items); + mock_keychain.ExpectCreatesAndFreesBalanced(); + } + + { // Digest auth that should match. + std::vector<SecKeychainItemRef> matching_items; + internal_keychain_helpers::FindMatchingKeychainItems( + mock_keychain, std::string("https://some.domain.com/high_security"), + PasswordForm::SCHEME_DIGEST, &matching_items); + EXPECT_EQ(static_cast<size_t>(1), matching_items.size()); + FreeKeychainItems(mock_keychain, &matching_items); + mock_keychain.ExpectCreatesAndFreesBalanced(); + } +} |