blob: 252af3d813ce097c9202f0a56151ca497734440a [file] [log] [blame]
// Copyright (c) 2015 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_libsecret.h"
#include <dlfcn.h>
#include <list>
#include "base/basictypes.h"
#include "base/logging.h"
#include "base/memory/scoped_ptr.h"
#include "base/metrics/histogram.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "components/password_manager/core/browser/password_manager_metrics_util.h"
using autofill::PasswordForm;
using base::UTF8ToUTF16;
using base::UTF16ToUTF8;
using namespace password_manager::metrics_util;
namespace {
const char kEmptyString[] = "";
const int kMaxPossibleTimeTValue = std::numeric_limits<int>::max();
}
typeof(&::secret_password_store_sync)
LibsecretLoader::secret_password_store_sync;
typeof(&::secret_service_search_sync)
LibsecretLoader::secret_service_search_sync;
typeof(&::secret_password_clear_sync)
LibsecretLoader::secret_password_clear_sync;
typeof(&::secret_item_get_secret) LibsecretLoader::secret_item_get_secret;
typeof(&::secret_value_get_text) LibsecretLoader::secret_value_get_text;
typeof(&::secret_item_get_attributes)
LibsecretLoader::secret_item_get_attributes;
typeof(&::secret_item_load_secret_sync)
LibsecretLoader::secret_item_load_secret_sync;
typeof(&::secret_value_unref) LibsecretLoader::secret_value_unref;
bool LibsecretLoader::libsecret_loaded = false;
const LibsecretLoader::FunctionInfo LibsecretLoader::functions[] = {
{"secret_password_store_sync",
reinterpret_cast<void**>(&secret_password_store_sync)},
{"secret_service_search_sync",
reinterpret_cast<void**>(&secret_service_search_sync)},
{"secret_password_clear_sync",
reinterpret_cast<void**>(&secret_password_clear_sync)},
{"secret_item_get_secret",
reinterpret_cast<void**>(&secret_item_get_secret)},
{"secret_value_get_text", reinterpret_cast<void**>(&secret_value_get_text)},
{"secret_item_get_attributes",
reinterpret_cast<void**>(&secret_item_get_attributes)},
{"secret_item_load_secret_sync",
reinterpret_cast<void**>(&secret_item_load_secret_sync)},
{"secret_value_unref", reinterpret_cast<void**>(&secret_value_unref)},
{nullptr, nullptr}};
bool LibsecretLoader::LoadLibsecret() {
if (libsecret_loaded)
return true;
void* handle = dlopen("libsecret-1.so.0", RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
// We wanted to use libsecret, but we couldn't load it. Warn, because
// either the user asked for this, or we autodetected it incorrectly. (Or
// the system has broken libraries, which is also good to warn about.)
VLOG(1) << "Could not load libsecret-1.so.0: " << dlerror();
return false;
}
for (size_t i = 0; functions[i].name; ++i) {
dlerror();
*functions[i].pointer = dlsym(handle, functions[i].name);
const char* error = dlerror();
if (error) {
VLOG(1) << "Unable to load symbol " << functions[i].name << ": " << error;
dlclose(handle);
return false;
}
}
libsecret_loaded = true;
// We leak the library handle. That's OK: this function is called only once.
return true;
}
namespace {
const char kLibsecretAppString[] = "chrome";
// Schema is analagous to the fields in PasswordForm.
const SecretSchema kLibsecretSchema = {
"chrome_libsecret_password_schema",
// We have to use SECRET_SCHEMA_DONT_MATCH_NAME in order to get old
// passwords stored with gnome_keyring.
SECRET_SCHEMA_DONT_MATCH_NAME,
{{"origin_url", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"action_url", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"username_element", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"username_value", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"password_element", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"submit_element", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"signon_realm", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"ssl_valid", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"preferred", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"date_created", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"blacklisted_by_user", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"scheme", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"type", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"times_used", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"date_synced", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"display_name", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"avatar_url", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"federation_url", SECRET_SCHEMA_ATTRIBUTE_STRING},
{"skip_zero_click", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"generation_upload_status", SECRET_SCHEMA_ATTRIBUTE_INTEGER},
{"form_data", SECRET_SCHEMA_ATTRIBUTE_STRING},
// This field is always "chrome-profile_id" so that we can search for it.
{"application", SECRET_SCHEMA_ATTRIBUTE_STRING},
{nullptr, SECRET_SCHEMA_ATTRIBUTE_STRING}}};
const char* GetStringFromAttributes(GHashTable* attrs, const char* keyname) {
gpointer value = g_hash_table_lookup(attrs, keyname);
return value ? static_cast<char*>(value) : kEmptyString;
}
uint32_t GetUintFromAttributes(GHashTable* attrs, const char* keyname) {
gpointer value = g_hash_table_lookup(attrs, keyname);
if (!value)
return uint32_t();
uint32_t result;
bool value_ok = base::StringToUint(static_cast<char*>(value), &result);
DCHECK(value_ok);
return result;
}
// Convert the attributes into a new PasswordForm.
// Note: does *not* get the actual password, as that is not a key attribute!
// Returns nullptr if the attributes are for the wrong application.
scoped_ptr<PasswordForm> FormOutOfAttributes(GHashTable* attrs) {
base::StringPiece app_value = GetStringFromAttributes(attrs, "application");
if (!app_value.starts_with(kLibsecretAppString))
return scoped_ptr<PasswordForm>();
scoped_ptr<PasswordForm> form(new PasswordForm());
form->origin = GURL(GetStringFromAttributes(attrs, "origin_url"));
form->action = GURL(GetStringFromAttributes(attrs, "action_url"));
form->username_element =
UTF8ToUTF16(GetStringFromAttributes(attrs, "username_element"));
form->username_value =
UTF8ToUTF16(GetStringFromAttributes(attrs, "username_value"));
form->password_element =
UTF8ToUTF16(GetStringFromAttributes(attrs, "password_element"));
form->submit_element =
UTF8ToUTF16(GetStringFromAttributes(attrs, "submit_element"));
form->signon_realm = GetStringFromAttributes(attrs, "signon_realm");
form->ssl_valid = GetUintFromAttributes(attrs, "ssl_valid");
form->preferred = GetUintFromAttributes(attrs, "preferred");
int64 date_created = 0;
bool date_ok = base::StringToInt64(
GetStringFromAttributes(attrs, "date_created"), &date_created);
DCHECK(date_ok);
// In the past |date_created| was stored as time_t. Currently is stored as
// base::Time's internal value. We need to distinguish, which format the
// number in |date_created| was stored in. We use the fact that
// kMaxPossibleTimeTValue interpreted as the internal value corresponds to an
// unlikely date back in 17th century, and anything above
// kMaxPossibleTimeTValue clearly must be in the internal value format.
form->date_created = date_created < kMaxPossibleTimeTValue
? base::Time::FromTimeT(date_created)
: base::Time::FromInternalValue(date_created);
form->blacklisted_by_user =
GetUintFromAttributes(attrs, "blacklisted_by_user");
form->type =
static_cast<PasswordForm::Type>(GetUintFromAttributes(attrs, "type"));
form->times_used = GetUintFromAttributes(attrs, "times_used");
form->scheme =
static_cast<PasswordForm::Scheme>(GetUintFromAttributes(attrs, "scheme"));
int64 date_synced = 0;
base::StringToInt64(GetStringFromAttributes(attrs, "date_synced"),
&date_synced);
form->date_synced = base::Time::FromInternalValue(date_synced);
form->display_name =
UTF8ToUTF16(GetStringFromAttributes(attrs, "display_name"));
form->avatar_url = GURL(GetStringFromAttributes(attrs, "avatar_url"));
form->federation_url = GURL(GetStringFromAttributes(attrs, "federation_url"));
form->skip_zero_click = GetUintFromAttributes(attrs, "skip_zero_click");
form->generation_upload_status =
static_cast<PasswordForm::GenerationUploadStatus>(
GetUintFromAttributes(attrs, "generation_upload_status"));
base::StringPiece encoded_form_data =
GetStringFromAttributes(attrs, "form_data");
if (!encoded_form_data.empty()) {
bool success = DeserializeFormDataFromBase64String(encoded_form_data,
&form->form_data);
FormDeserializationStatus status = success ? GNOME_SUCCESS : GNOME_FAILURE;
LogFormDataDeserializationStatus(status);
}
return form.Pass();
}
class LibsecretAttributesBuilder {
public:
LibsecretAttributesBuilder();
~LibsecretAttributesBuilder();
void Append(const std::string& name, const std::string& value);
void Append(const std::string& name, int64 value);
// GHashTable, its keys and values returned from Get() are destroyed in
// |LibsecretAttributesBuilder| desctructor.
GHashTable* Get() { return attrs_; }
private:
// |name_values_| is a storage for strings referenced in |attrs_|.
std::list<std::string> name_values_;
GHashTable* attrs_;
};
LibsecretAttributesBuilder::LibsecretAttributesBuilder() {
attrs_ = g_hash_table_new_full(g_str_hash, g_str_equal,
nullptr, // no deleter for keys
nullptr); // no deleter for values
}
LibsecretAttributesBuilder::~LibsecretAttributesBuilder() {
g_hash_table_destroy(attrs_);
}
void LibsecretAttributesBuilder::Append(const std::string& name,
const std::string& value) {
name_values_.push_back(name);
gpointer name_str =
static_cast<gpointer>(const_cast<char*>(name_values_.back().c_str()));
name_values_.push_back(value);
gpointer value_str =
static_cast<gpointer>(const_cast<char*>(name_values_.back().c_str()));
g_hash_table_insert(attrs_, name_str, value_str);
}
void LibsecretAttributesBuilder::Append(const std::string& name, int64 value) {
Append(name, base::Int64ToString(value));
}
// Generates a profile-specific app string based on profile_id_.
std::string GetProfileSpecificAppString(LocalProfileId id) {
// Originally, the application string was always just "chrome" and used only
// so that we had *something* to search for since GNOME Keyring won't search
// for nothing. Now we use it to distinguish passwords for different profiles.
return base::StringPrintf("%s-%d", kLibsecretAppString, id);
}
} // namespace
bool LibsecretLoader::LibsecretIsAvailable() {
if (!libsecret_loaded)
return false;
// A dummy query is made to check for availability, because libsecret doesn't
// have a dedicated availability function. For performance reasons, the query
// is meant to return an empty result.
LibsecretAttributesBuilder attrs;
attrs.Append("application", "chrome-string_to_get_empty_result");
GError* error = nullptr;
GList* found = secret_service_search_sync(nullptr, // default secret service
&kLibsecretSchema, attrs.Get(),
SECRET_SEARCH_ALL,
nullptr, // no cancellable ojbect
&error);
bool success = (error == nullptr);
if (error)
g_error_free(error);
if (found)
g_list_free(found);
return success;
}
NativeBackendLibsecret::NativeBackendLibsecret(LocalProfileId id)
: app_string_(GetProfileSpecificAppString(id)) {
}
NativeBackendLibsecret::~NativeBackendLibsecret() {
}
bool NativeBackendLibsecret::Init() {
return LoadLibsecret() && LibsecretIsAvailable();
}
password_manager::PasswordStoreChangeList NativeBackendLibsecret::AddLogin(
const PasswordForm& form) {
// Based on LoginDatabase::AddLogin(), we search for an existing match based
// on origin_url, username_element, username_value, password_element, submit
// element, and signon_realm first, remove that, and then add the new entry.
// We'd add the new one first, and then delete the original, but then the
// delete might actually delete the newly-added entry!
ScopedVector<autofill::PasswordForm> forms =
AddUpdateLoginSearch(form, SEARCH_USE_SUBMIT);
password_manager::PasswordStoreChangeList changes;
if (forms.size() > 0) {
if (forms.size() > 1) {
VLOG(1) << "Adding login when there are " << forms.size()
<< " matching logins already! Will replace only the first.";
}
if (RemoveLogin(*forms[0])) {
changes.push_back(password_manager::PasswordStoreChange(
password_manager::PasswordStoreChange::REMOVE, *forms[0]));
}
}
if (RawAddLogin(form)) {
changes.push_back(password_manager::PasswordStoreChange(
password_manager::PasswordStoreChange::ADD, form));
}
return changes;
}
bool NativeBackendLibsecret::UpdateLogin(
const PasswordForm& form,
password_manager::PasswordStoreChangeList* changes) {
// Based on LoginDatabase::UpdateLogin(), we search for forms to update by
// origin_url, username_element, username_value, password_element, and
// signon_realm. We then compare the result to the updated form. If they
// differ in any of the mutable fields, then we remove the original, and
// then add the new entry. We'd add the new one first, and then delete the
// original, but then the delete might actually delete the newly-added entry!
DCHECK(changes);
changes->clear();
ScopedVector<autofill::PasswordForm> forms =
AddUpdateLoginSearch(form, SEARCH_IGNORE_SUBMIT);
bool removed = false;
for (size_t i = 0; i < forms.size(); ++i) {
if (*forms[i] != form) {
RemoveLogin(*forms[i]);
removed = true;
}
}
if (!removed)
return true;
if (RawAddLogin(form)) {
password_manager::PasswordStoreChange change(
password_manager::PasswordStoreChange::UPDATE, form);
changes->push_back(change);
return true;
}
return false;
}
bool NativeBackendLibsecret::RemoveLogin(const autofill::PasswordForm& form) {
GError* error = nullptr;
secret_password_clear_sync(
&kLibsecretSchema, nullptr, &error, "origin_url",
form.origin.spec().c_str(), "username_element",
UTF16ToUTF8(form.username_element).c_str(), "username_value",
UTF16ToUTF8(form.username_value).c_str(), "password_element",
UTF16ToUTF8(form.password_element).c_str(), "submit_element",
UTF16ToUTF8(form.submit_element).c_str(), "signon_realm",
form.signon_realm.c_str(), "application", app_string_.c_str(), nullptr);
if (error) {
VLOG(1) << "Libsecret delete failed: " << error->message;
g_error_free(error);
return false;
}
return true;
}
bool NativeBackendLibsecret::RemoveLoginsCreatedBetween(
base::Time delete_begin,
base::Time delete_end,
password_manager::PasswordStoreChangeList* changes) {
return RemoveLoginsBetween(delete_begin, delete_end, CREATION_TIMESTAMP,
changes);
}
bool NativeBackendLibsecret::RemoveLoginsSyncedBetween(
base::Time delete_begin,
base::Time delete_end,
password_manager::PasswordStoreChangeList* changes) {
return RemoveLoginsBetween(delete_begin, delete_end, SYNC_TIMESTAMP, changes);
}
bool NativeBackendLibsecret::GetLogins(
const PasswordForm& form,
ScopedVector<autofill::PasswordForm>* forms) {
return GetLoginsList(&form, ALL_LOGINS, forms);
}
ScopedVector<autofill::PasswordForm>
NativeBackendLibsecret::AddUpdateLoginSearch(
const autofill::PasswordForm& lookup_form,
AddUpdateLoginSearchOptions options) {
LibsecretAttributesBuilder attrs;
attrs.Append("origin_url", lookup_form.origin.spec());
attrs.Append("username_element", UTF16ToUTF8(lookup_form.username_element));
attrs.Append("username_value", UTF16ToUTF8(lookup_form.username_value));
attrs.Append("password_element", UTF16ToUTF8(lookup_form.password_element));
if (options == SEARCH_USE_SUBMIT)
attrs.Append("submit_element", UTF16ToUTF8(lookup_form.submit_element));
attrs.Append("signon_realm", lookup_form.signon_realm);
attrs.Append("application", app_string_);
GError* error = nullptr;
GList* found = secret_service_search_sync(nullptr, // default secret service
&kLibsecretSchema, attrs.Get(),
SECRET_SEARCH_ALL,
nullptr, // no cancellable ojbect
&error);
if (error) {
VLOG(1) << "Unable to get logins " << error->message;
g_error_free(error);
if (found)
g_list_free(found);
return ScopedVector<autofill::PasswordForm>();
}
return ConvertFormList(found, &lookup_form);
}
bool NativeBackendLibsecret::RawAddLogin(const PasswordForm& form) {
int64 date_created = form.date_created.ToInternalValue();
// If we are asked to save a password with 0 date, use the current time.
// We don't want to actually save passwords as though on January 1, 1601.
if (!date_created)
date_created = base::Time::Now().ToInternalValue();
int64 date_synced = form.date_synced.ToInternalValue();
std::string form_data;
SerializeFormDataToBase64String(form.form_data, &form_data);
GError* error = nullptr;
secret_password_store_sync(
&kLibsecretSchema,
nullptr, // Default collection.
form.origin.spec().c_str(), // Display name.
UTF16ToUTF8(form.password_value).c_str(),
nullptr, // no cancellable ojbect
&error,
"origin_url", form.origin.spec().c_str(),
"action_url", form.action.spec().c_str(),
"username_element", UTF16ToUTF8(form.username_element).c_str(),
"username_value", UTF16ToUTF8(form.username_value).c_str(),
"password_element", UTF16ToUTF8(form.password_element).c_str(),
"submit_element", UTF16ToUTF8(form.submit_element).c_str(),
"signon_realm", form.signon_realm.c_str(),
"ssl_valid", form.ssl_valid,
"preferred", form.preferred,
"date_created", base::Int64ToString(date_created).c_str(),
"blacklisted_by_user", form.blacklisted_by_user,
"type", form.type,
"times_used", form.times_used,
"scheme", form.scheme,
"date_synced", base::Int64ToString(date_synced).c_str(),
"display_name", UTF16ToUTF8(form.display_name).c_str(),
"avatar_url", form.avatar_url.spec().c_str(),
"federation_url", form.federation_url.spec().c_str(),
"skip_zero_click", form.skip_zero_click,
"generation_upload_status", form.generation_upload_status,
"form_data", form_data.c_str(),
"application", app_string_.c_str(), nullptr);
if (error) {
VLOG(1) << "Libsecret add raw login failed: " << error->message;
g_error_free(error);
return false;
}
return true;
}
bool NativeBackendLibsecret::GetAutofillableLogins(
ScopedVector<autofill::PasswordForm>* forms) {
return GetLoginsList(nullptr, AUTOFILLABLE_LOGINS, forms);
}
bool NativeBackendLibsecret::GetBlacklistLogins(
ScopedVector<autofill::PasswordForm>* forms) {
return GetLoginsList(nullptr, BLACKLISTED_LOGINS, forms);
}
bool NativeBackendLibsecret::GetLoginsList(
const PasswordForm* lookup_form,
GetLoginsListOptions options,
ScopedVector<autofill::PasswordForm>* forms) {
LibsecretAttributesBuilder attrs;
attrs.Append("application", app_string_);
if (options != ALL_LOGINS)
attrs.Append("blacklisted_by_user", options == BLACKLISTED_LOGINS);
if (lookup_form &&
!password_manager::ShouldPSLDomainMatchingApply(
password_manager::GetRegistryControlledDomain(
GURL(lookup_form->signon_realm))))
attrs.Append("signon_realm", lookup_form->signon_realm);
GError* error = nullptr;
GList* found = secret_service_search_sync(nullptr, // default secret service
&kLibsecretSchema, attrs.Get(),
SECRET_SEARCH_ALL,
nullptr, // no cancellable ojbect
&error);
if (error) {
VLOG(1) << "Unable to get logins " << error->message;
g_error_free(error);
if (found)
g_list_free(found);
return false;
}
*forms = ConvertFormList(found, lookup_form);
return true;
}
bool NativeBackendLibsecret::GetLoginsBetween(
base::Time get_begin,
base::Time get_end,
TimestampToCompare date_to_compare,
ScopedVector<autofill::PasswordForm>* forms) {
forms->clear();
ScopedVector<autofill::PasswordForm> all_forms;
if (!GetLoginsList(nullptr, ALL_LOGINS, &all_forms))
return false;
base::Time autofill::PasswordForm::*date_member =
date_to_compare == CREATION_TIMESTAMP
? &autofill::PasswordForm::date_created
: &autofill::PasswordForm::date_synced;
for (auto& saved_form : all_forms) {
if (get_begin <= saved_form->*date_member &&
(get_end.is_null() || saved_form->*date_member < get_end)) {
forms->push_back(saved_form);
saved_form = nullptr;
}
}
return true;
}
bool NativeBackendLibsecret::RemoveLoginsBetween(
base::Time get_begin,
base::Time get_end,
TimestampToCompare date_to_compare,
password_manager::PasswordStoreChangeList* changes) {
DCHECK(changes);
changes->clear();
ScopedVector<autofill::PasswordForm> forms;
if (!GetLoginsBetween(get_begin, get_end, date_to_compare, &forms))
return false;
bool ok = true;
for (size_t i = 0; i < forms.size(); ++i) {
if (RemoveLogin(*forms[i])) {
changes->push_back(password_manager::PasswordStoreChange(
password_manager::PasswordStoreChange::REMOVE, *forms[i]));
} else {
ok = false;
}
}
return ok;
}
ScopedVector<autofill::PasswordForm> NativeBackendLibsecret::ConvertFormList(
GList* found,
const PasswordForm* lookup_form) {
ScopedVector<autofill::PasswordForm> forms;
password_manager::PSLDomainMatchMetric psl_domain_match_metric =
password_manager::PSL_DOMAIN_MATCH_NONE;
GError* error = nullptr;
for (GList* element = g_list_first(found); element != nullptr;
element = g_list_next(element)) {
SecretItem* secretItem = static_cast<SecretItem*>(element->data);
LibsecretLoader::secret_item_load_secret_sync(secretItem, nullptr, &error);
if (error) {
VLOG(1) << "Unable to load secret item" << error->message;
g_error_free(error);
error = nullptr;
continue;
}
GHashTable* attrs = secret_item_get_attributes(secretItem);
scoped_ptr<PasswordForm> form(FormOutOfAttributes(attrs));
g_hash_table_unref(attrs);
if (form) {
if (lookup_form && form->signon_realm != lookup_form->signon_realm) {
// This is not an exact match, we try PSL matching.
if (lookup_form->scheme != PasswordForm::SCHEME_HTML ||
form->scheme != PasswordForm::SCHEME_HTML ||
!(password_manager::IsPublicSuffixDomainMatch(
lookup_form->signon_realm, form->signon_realm))) {
continue;
}
psl_domain_match_metric = password_manager::PSL_DOMAIN_MATCH_FOUND;
form->original_signon_realm = form->signon_realm;
form->signon_realm = lookup_form->signon_realm;
form->origin = lookup_form->origin;
}
SecretValue* secretValue = secret_item_get_secret(secretItem);
if (secretValue) {
form->password_value = UTF8ToUTF16(secret_value_get_text(secretValue));
secret_value_unref(secretValue);
} else {
VLOG(1) << "Unable to access password from list element!";
}
forms.push_back(form.Pass());
} else {
VLOG(1) << "Could not initialize PasswordForm from attributes!";
}
}
if (lookup_form) {
const GURL signon_realm(lookup_form->signon_realm);
std::string registered_domain =
password_manager::GetRegistryControlledDomain(signon_realm);
UMA_HISTOGRAM_ENUMERATION(
"PasswordManager.PslDomainMatchTriggering",
password_manager::ShouldPSLDomainMatchingApply(registered_domain)
? psl_domain_match_metric
: password_manager::PSL_DOMAIN_MATCH_NOT_USED,
password_manager::PSL_DOMAIN_MATCH_COUNT);
}
g_list_free(found);
return forms.Pass();
}