| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/password_manager/native_backend_kwallet_x.h" |
| |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/logging.h" |
| #include "base/pickle.h" |
| #include "base/stl_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/synchronization/waitable_event.h" |
| #include "base/threading/thread_restrictions.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "dbus/bus.h" |
| #include "dbus/message.h" |
| #include "dbus/object_path.h" |
| #include "dbus/object_proxy.h" |
| #include "grit/chromium_strings.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| using content::BrowserThread; |
| using content::PasswordForm; |
| |
| namespace { |
| |
| // We could localize this string, but then changing your locale would cause |
| // you to lose access to all your stored passwords. Maybe best not to do that. |
| // Name of the folder to store passwords in. |
| const char kKWalletFolder[] = "Chrome Form Data"; |
| |
| // DBus service, path, and interface names for klauncher and kwalletd. |
| const char kKWalletServiceName[] = "org.kde.kwalletd"; |
| const char kKWalletPath[] = "/modules/kwalletd"; |
| const char kKWalletInterface[] = "org.kde.KWallet"; |
| const char kKLauncherServiceName[] = "org.kde.klauncher"; |
| const char kKLauncherPath[] = "/KLauncher"; |
| const char kKLauncherInterface[] = "org.kde.KLauncher"; |
| |
| // Compares two PasswordForms and returns true if they are the same. |
| // If |update_check| is false, we only check the fields that are checked by |
| // LoginDatabase::UpdateLogin() when updating logins; otherwise, we check the |
| // fields that are checked by LoginDatabase::RemoveLogin() for removing them. |
| bool CompareForms(const content::PasswordForm& a, |
| const content::PasswordForm& b, |
| bool update_check) { |
| // An update check doesn't care about the submit element. |
| if (!update_check && a.submit_element != b.submit_element) |
| return false; |
| return a.origin == b.origin && |
| a.password_element == b.password_element && |
| a.signon_realm == b.signon_realm && |
| a.username_element == b.username_element && |
| a.username_value == b.username_value; |
| } |
| |
| // Checks a serialized list of PasswordForms for sanity. Returns true if OK. |
| // Note that |realm| is only used for generating a useful warning message. |
| bool CheckSerializedValue(const uint8_t* byte_array, |
| size_t length, |
| const std::string& realm) { |
| const Pickle::Header* header = |
| reinterpret_cast<const Pickle::Header*>(byte_array); |
| if (length < sizeof(*header) || |
| header->payload_size > length - sizeof(*header)) { |
| LOG(WARNING) << "Invalid KWallet entry detected (realm: " << realm << ")"; |
| return false; |
| } |
| return true; |
| } |
| |
| // Convenience function to read a GURL from a Pickle. Assumes the URL has |
| // been written as a UTF-8 string. Returns true on success. |
| bool ReadGURL(PickleIterator* iter, bool warn_only, GURL* url) { |
| std::string url_string; |
| if (!iter->ReadString(&url_string)) { |
| if (!warn_only) |
| LOG(ERROR) << "Failed to deserialize URL."; |
| *url = GURL(); |
| return false; |
| } |
| *url = GURL(url_string); |
| return true; |
| } |
| |
| } // namespace |
| |
| NativeBackendKWallet::NativeBackendKWallet(LocalProfileId id, |
| PrefService* prefs) |
| : profile_id_(id), |
| prefs_(prefs), |
| kwallet_proxy_(NULL), |
| app_name_(l10n_util::GetStringUTF8(IDS_PRODUCT_NAME)) { |
| // TODO(mdm): after a few more releases, remove the code which is now dead due |
| // to the true || here, and simplify this code. We don't do it yet to make it |
| // easier to revert if necessary. |
| if (true || PasswordStoreX::PasswordsUseLocalProfileId(prefs)) { |
| folder_name_ = GetProfileSpecificFolderName(); |
| // We already did the migration previously. Don't try again. |
| migrate_tried_ = true; |
| } else { |
| folder_name_ = kKWalletFolder; |
| migrate_tried_ = false; |
| } |
| } |
| |
| NativeBackendKWallet::~NativeBackendKWallet() { |
| // This destructor is called on the thread that is destroying the Profile |
| // containing the PasswordStore that owns this NativeBackend. Generally that |
| // won't be the DB thread; it will be the UI thread. So we post a message to |
| // shut it down on the DB thread, and it will be destructed afterward when the |
| // scoped_refptr<dbus::Bus> goes out of scope. The NativeBackend will be |
| // destroyed before that occurs, but that's OK. |
| if (session_bus_.get()) { |
| BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, |
| base::Bind(&dbus::Bus::ShutdownAndBlock, |
| session_bus_.get())); |
| } |
| } |
| |
| bool NativeBackendKWallet::Init() { |
| // Without the |optional_bus| parameter, a real bus will be instantiated. |
| return InitWithBus(scoped_refptr<dbus::Bus>()); |
| } |
| |
| bool NativeBackendKWallet::InitWithBus(scoped_refptr<dbus::Bus> optional_bus) { |
| // We must synchronously do a few DBus calls to figure out if initialization |
| // succeeds, but later, we'll want to do most work on the DB thread. So we |
| // have to do the initialization on the DB thread here too, and wait for it. |
| bool success = false; |
| base::WaitableEvent event(false, false); |
| // NativeBackendKWallet isn't reference counted, but we wait for InitWithBus |
| // to finish, so we can safely use base::Unretained here. |
| BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, |
| base::Bind(&NativeBackendKWallet::InitOnDBThread, |
| base::Unretained(this), |
| optional_bus, &event, &success)); |
| |
| // This ScopedAllowWait should not be here. http://crbug.com/125331 |
| base::ThreadRestrictions::ScopedAllowWait allow_wait; |
| event.Wait(); |
| return success; |
| } |
| |
| void NativeBackendKWallet::InitOnDBThread(scoped_refptr<dbus::Bus> optional_bus, |
| base::WaitableEvent* event, |
| bool* success) { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| DCHECK(!session_bus_.get()); |
| if (optional_bus.get()) { |
| // The optional_bus parameter is given when this method is called in tests. |
| session_bus_ = optional_bus; |
| } else { |
| // Get a (real) connection to the session bus. |
| dbus::Bus::Options options; |
| options.bus_type = dbus::Bus::SESSION; |
| options.connection_type = dbus::Bus::PRIVATE; |
| session_bus_ = new dbus::Bus(options); |
| } |
| kwallet_proxy_ = |
| session_bus_->GetObjectProxy(kKWalletServiceName, |
| dbus::ObjectPath(kKWalletPath)); |
| // kwalletd may not be running. If we get a temporary failure initializing it, |
| // try to start it and then try again. (Note the short-circuit evaluation.) |
| const InitResult result = InitWallet(); |
| *success = (result == INIT_SUCCESS || |
| (result == TEMPORARY_FAIL && |
| StartKWalletd() && InitWallet() == INIT_SUCCESS)); |
| event->Signal(); |
| } |
| |
| bool NativeBackendKWallet::StartKWalletd() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| // Sadly kwalletd doesn't use DBus activation, so we have to make a call to |
| // klauncher to start it. |
| dbus::ObjectProxy* klauncher = |
| session_bus_->GetObjectProxy(kKLauncherServiceName, |
| dbus::ObjectPath(kKLauncherPath)); |
| |
| dbus::MethodCall method_call(kKLauncherInterface, |
| "start_service_by_desktop_name"); |
| dbus::MessageWriter builder(&method_call); |
| std::vector<std::string> empty; |
| builder.AppendString("kwalletd"); // serviceName |
| builder.AppendArrayOfStrings(empty); // urls |
| builder.AppendArrayOfStrings(empty); // envs |
| builder.AppendString(std::string()); // startup_id |
| builder.AppendBool(false); // blind |
| scoped_ptr<dbus::Response> response( |
| klauncher->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting klauncher to start kwalletd"; |
| return false; |
| } |
| dbus::MessageReader reader(response.get()); |
| int32_t ret = -1; |
| std::string dbus_name; |
| std::string error; |
| int32_t pid = -1; |
| if (!reader.PopInt32(&ret) || !reader.PopString(&dbus_name) || |
| !reader.PopString(&error) || !reader.PopInt32(&pid)) { |
| LOG(ERROR) << "Error reading response from klauncher to start kwalletd: " |
| << response->ToString(); |
| return false; |
| } |
| if (!error.empty() || ret) { |
| LOG(ERROR) << "Error launching kwalletd: error '" << error << "' " |
| << " (code " << ret << ")"; |
| return false; |
| } |
| |
| return true; |
| } |
| |
| NativeBackendKWallet::InitResult NativeBackendKWallet::InitWallet() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| { |
| // Check that KWallet is enabled. |
| dbus::MethodCall method_call(kKWalletInterface, "isEnabled"); |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (isEnabled)"; |
| return TEMPORARY_FAIL; |
| } |
| dbus::MessageReader reader(response.get()); |
| bool enabled = false; |
| if (!reader.PopBool(&enabled)) { |
| LOG(ERROR) << "Error reading response from kwalletd (isEnabled): " |
| << response->ToString(); |
| return PERMANENT_FAIL; |
| } |
| // Not enabled? Don't use KWallet. But also don't warn here. |
| if (!enabled) { |
| VLOG(1) << "kwalletd reports that KWallet is not enabled."; |
| return PERMANENT_FAIL; |
| } |
| } |
| |
| { |
| // Get the wallet name. |
| dbus::MethodCall method_call(kKWalletInterface, "networkWallet"); |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (networkWallet)"; |
| return TEMPORARY_FAIL; |
| } |
| dbus::MessageReader reader(response.get()); |
| if (!reader.PopString(&wallet_name_)) { |
| LOG(ERROR) << "Error reading response from kwalletd (networkWallet): " |
| << response->ToString(); |
| return PERMANENT_FAIL; |
| } |
| } |
| |
| return INIT_SUCCESS; |
| } |
| |
| bool NativeBackendKWallet::AddLogin(const PasswordForm& form) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| |
| PasswordFormList forms; |
| GetLoginsList(&forms, form.signon_realm, wallet_handle); |
| |
| // We search for a login to update, rather than unconditionally appending the |
| // login, because in some cases (especially involving sync) we can be asked to |
| // add a login that already exists. In these cases we want to just update. |
| bool updated = false; |
| for (size_t i = 0; i < forms.size(); ++i) { |
| // Use the more restrictive removal comparison, so that we never have |
| // duplicate logins that would all be removed together by RemoveLogin(). |
| if (CompareForms(form, *forms[i], false)) { |
| *forms[i] = form; |
| updated = true; |
| } |
| } |
| if (!updated) |
| forms.push_back(new PasswordForm(form)); |
| |
| bool ok = SetLoginsList(forms, form.signon_realm, wallet_handle); |
| |
| STLDeleteElements(&forms); |
| return ok; |
| } |
| |
| bool NativeBackendKWallet::UpdateLogin(const PasswordForm& form) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| |
| PasswordFormList forms; |
| GetLoginsList(&forms, form.signon_realm, wallet_handle); |
| |
| for (size_t i = 0; i < forms.size(); ++i) { |
| if (CompareForms(form, *forms[i], true)) |
| *forms[i] = form; |
| } |
| |
| bool ok = SetLoginsList(forms, form.signon_realm, wallet_handle); |
| |
| STLDeleteElements(&forms); |
| return ok; |
| } |
| |
| bool NativeBackendKWallet::RemoveLogin(const PasswordForm& form) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| |
| PasswordFormList all_forms; |
| GetLoginsList(&all_forms, form.signon_realm, wallet_handle); |
| |
| PasswordFormList kept_forms; |
| kept_forms.reserve(all_forms.size()); |
| for (size_t i = 0; i < all_forms.size(); ++i) { |
| if (CompareForms(form, *all_forms[i], false)) |
| delete all_forms[i]; |
| else |
| kept_forms.push_back(all_forms[i]); |
| } |
| |
| // Update the entry in the wallet, possibly deleting it. |
| bool ok = SetLoginsList(kept_forms, form.signon_realm, wallet_handle); |
| |
| STLDeleteElements(&kept_forms); |
| return ok; |
| } |
| |
| bool NativeBackendKWallet::RemoveLoginsCreatedBetween( |
| const base::Time& delete_begin, |
| const base::Time& delete_end) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| |
| // We could probably also use readEntryList here. |
| std::vector<std::string> realm_list; |
| { |
| dbus::MethodCall method_call(kKWalletInterface, "entryList"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (entryList)"; |
| return false; |
| } |
| dbus::MessageReader reader(response.get()); |
| dbus::MessageReader array(response.get()); |
| if (!reader.PopArray(&array)) { |
| LOG(ERROR) << "Error reading response from kwalletd (entryList): " |
| << response->ToString(); |
| return false; |
| } |
| while (array.HasMoreData()) { |
| std::string realm; |
| if (!array.PopString(&realm)) { |
| LOG(ERROR) << "Error reading response from kwalletd (entryList): " |
| << response->ToString(); |
| return false; |
| } |
| realm_list.push_back(realm); |
| } |
| } |
| |
| bool ok = true; |
| for (size_t i = 0; i < realm_list.size(); ++i) { |
| const std::string& signon_realm = realm_list[i]; |
| dbus::MethodCall method_call(kKWalletInterface, "readEntry"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(signon_realm); // key |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (readEntry)"; |
| continue; |
| } |
| dbus::MessageReader reader(response.get()); |
| uint8_t* bytes = NULL; |
| size_t length = 0; |
| if (!reader.PopArrayOfBytes(&bytes, &length)) { |
| LOG(ERROR) << "Error reading response from kwalletd (readEntry): " |
| << response->ToString(); |
| continue; |
| } |
| if (!bytes || !CheckSerializedValue(bytes, length, signon_realm)) |
| continue; |
| |
| // Can't we all just agree on whether bytes are signed or not? Please? |
| Pickle pickle(reinterpret_cast<const char*>(bytes), length); |
| PasswordFormList all_forms; |
| DeserializeValue(signon_realm, pickle, &all_forms); |
| |
| PasswordFormList kept_forms; |
| kept_forms.reserve(all_forms.size()); |
| for (size_t i = 0; i < all_forms.size(); ++i) { |
| if (delete_begin <= all_forms[i]->date_created && |
| (delete_end.is_null() || all_forms[i]->date_created < delete_end)) { |
| delete all_forms[i]; |
| } else { |
| kept_forms.push_back(all_forms[i]); |
| } |
| } |
| |
| if (!SetLoginsList(kept_forms, signon_realm, wallet_handle)) |
| ok = false; |
| STLDeleteElements(&kept_forms); |
| } |
| return ok; |
| } |
| |
| bool NativeBackendKWallet::GetLogins(const PasswordForm& form, |
| PasswordFormList* forms) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| return GetLoginsList(forms, form.signon_realm, wallet_handle); |
| } |
| |
| bool NativeBackendKWallet::GetLoginsCreatedBetween(const base::Time& get_begin, |
| const base::Time& get_end, |
| PasswordFormList* forms) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| return GetLoginsList(forms, get_begin, get_end, wallet_handle); |
| } |
| |
| bool NativeBackendKWallet::GetAutofillableLogins(PasswordFormList* forms) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| return GetLoginsList(forms, true, wallet_handle); |
| } |
| |
| bool NativeBackendKWallet::GetBlacklistLogins(PasswordFormList* forms) { |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return false; |
| return GetLoginsList(forms, false, wallet_handle); |
| } |
| |
| bool NativeBackendKWallet::GetLoginsList(PasswordFormList* forms, |
| const std::string& signon_realm, |
| int wallet_handle) { |
| // Is there an entry in the wallet? |
| { |
| dbus::MethodCall method_call(kKWalletInterface, "hasEntry"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(signon_realm); // key |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (hasEntry)"; |
| return false; |
| } |
| dbus::MessageReader reader(response.get()); |
| bool has_entry = false; |
| if (!reader.PopBool(&has_entry)) { |
| LOG(ERROR) << "Error reading response from kwalletd (hasEntry): " |
| << response->ToString(); |
| return false; |
| } |
| if (!has_entry) { |
| // This is not an error. There just isn't a matching entry. |
| return true; |
| } |
| } |
| |
| { |
| dbus::MethodCall method_call(kKWalletInterface, "readEntry"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(signon_realm); // key |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (readEntry)"; |
| return false; |
| } |
| dbus::MessageReader reader(response.get()); |
| uint8_t* bytes = NULL; |
| size_t length = 0; |
| if (!reader.PopArrayOfBytes(&bytes, &length)) { |
| LOG(ERROR) << "Error reading response from kwalletd (readEntry): " |
| << response->ToString(); |
| return false; |
| } |
| if (!bytes) |
| return false; |
| if (!CheckSerializedValue(bytes, length, signon_realm)) { |
| // This is weird, but we choose not to call it an error. There is an |
| // invalid entry somehow, but by just ignoring it, we make it easier to |
| // repair without having to delete it using kwalletmanager (that is, by |
| // just saving a new password within this realm to overwrite it). |
| return true; |
| } |
| |
| // Can't we all just agree on whether bytes are signed or not? Please? |
| Pickle pickle(reinterpret_cast<const char*>(bytes), length); |
| PasswordFormList all_forms; |
| DeserializeValue(signon_realm, pickle, forms); |
| } |
| |
| return true; |
| } |
| |
| bool NativeBackendKWallet::GetLoginsList(PasswordFormList* forms, |
| bool autofillable, |
| int wallet_handle) { |
| PasswordFormList all_forms; |
| if (!GetAllLogins(&all_forms, wallet_handle)) |
| return false; |
| |
| // We have to read all the entries, and then filter them here. |
| forms->reserve(forms->size() + all_forms.size()); |
| for (size_t i = 0; i < all_forms.size(); ++i) { |
| if (all_forms[i]->blacklisted_by_user == !autofillable) |
| forms->push_back(all_forms[i]); |
| else |
| delete all_forms[i]; |
| } |
| |
| return true; |
| } |
| |
| bool NativeBackendKWallet::GetLoginsList(PasswordFormList* forms, |
| const base::Time& begin, |
| const base::Time& end, |
| int wallet_handle) { |
| PasswordFormList all_forms; |
| if (!GetAllLogins(&all_forms, wallet_handle)) |
| return false; |
| |
| // We have to read all the entries, and then filter them here. |
| forms->reserve(forms->size() + all_forms.size()); |
| for (size_t i = 0; i < all_forms.size(); ++i) { |
| if (begin <= all_forms[i]->date_created && |
| (end.is_null() || all_forms[i]->date_created < end)) { |
| forms->push_back(all_forms[i]); |
| } else { |
| delete all_forms[i]; |
| } |
| } |
| |
| return true; |
| } |
| |
| bool NativeBackendKWallet::GetAllLogins(PasswordFormList* forms, |
| int wallet_handle) { |
| // We could probably also use readEntryList here. |
| std::vector<std::string> realm_list; |
| { |
| dbus::MethodCall method_call(kKWalletInterface, "entryList"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (entryList)"; |
| return false; |
| } |
| dbus::MessageReader reader(response.get()); |
| if (!reader.PopArrayOfStrings(&realm_list)) { |
| LOG(ERROR) << "Error reading response from kwalletd (entryList): " |
| << response->ToString(); |
| return false; |
| } |
| } |
| |
| for (size_t i = 0; i < realm_list.size(); ++i) { |
| const std::string& signon_realm = realm_list[i]; |
| dbus::MethodCall method_call(kKWalletInterface, "readEntry"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(signon_realm); // key |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (readEntry)"; |
| continue; |
| } |
| dbus::MessageReader reader(response.get()); |
| uint8_t* bytes = NULL; |
| size_t length = 0; |
| if (!reader.PopArrayOfBytes(&bytes, &length)) { |
| LOG(ERROR) << "Error reading response from kwalletd (readEntry): " |
| << response->ToString(); |
| continue; |
| } |
| if (!bytes || !CheckSerializedValue(bytes, length, signon_realm)) |
| continue; |
| |
| // Can't we all just agree on whether bytes are signed or not? Please? |
| Pickle pickle(reinterpret_cast<const char*>(bytes), length); |
| PasswordFormList all_forms; |
| DeserializeValue(signon_realm, pickle, forms); |
| } |
| return true; |
| } |
| |
| bool NativeBackendKWallet::SetLoginsList(const PasswordFormList& forms, |
| const std::string& signon_realm, |
| int wallet_handle) { |
| if (forms.empty()) { |
| // No items left? Remove the entry from the wallet. |
| dbus::MethodCall method_call(kKWalletInterface, "removeEntry"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(signon_realm); // key |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (removeEntry)"; |
| return kInvalidKWalletHandle; |
| } |
| dbus::MessageReader reader(response.get()); |
| int ret = 0; |
| if (!reader.PopInt32(&ret)) { |
| LOG(ERROR) << "Error reading response from kwalletd (removeEntry): " |
| << response->ToString(); |
| return false; |
| } |
| if (ret != 0) |
| LOG(ERROR) << "Bad return code " << ret << " from KWallet removeEntry"; |
| return ret == 0; |
| } |
| |
| Pickle value; |
| SerializeValue(forms, &value); |
| |
| dbus::MethodCall method_call(kKWalletInterface, "writeEntry"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(wallet_handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(signon_realm); // key |
| builder.AppendArrayOfBytes(static_cast<const uint8_t*>(value.data()), |
| value.size()); // value |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (writeEntry)"; |
| return kInvalidKWalletHandle; |
| } |
| dbus::MessageReader reader(response.get()); |
| int ret = 0; |
| if (!reader.PopInt32(&ret)) { |
| LOG(ERROR) << "Error reading response from kwalletd (writeEntry): " |
| << response->ToString(); |
| return false; |
| } |
| if (ret != 0) |
| LOG(ERROR) << "Bad return code " << ret << " from KWallet writeEntry"; |
| return ret == 0; |
| } |
| |
| // static |
| void NativeBackendKWallet::SerializeValue(const PasswordFormList& forms, |
| Pickle* pickle) { |
| pickle->WriteInt(kPickleVersion); |
| pickle->WriteUInt64(forms.size()); |
| for (PasswordFormList::const_iterator it = forms.begin(); |
| it != forms.end(); ++it) { |
| const PasswordForm* form = *it; |
| pickle->WriteInt(form->scheme); |
| pickle->WriteString(form->origin.spec()); |
| pickle->WriteString(form->action.spec()); |
| pickle->WriteString16(form->username_element); |
| pickle->WriteString16(form->username_value); |
| pickle->WriteString16(form->password_element); |
| pickle->WriteString16(form->password_value); |
| pickle->WriteString16(form->submit_element); |
| pickle->WriteBool(form->ssl_valid); |
| pickle->WriteBool(form->preferred); |
| pickle->WriteBool(form->blacklisted_by_user); |
| pickle->WriteInt64(form->date_created.ToTimeT()); |
| } |
| } |
| |
| // static |
| bool NativeBackendKWallet::DeserializeValueSize(const std::string& signon_realm, |
| const PickleIterator& init_iter, |
| bool size_32, bool warn_only, |
| PasswordFormList* forms) { |
| PickleIterator iter = init_iter; |
| |
| uint64_t count = 0; |
| if (size_32) { |
| uint32_t count_32 = 0; |
| if (!iter.ReadUInt32(&count_32)) { |
| LOG(ERROR) << "Failed to deserialize KWallet entry " |
| << "(realm: " << signon_realm << ")"; |
| return false; |
| } |
| count = count_32; |
| } else { |
| if (!iter.ReadUInt64(&count)) { |
| LOG(ERROR) << "Failed to deserialize KWallet entry " |
| << "(realm: " << signon_realm << ")"; |
| return false; |
| } |
| } |
| |
| if (count > 0xFFFF) { |
| // Trying to pin down the cause of http://crbug.com/80728 (or fix it). |
| // This is a very large number of passwords to be saved for a single realm. |
| // It is almost certainly a corrupt pickle and not real data. Ignore it. |
| // This very well might actually be http://crbug.com/107701, so if we're |
| // reading an old pickle, we don't even log this the first time we try to |
| // read it. (That is, when we're reading the native architecture size.) |
| if (!warn_only) { |
| LOG(ERROR) << "Suspiciously large number of entries in KWallet entry " |
| << "(" << count << "; realm: " << signon_realm << ")"; |
| } |
| return false; |
| } |
| |
| forms->reserve(forms->size() + count); |
| for (uint64_t i = 0; i < count; ++i) { |
| scoped_ptr<PasswordForm> form(new PasswordForm()); |
| form->signon_realm.assign(signon_realm); |
| |
| int scheme = 0; |
| int64 date_created = 0; |
| // Note that these will be read back in the order listed due to |
| // short-circuit evaluation. This is important. |
| if (!iter.ReadInt(&scheme) || |
| !ReadGURL(&iter, warn_only, &form->origin) || |
| !ReadGURL(&iter, warn_only, &form->action) || |
| !iter.ReadString16(&form->username_element) || |
| !iter.ReadString16(&form->username_value) || |
| !iter.ReadString16(&form->password_element) || |
| !iter.ReadString16(&form->password_value) || |
| !iter.ReadString16(&form->submit_element) || |
| !iter.ReadBool(&form->ssl_valid) || |
| !iter.ReadBool(&form->preferred) || |
| !iter.ReadBool(&form->blacklisted_by_user) || |
| !iter.ReadInt64(&date_created)) { |
| if (warn_only) { |
| LOG(WARNING) << "Failed to deserialize version 0 KWallet entry " |
| << "(realm: " << signon_realm << ") with native " |
| << "architecture size; will try alternate size."; |
| } else { |
| LOG(ERROR) << "Failed to deserialize KWallet entry " |
| << "(realm: " << signon_realm << ")"; |
| } |
| return false; |
| } |
| form->scheme = static_cast<PasswordForm::Scheme>(scheme); |
| form->date_created = base::Time::FromTimeT(date_created); |
| forms->push_back(form.release()); |
| } |
| |
| return true; |
| } |
| |
| // static |
| void NativeBackendKWallet::DeserializeValue(const std::string& signon_realm, |
| const Pickle& pickle, |
| PasswordFormList* forms) { |
| PickleIterator iter(pickle); |
| |
| int version = -1; |
| if (!iter.ReadInt(&version) || |
| version < 0 || version > kPickleVersion) { |
| LOG(ERROR) << "Failed to deserialize KWallet entry " |
| << "(realm: " << signon_realm << ")"; |
| return; |
| } |
| |
| if (version == kPickleVersion) { |
| // In current pickles, we expect 64-bit sizes. Failure is an error. |
| DeserializeValueSize(signon_realm, iter, false, false, forms); |
| return; |
| } |
| |
| const size_t saved_forms_size = forms->size(); |
| const bool size_32 = sizeof(size_t) == sizeof(uint32_t); |
| if (!DeserializeValueSize(signon_realm, iter, size_32, true, forms)) { |
| // We failed to read the pickle using the native architecture of the system. |
| // Try again with the opposite architecture. Note that we do this even on |
| // 32-bit machines, in case we're reading a 64-bit pickle. (Probably rare, |
| // since mostly we expect upgrades, not downgrades, but both are possible.) |
| forms->resize(saved_forms_size); |
| DeserializeValueSize(signon_realm, iter, !size_32, false, forms); |
| } |
| } |
| |
| int NativeBackendKWallet::WalletHandle() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| |
| // Open the wallet. |
| // TODO(mdm): Are we leaking these handles? Find out. |
| int32_t handle = kInvalidKWalletHandle; |
| { |
| dbus::MethodCall method_call(kKWalletInterface, "open"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendString(wallet_name_); // wallet |
| builder.AppendInt64(0); // wid |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (open)"; |
| return kInvalidKWalletHandle; |
| } |
| dbus::MessageReader reader(response.get()); |
| if (!reader.PopInt32(&handle)) { |
| LOG(ERROR) << "Error reading response from kwalletd (open): " |
| << response->ToString(); |
| return kInvalidKWalletHandle; |
| } |
| if (handle == kInvalidKWalletHandle) { |
| LOG(ERROR) << "Error obtaining KWallet handle"; |
| return kInvalidKWalletHandle; |
| } |
| } |
| |
| // Check if our folder exists. |
| bool has_folder = false; |
| { |
| dbus::MethodCall method_call(kKWalletInterface, "hasFolder"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (hasFolder)"; |
| return kInvalidKWalletHandle; |
| } |
| dbus::MessageReader reader(response.get()); |
| if (!reader.PopBool(&has_folder)) { |
| LOG(ERROR) << "Error reading response from kwalletd (hasFolder): " |
| << response->ToString(); |
| return kInvalidKWalletHandle; |
| } |
| } |
| |
| // Create it if it didn't. |
| if (!has_folder) { |
| dbus::MethodCall method_call(kKWalletInterface, "createFolder"); |
| dbus::MessageWriter builder(&method_call); |
| builder.AppendInt32(handle); // handle |
| builder.AppendString(folder_name_); // folder |
| builder.AppendString(app_name_); // appid |
| scoped_ptr<dbus::Response> response( |
| kwallet_proxy_->CallMethodAndBlock( |
| &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT)); |
| if (!response.get()) { |
| LOG(ERROR) << "Error contacting kwalletd (createFolder)"; |
| return kInvalidKWalletHandle; |
| } |
| dbus::MessageReader reader(response.get()); |
| bool success = false; |
| if (!reader.PopBool(&success)) { |
| LOG(ERROR) << "Error reading response from kwalletd (createFolder): " |
| << response->ToString(); |
| return kInvalidKWalletHandle; |
| } |
| if (!success) { |
| LOG(ERROR) << "Error creating KWallet folder"; |
| return kInvalidKWalletHandle; |
| } |
| } |
| |
| // Successful initialization. Try migration if necessary. |
| if (!migrate_tried_) |
| MigrateToProfileSpecificLogins(); |
| |
| return handle; |
| } |
| |
| std::string NativeBackendKWallet::GetProfileSpecificFolderName() const { |
| // Originally, the folder name was always just "Chrome Form Data". |
| // Now we use it to distinguish passwords for different profiles. |
| return base::StringPrintf("%s (%d)", kKWalletFolder, profile_id_); |
| } |
| |
| void NativeBackendKWallet::MigrateToProfileSpecificLogins() { |
| DCHECK(BrowserThread::CurrentlyOn(BrowserThread::DB)); |
| |
| DCHECK(!migrate_tried_); |
| DCHECK_EQ(folder_name_, kKWalletFolder); |
| |
| // Record the fact that we've attempted migration already right away, so that |
| // we don't get recursive calls back to MigrateToProfileSpecificLogins(). |
| migrate_tried_ = true; |
| |
| // First get all the logins, using the old folder name. |
| int wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| return; |
| PasswordFormList forms; |
| if (!GetAllLogins(&forms, wallet_handle)) |
| return; |
| |
| // Now switch to a profile-specific folder name. |
| folder_name_ = GetProfileSpecificFolderName(); |
| |
| // Try to add all the logins with the new folder name. |
| // This could be done more efficiently by grouping by signon realm and using |
| // SetLoginsList(), but we do this for simplicity since it is only done once. |
| // Note, however, that we do need another call to WalletHandle() to create |
| // this folder if necessary. |
| bool ok = true; |
| for (size_t i = 0; i < forms.size(); ++i) { |
| if (!AddLogin(*forms[i])) |
| ok = false; |
| delete forms[i]; |
| } |
| if (forms.empty()) { |
| // If there were no logins to migrate, we do an extra call to WalletHandle() |
| // for its side effect of attempting to create the profile-specific folder. |
| // This is not strictly necessary, but it's safe and helps in testing. |
| wallet_handle = WalletHandle(); |
| if (wallet_handle == kInvalidKWalletHandle) |
| ok = false; |
| } |
| |
| if (ok) { |
| // All good! Keep the new app string and set a persistent pref. |
| // NOTE: We explicitly don't delete the old passwords yet. They are |
| // potentially shared with other profiles and other user data dirs! |
| // Each other profile must be able to migrate the shared data as well, |
| // so we must leave it alone. After a few releases, we'll add code to |
| // delete them, and eventually remove this migration code. |
| // TODO(mdm): follow through with the plan above. |
| PasswordStoreX::SetPasswordsUseLocalProfileId(prefs_); |
| } else { |
| // We failed to migrate for some reason. Use the old folder name. |
| folder_name_ = kKWalletFolder; |
| } |
| } |