0

Add SecretPortalKeyProvider

This change implements an OSCrypt async client using the Freedesktop
Secret Service interface.  Both gnome-keyring and KWallet expose this
interface, so we may eventually remove both of those synchronous
backends.

Change-Id: I2462128bf4416e249044e73336961641929dde05
Bug: 40086962
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5792413
Reviewed-by: Will Harris <wfh@chromium.org>
Reviewed-by: Ryo Hashimoto <hashimoto@chromium.org>
Commit-Queue: Thomas Anderson <thomasanderson@chromium.org>
Reviewed-by: Lei Zhang <thestig@chromium.org>
Reviewed-by: Luc Nguyen <lucnguyen@google.com>
Code-Coverage: findit-for-me@appspot.gserviceaccount.com <findit-for-me@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/heads/main@{#1354558}
This commit is contained in:
Tom Anderson
2024-09-12 15:26:50 +00:00
committed by Chromium LUCI CQ
parent dbb4fd229e
commit 64808f96cd
17 changed files with 1056 additions and 7 deletions

@ -6901,7 +6901,10 @@ static_library("browser") {
"dbus_memory_pressure_evaluator_linux.h",
]
deps += [ "//chrome/common:chrome_features" ]
deps += [
"//chrome/common:chrome_features",
"//components/os_crypt/async/browser:secret_portal_key_provider",
]
}
if (is_linux || is_chromeos_lacros) {

@ -71,6 +71,15 @@ BASE_FEATURE(kClosedTabCache,
"ClosedTabCache",
base::FEATURE_DISABLED_BY_DEFAULT);
#if BUILDFLAG(IS_LINUX)
// Enables usage of os_crypt_async::SecretPortalKeyProvider. Once
// `kSecretPortalKeyProviderUseForEncryption` is enabled, this flag cannot be
// disabled without losing data.
BASE_FEATURE(kDbusSecretPortal,
"DbusSecretPortal",
base::FEATURE_DISABLED_BY_DEFAULT);
#endif // BUILDFLAG(IS_LINUX)
// Destroy profiles when their last browser window is closed, instead of when
// the browser exits.
// On Lacros the feature is enabled only for secondary profiles, check the
@ -338,6 +347,14 @@ BASE_FEATURE(kSandboxExternalProtocolBlockedWarning,
"SandboxExternalProtocolBlockedWarning",
base::FEATURE_ENABLED_BY_DEFAULT);
#if BUILDFLAG(IS_LINUX)
// If true, encrypt new data with the key provided by SecretPortalKeyProvider.
// Otherwise, it will only decrypt existing data.
BASE_FEATURE(kSecretPortalKeyProviderUseForEncryption,
"SecretPortalKeyProviderUseForEncryption",
base::FEATURE_DISABLED_BY_DEFAULT);
#endif // BUILDFLAG(IS_LINUX)
// This flag controls whether to trigger prerendering when the default search
// engine suggests to prerender a search result.
BASE_FEATURE(kSupportSearchSuggestionForPrerender2,

@ -29,6 +29,10 @@ BASE_DECLARE_FEATURE(kCertificateTransparencyAskBeforeEnabling);
BASE_DECLARE_FEATURE(kCertVerificationNetworkTime);
BASE_DECLARE_FEATURE(kClosedTabCache);
#if BUILDFLAG(IS_LINUX)
BASE_DECLARE_FEATURE(kDbusSecretPortal);
#endif
BASE_DECLARE_FEATURE(kDestroyProfileOnBrowserClose);
BASE_DECLARE_FEATURE(kDestroySystemProfiles);
@ -138,6 +142,11 @@ BASE_DECLARE_FEATURE(kRegisterOsUpdateHandlerWin);
BASE_DECLARE_FEATURE(kRestartNetworkServiceUnsandboxedForFailedLaunch);
BASE_DECLARE_FEATURE(kSandboxExternalProtocolBlocked);
BASE_DECLARE_FEATURE(kSandboxExternalProtocolBlockedWarning);
#if BUILDFLAG(IS_LINUX)
BASE_DECLARE_FEATURE(kSecretPortalKeyProviderUseForEncryption);
#endif
BASE_DECLARE_FEATURE(kSupportSearchSuggestionForPrerender2);
#if !BUILDFLAG(IS_ANDROID)

@ -256,6 +256,11 @@
#include "components/enterprise/browser/controller/chrome_browser_cloud_management_controller.h"
#endif
#if BUILDFLAG(IS_LINUX)
#include "chrome/browser/browser_features.h"
#include "components/os_crypt/async/browser/secret_portal_key_provider.h"
#endif
#if BUILDFLAG(IS_WIN) || (BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS_LACROS))
// How often to check if the persistent instance of Chrome needs to restart
// to install an update.
@ -1118,6 +1123,10 @@ void BrowserProcessImpl::RegisterPrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(metrics::prefs::kMetricsReportingEnabled,
GoogleUpdateSettings::GetCollectStatsConsent());
registry->RegisterBooleanPref(prefs::kDevToolsRemoteDebuggingAllowed, true);
#if BUILDFLAG(IS_LINUX)
os_crypt_async::SecretPortalKeyProvider::RegisterLocalPrefs(registry);
#endif
}
GlobalFeatures* BrowserProcessImpl::GetFeatures() {
@ -1395,6 +1404,17 @@ void BrowserProcessImpl::PreMainMessageLoopRun() {
}
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
if (base::FeatureList::IsEnabled(features::kDbusSecretPortal)) {
providers.emplace_back(
/*precedence=*/10u,
std::make_unique<os_crypt_async::SecretPortalKeyProvider>(
local_state(),
base::FeatureList::IsEnabled(
features::kSecretPortalKeyProviderUseForEncryption)));
}
#endif // BUILDFLAG(IS_LINUX)
os_crypt_async_ =
std::make_unique<os_crypt_async::OSCryptAsync>(std::move(providers));

@ -35,6 +35,13 @@
#include "chrome/install_static/test/scoped_install_details.h"
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
#include "chrome/browser/chrome_browser_main.h"
#include "chrome/browser/chrome_browser_main_extra_parts.h"
#include "components/os_crypt/async/browser/secret_portal_key_provider.h"
#include "components/os_crypt/async/browser/test_secret_portal.h"
#endif // BUILDFLAG(IS_LINUX)
namespace {
enum TestConfiguration {
@ -68,6 +75,10 @@ enum TestConfiguration {
// have been data encrypted with this key before the policy was disabled.
kOSCryptAsyncWithAppBoundProviderDisabledByPolicy,
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
// The Secret Portal key provider is being registered with Chrome.
kOSCryptAsyncWithSecretPortalProvider,
#endif // BUILDFLAG(IS_LINUX)
};
enum MetricsExpectation {
@ -77,6 +88,9 @@ enum MetricsExpectation {
kAppBoundEncryptMetrics,
kAppBoundDecryptMetrics,
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
kSecretPortalMetrics,
#endif // BUILDFLAG(IS_LINUX)
kNoMetrics,
};
@ -122,6 +136,38 @@ class CookieEncryptionProviderBrowserTest
std::make_unique<os_crypt::FakeInstallDetails>()) {}
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
void CreatedBrowserMainParts(content::BrowserMainParts* parts) override {
InProcessBrowserTest::CreatedBrowserMainParts(parts);
base::OnceClosure initialize_server = base::BindOnce(
[](CookieEncryptionProviderBrowserTest* self) {
self->test_secret_portal_ =
std::make_unique<os_crypt_async::TestSecretPortal>(
content::IsPreTest());
os_crypt_async::SecretPortalKeyProvider::
GetSecretServiceNameForTest() =
self->test_secret_portal_->BusName();
},
base::Unretained(this));
class PostCreateThreadsObserver : public ChromeBrowserMainExtraParts {
public:
explicit PostCreateThreadsObserver(base::OnceClosure post_create_threads)
: post_create_threads_(std::move(post_create_threads)) {}
void PostCreateThreads() override {
std::move(post_create_threads_).Run();
}
private:
base::OnceClosure post_create_threads_;
};
static_cast<ChromeBrowserMainParts*>(parts)->AddParts(
std::make_unique<PostCreateThreadsObserver>(
std::move(initialize_server)));
}
#endif // BUILDFLAG(IS_LINUX)
void SetUp() override {
#if BUILDFLAG(IS_WIN)
if ((IsElevationRequired(GetParam().before) ||
@ -149,6 +195,9 @@ class CookieEncryptionProviderBrowserTest
disabled_features.push_back(
features::kRegisterAppBoundEncryptionProvider);
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
disabled_features.push_back(features::kDbusSecretPortal);
#endif // BUILDFLAG(IS_LINUX)
break;
#if BUILDFLAG(IS_WIN)
case kOSCryptAsyncWithAppBoundProvider:
@ -214,6 +263,15 @@ class CookieEncryptionProviderBrowserTest
&policy_provider_);
break;
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
case kOSCryptAsyncWithSecretPortalProvider:
enabled_features.push_back(
features::kUseOsCryptAsyncForCookieEncryption);
enabled_features.push_back(features::kDbusSecretPortal);
enabled_features.push_back(
features::kSecretPortalKeyProviderUseForEncryption);
break;
#endif // BUILDFLAG(IS_LINUX)
}
scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
@ -264,6 +322,15 @@ class CookieEncryptionProviderBrowserTest
"OSCrypt.AppBoundProvider.Encrypt.ResultCode", 0);
break;
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
case kSecretPortalMetrics:
histogram_tester_.ExpectBucketCount(
os_crypt_async::SecretPortalKeyProvider::kUmaInitStatusEnum,
os_crypt_async::SecretPortalKeyProvider::InitStatus::kSuccess, 1);
histogram_tester_.ExpectTotalCount(
os_crypt_async::SecretPortalKeyProvider::kUmaNewInitFailureEnum, 0);
break;
#endif // BUILDFLAG(IS_LINUX)
case kNoMetrics:
histogram_tester_.ExpectTotalCount(
"OSCrypt.AppBoundProvider.Decrypt.ResultCode", 0);
@ -285,6 +352,9 @@ class CookieEncryptionProviderBrowserTest
std::optional<base::ScopedClosureRunner> maybe_uninstall_service_;
testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
std::unique_ptr<os_crypt_async::TestSecretPortal> test_secret_portal_;
#endif // BUILDFLAG(IS_LINUX)
};
IN_PROC_BROWSER_TEST_P(CookieEncryptionProviderBrowserTest, PRE_CookieStorage) {
@ -415,5 +485,12 @@ INSTANTIATE_TEST_SUITE_P(
.metrics_expectation_before = kAppBoundEncryptMetrics,
.metrics_expectation_after = kAppBoundDecryptMetrics},
#endif // BUILDFLAG(IS_WIN)
#if BUILDFLAG(IS_LINUX)
{.name = "secret_portal",
.before = kOSCryptAsyncWithSecretPortalProvider,
.after = kOSCryptAsyncWithSecretPortalProvider,
.metrics_expectation_before = kSecretPortalMetrics,
.metrics_expectation_after = kSecretPortalMetrics},
#endif // BUILDFLAG(IS_LINUX)
}),
[](const auto& info) { return info.param.name; });

@ -11691,7 +11691,11 @@ if (!is_android && !is_chromeos_device) {
if (ozone_platform_x11 && enable_remoting) {
sources += [ "//ui/display/linux/test/virtual_display_util_linux_interactive_uitest.cc" ]
}
deps += [ "//ui/linux:linux_ui" ]
deps += [
"//components/os_crypt/async/browser:secret_portal_key_provider",
"//components/os_crypt/async/browser:test_secret_portal",
"//ui/linux:linux_ui",
]
}
if (is_linux || is_chromeos_lacros) {

@ -56,6 +56,48 @@ if (is_win) {
}
}
if (is_linux) {
source_set("secret_portal_key_provider") {
sources = [
"secret_portal_key_provider.cc",
"secret_portal_key_provider.h",
]
deps = [
":key_provider_interface",
"//base",
"//components/dbus/properties",
"//components/dbus/thread_linux",
"//components/dbus/utils",
"//components/os_crypt/async/common",
"//components/os_crypt/sync",
"//components/prefs",
"//crypto",
"//dbus",
]
}
source_set("test_secret_portal") {
testonly = true
sources = [
"test_secret_portal.cc",
"test_secret_portal.h",
]
deps = [
":key_provider_interface",
":secret_portal_key_provider",
"//base",
"//components/dbus/properties",
"//components/dbus/utils",
"//components/os_crypt/async/common",
"//dbus",
"//testing/gtest",
]
}
}
source_set("unit_tests") {
testonly = true
sources = [ "os_crypt_async_unittest.cc" ]
@ -79,4 +121,16 @@ source_set("unit_tests") {
"//components/prefs:test_support",
]
}
if (is_linux) {
sources += [ "secret_portal_key_provider_unittest.cc" ]
deps += [
":secret_portal_key_provider",
"//components/dbus/properties",
"//components/dbus/utils",
"//components/prefs:test_support",
"//dbus:test_support",
"//testing/gmock",
]
}
}

@ -1,13 +1,12 @@
include_rules = [
"+components/dbus",
"+components/prefs",
"+crypto",
"+dbus",
]
specific_include_rules = {
".*_unittest\.cc": [
"+components/os_crypt/sync",
"+crypto",
],
"test_utils\.cc": [
"+crypto",
],
}

@ -0,0 +1,334 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/os_crypt/async/browser/secret_portal_key_provider.h"
#include <fcntl.h>
#include <linux/limits.h>
#include <array>
#include <utility>
#include "base/environment.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/nix/xdg_util.h"
#include "base/no_destructor.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/current_thread.h"
#include "components/dbus/properties/types.h"
#include "components/dbus/thread_linux/dbus_thread_linux.h"
#include "components/dbus/utils/name_has_owner.h"
#include "components/os_crypt/async/common/algorithm.mojom.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "crypto/hkdf.h"
#include "dbus/bus.h"
#include "dbus/message.h"
#include "dbus/object_proxy.h"
namespace os_crypt_async {
namespace {
constexpr char kSaltForHkdf[] = "fdo_portal_secret_salt";
constexpr char kInfoForHkdf[] = "HKDF-SHA-256 AES-256-GCM";
scoped_refptr<dbus::Bus> CreateBus() {
dbus::Bus::Options options;
options.bus_type = dbus::Bus::SESSION;
options.connection_type = dbus::Bus::PRIVATE;
options.dbus_task_runner = dbus_thread_linux::GetTaskRunner();
return base::MakeRefCounted<dbus::Bus>(options);
}
} // namespace
// static
void SecretPortalKeyProvider::RegisterLocalPrefs(PrefRegistrySimple* registry) {
registry->RegisterStringPref(kOsCryptTokenPrefName, "");
registry->RegisterStringPref(kOsCryptPrevDesktopPrefName, "");
registry->RegisterBooleanPref(kOsCryptPrevInitSuccessPrefName, false);
}
SecretPortalKeyProvider::SecretPortalKeyProvider(PrefService* local_state,
bool use_for_encryption)
: SecretPortalKeyProvider(local_state, CreateBus(), use_for_encryption) {}
SecretPortalKeyProvider::~SecretPortalKeyProvider() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
Finalize(InitStatus::kDestructedBeforeComplete);
bus_->GetDBusTaskRunner()->PostTask(
FROM_HERE, base::BindOnce(&dbus::Bus::ShutdownAndBlock, bus_));
}
SecretPortalKeyProvider::SecretPortalKeyProvider(PrefService* local_state,
scoped_refptr<dbus::Bus> bus,
bool use_for_encryption)
: local_state_(local_state),
use_for_encryption_(use_for_encryption),
bus_(bus) {}
void SecretPortalKeyProvider::GetKey(KeyCallback callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK(!key_callback_);
CHECK(callback);
key_callback_ = std::move(callback);
dbus_utils::NameHasOwner(
bus_.get(), GetSecretServiceName(),
base::BindOnce(&SecretPortalKeyProvider::OnNameHasOwnerResponse,
weak_ptr_factory_.GetWeakPtr()));
}
bool SecretPortalKeyProvider::UseForEncryption() {
return use_for_encryption_;
}
bool SecretPortalKeyProvider::IsCompatibleWithOsCryptSync() {
return false;
}
void SecretPortalKeyProvider::OnNameHasOwnerResponse(
std::optional<bool> name_has_owner) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!name_has_owner.value_or(false)) {
return Finalize(InitStatus::kNoService);
}
// Create a pipe to retrieve the secret.
int fds[2];
if (pipe2(fds, O_CLOEXEC) != 0) {
LOG(ERROR) << "Failed to create pipe for secret retrieval.";
return Finalize(InitStatus::kPipeFailed);
}
read_fd_ = base::ScopedFD(fds[0]);
base::ScopedFD write_fd(fds[1]);
dbus::MethodCall method_call(kInterfaceSecret, kMethodRetrieveSecret);
response_path_ =
std::make_unique<dbus::ObjectPath>(base::nix::XdgDesktopPortalRequestPath(
bus_->GetConnectionName(), kHandleToken));
auto* response_proxy =
bus_->GetObjectProxy(GetSecretServiceName(), *response_path_);
response_proxy->ConnectToSignal(
kInterfaceRequest, kSignalResponse,
base::BindRepeating(&SecretPortalKeyProvider::OnResponseSignal,
weak_ptr_factory_.GetWeakPtr()),
base::BindOnce(&SecretPortalKeyProvider::OnSignalConnected,
weak_ptr_factory_.GetWeakPtr()));
dbus::MessageWriter writer(&method_call);
writer.AppendFileDescriptor(write_fd.get() /* the FD gets duplicated */);
DbusDictionary options;
if (local_state_->HasPrefPath(kOsCryptTokenPrefName)) {
const std::string token = local_state_->GetString(kOsCryptTokenPrefName);
if (!token.empty()) {
options.Put("token", MakeDbusVariant(DbusString(token)));
}
}
options.Put("handle_token", MakeDbusVariant(DbusString(kHandleToken)));
options.Write(&writer);
auto* secret_proxy = bus_->GetObjectProxy(
GetSecretServiceName(), dbus::ObjectPath(kObjectPathSecret));
secret_proxy->CallMethod(
&method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
base::BindOnce(&SecretPortalKeyProvider::OnRetrieveSecretResponse,
weak_ptr_factory_.GetWeakPtr()));
}
void SecretPortalKeyProvider::OnRetrieveSecretResponse(
dbus::Response* response) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!response) {
LOG(ERROR) << "Failed to retrieve secret: No response from portal.";
return Finalize(InitStatus::kNoResponse);
}
dbus::MessageReader reader(response);
// Read the object path of the response handle.
dbus::ObjectPath response_path;
if (!reader.PopObjectPath(&response_path)) {
LOG(ERROR) << "Failed to retrieve secret: Invalid response format.";
return Finalize(InitStatus::kInvalidResponseFormat);
}
CHECK(response_path_);
const bool matches = response_path == *response_path_;
response_path_.reset();
if (!matches) {
LOG(ERROR) << "Response path does not match.";
return Finalize(InitStatus::kResponsePathMismatch);
}
// Read the secret from the pipe. This must happen asynchronously because the
// file may not become readable until the keyring is unlocked by typing a
// password.
read_watcher_ = base::FileDescriptorWatcher::WatchReadable(
read_fd_.get(),
base::BindRepeating(&SecretPortalKeyProvider::OnFdReadable,
weak_ptr_factory_.GetWeakPtr()));
}
void SecretPortalKeyProvider::OnSignalConnected(
const std::string& interface_name,
const std::string& signal_name,
bool connected) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!connected) {
LOG(ERROR) << "Failed to connect to " << interface_name << "."
<< signal_name;
return Finalize(InitStatus::kSignalConnectFailed);
}
}
void SecretPortalKeyProvider::OnResponseSignal(dbus::Signal* signal) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
dbus::MessageReader reader(signal);
uint32_t response;
if (!reader.PopUint32(&response)) {
LOG(ERROR) << "Failed to read response from signal.";
return Finalize(InitStatus::kSignalReadFailed);
}
if (response != 0) {
LOG(ERROR) << "Keyring unlock cancelled: " << response;
return Finalize(InitStatus::kUserCancelledUnlock);
}
dbus::MessageReader dict_reader(nullptr);
if (!reader.PopArray(&dict_reader)) {
LOG(ERROR) << "Failed to read array.";
return Finalize(InitStatus::kSignalParseFailed);
}
bool got_token = false;
while (dict_reader.HasMoreData()) {
dbus::MessageReader dict_entry_reader(nullptr);
if (!dict_reader.PopDictEntry(&dict_entry_reader)) {
LOG(ERROR) << "Failed to read dict entry.";
return Finalize(InitStatus::kSignalParseFailed);
}
std::string key;
if (!dict_entry_reader.PopString(&key)) {
LOG(ERROR) << "Failed to read key.";
return Finalize(InitStatus::kSignalParseFailed);
}
if (key == "token") {
std::string value;
if (!dict_entry_reader.PopVariantOfString(&value)) {
LOG(ERROR) << "Failed to read value.";
return Finalize(InitStatus::kSignalParseFailed);
}
local_state_->SetString(kOsCryptTokenPrefName, value);
got_token = true;
}
}
// TODO(https://crbug.com/40086962): Investigate if a token should be
// required to continue.
base::UmaHistogramBoolean(kUmaGotTokenBoolean, got_token);
}
void SecretPortalKeyProvider::OnFdReadable() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::array<uint8_t, PIPE_BUF> buffer;
ssize_t bytes_read = read(read_fd_.get(), buffer.data(), buffer.size());
if (bytes_read < 0) {
LOG(ERROR) << "Failed to read secret from file descriptor.";
return Finalize(InitStatus::kPipeReadFailed);
}
if (bytes_read > 0) {
auto buffer_span =
base::span(buffer).subspan(0u, base::checked_cast<size_t>(bytes_read));
secret_.insert(secret_.end(), buffer_span.begin(), buffer_span.end());
return;
}
// EOF.
read_watcher_.reset();
read_fd_.reset();
ReceivedSecret();
}
void SecretPortalKeyProvider::ReceivedSecret() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (secret_.empty()) {
LOG(ERROR) << "Retrieved secret is empty.";
return Finalize(InitStatus::kEmptySecret);
}
auto hashed = crypto::HkdfSha256(
base::span(secret_), base::as_byte_span(kSaltForHkdf),
base::as_byte_span(kInfoForHkdf), Encryptor::Key::kAES256GCMKeySize);
secret_.clear();
Encryptor::Key derived_key(hashed, mojom::Algorithm::kAES256GCM);
Finalize(InitStatus::kSuccess, kKeyTag, std::move(derived_key));
}
void SecretPortalKeyProvider::Finalize(InitStatus init_status) {
CHECK_NE(init_status, InitStatus::kSuccess);
Finalize(init_status, std::string(), std::nullopt);
}
void SecretPortalKeyProvider::Finalize(InitStatus init_status,
const std::string& tag,
std::optional<Encryptor::Key> key) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
CHECK_EQ(init_status == InitStatus::kSuccess, key.has_value());
if (!key_callback_) {
// Already finalized.
return;
}
std::move(key_callback_).Run(tag, std::move(key));
response_path_.reset();
read_watcher_.reset();
read_fd_.reset();
secret_.clear();
base::UmaHistogramEnumeration(kUmaInitStatusEnum, init_status);
std::string desktop;
base::Environment::Create()->GetVar(base::nix::kXdgCurrentDesktopEnvVar,
&desktop);
const bool success = init_status == InitStatus::kSuccess;
if (local_state_->HasPrefPath(kOsCryptPrevDesktopPrefName)) {
const std::string prev_desktop =
local_state_->GetString(kOsCryptPrevDesktopPrefName);
if (desktop == prev_desktop) {
bool prev_init_success =
local_state_->GetBoolean(kOsCryptPrevInitSuccessPrefName);
if (prev_init_success && !success) {
base::UmaHistogramEnumeration(kUmaNewInitFailureEnum, init_status);
}
}
}
local_state_->SetString(kOsCryptPrevDesktopPrefName, desktop);
local_state_->SetBoolean(kOsCryptPrevInitSuccessPrefName, success);
}
// static
std::string SecretPortalKeyProvider::GetSecretServiceName() {
return GetSecretServiceNameForTest().value_or(kServiceSecret);
}
// static
std::optional<std::string>&
SecretPortalKeyProvider::GetSecretServiceNameForTest() {
static base::NoDestructor<std::optional<std::string>>
secret_service_name_for_test;
return *secret_service_name_for_test;
}
} // namespace os_crypt_async

@ -0,0 +1,151 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_OS_CRYPT_ASYNC_BROWSER_SECRET_PORTAL_KEY_PROVIDER_H_
#define COMPONENTS_OS_CRYPT_ASYNC_BROWSER_SECRET_PORTAL_KEY_PROVIDER_H_
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/files/file_descriptor_watcher_posix.h"
#include "base/files/scoped_file.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/sequence_checker.h"
#include "components/os_crypt/async/browser/key_provider.h"
class CookieEncryptionProviderBrowserTest;
class PrefRegistrySimple;
class PrefService;
namespace dbus {
class Bus;
class ObjectPath;
class Response;
class Signal;
} // namespace dbus
namespace os_crypt_async {
class SecretPortalKeyProviderTest;
// The SecretPortalKeyProvider uses the org.freedesktop.portal.Secret interface
// to retrieve an application-specific secret from the portal, which can then
// be used to encrypt confidential data. This is the only interface exposed
// from sandboxed environments like Flatpak.
class SecretPortalKeyProvider : public KeyProvider {
public:
enum class InitStatus {
// Values are logged in metrics, so should not be changed.
kSuccess = 0,
kNoService = 1,
kPipeFailed = 2,
kInvalidResponseFormat = 3,
kResponsePathMismatch = 4,
kPipeReadFailed = 5,
kEmptySecret = 6,
kNoResponse = 7,
kSignalReadFailed = 8,
kUserCancelledUnlock = 9,
kDestructedBeforeComplete = 10,
kSignalConnectFailed = 11,
kSignalParseFailed = 12,
kMaxValue = kSignalParseFailed,
};
static void RegisterLocalPrefs(PrefRegistrySimple* registry);
SecretPortalKeyProvider(PrefService* local_state, bool use_for_encryption);
~SecretPortalKeyProvider() override;
private:
friend class ::CookieEncryptionProviderBrowserTest;
friend class SecretPortalKeyProviderTest;
friend class TestSecretPortal;
FRIEND_TEST_ALL_PREFIXES(SecretPortalKeyProviderTest, GetKey);
// DBus constants. Exposed in header for tests.
static constexpr char kServiceSecret[] = "org.freedesktop.portal.Desktop";
static constexpr char kObjectPathSecret[] = "/org/freedesktop/portal/desktop";
static constexpr char kInterfaceSecret[] = "org.freedesktop.portal.Secret";
static constexpr char kInterfaceRequest[] = "org.freedesktop.portal.Request";
static constexpr char kMethodRetrieveSecret[] = "RetrieveSecret";
static constexpr char kSignalResponse[] = "Response";
static constexpr char kOsCryptTokenPrefName[] = "os_crypt.portal.token";
static constexpr char kOsCryptPrevDesktopPrefName[] =
"os_crypt.portal.prev_desktop";
static constexpr char kOsCryptPrevInitSuccessPrefName[] =
"os_crypt.portal.prev_init_success";
static constexpr char kHandleToken[] = "cr_secret_portal_response_token";
static constexpr char kKeyTag[] = "v12";
static constexpr char kUmaInitStatusEnum[] =
"OSCrypt.SecretPortalKeyProvider.InitStatus";
static constexpr char kUmaNewInitFailureEnum[] =
"OSCrypt.SecretPortalKeyProvider.NewInitFailure";
static constexpr char kUmaGotTokenBoolean[] =
"OSCrypt.SecretPortalKeyProvider.GotToken";
SecretPortalKeyProvider(PrefService* local_state,
scoped_refptr<dbus::Bus> bus,
bool use_for_encryption);
// KeyProvider:
void GetKey(KeyCallback callback) override;
bool UseForEncryption() override;
bool IsCompatibleWithOsCryptSync() override;
void OnNameHasOwnerResponse(std::optional<bool> name_has_owner);
void OnRetrieveSecretResponse(dbus::Response* response);
void OnSignalConnected(const std::string& interface_name,
const std::string& signal_name,
bool connected);
void OnResponseSignal(dbus::Signal* signal);
void OnFdReadable();
void ReceivedSecret();
// Finalize with an empty tag and key for error cases.
void Finalize(InitStatus init_status);
void Finalize(InitStatus init_status,
const std::string& tag,
std::optional<Encryptor::Key> key);
static std::string GetSecretServiceName();
static std::optional<std::string>& GetSecretServiceNameForTest();
const raw_ptr<PrefService> local_state_;
const bool use_for_encryption_;
scoped_refptr<dbus::Bus> bus_;
KeyCallback key_callback_;
std::unique_ptr<dbus::ObjectPath> response_path_;
base::ScopedFD read_fd_;
std::unique_ptr<base::FileDescriptorWatcher::Controller> read_watcher_;
std::vector<uint8_t> secret_;
SEQUENCE_CHECKER(sequence_checker_);
base::WeakPtrFactory<SecretPortalKeyProvider> weak_ptr_factory_{this};
};
} // namespace os_crypt_async
#endif // COMPONENTS_OS_CRYPT_ASYNC_BROWSER_SECRET_PORTAL_KEY_PROVIDER_H_

@ -0,0 +1,191 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/os_crypt/async/browser/secret_portal_key_provider.h"
#include "base/memory/scoped_refptr.h"
#include "base/nix/xdg_util.h"
#include "base/run_loop.h"
#include "base/test/task_environment.h"
#include "components/dbus/properties/types.h"
#include "components/dbus/utils/name_has_owner.h"
#include "components/prefs/testing_pref_service.h"
#include "dbus/message.h"
#include "dbus/mock_bus.h"
#include "dbus/mock_object_proxy.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
using ::testing::_;
using ::testing::Invoke;
using ::testing::Return;
namespace os_crypt_async {
namespace {
constexpr char kBusName[] = ":1.123";
constexpr char kSecret[] = "secret_for_testing";
constexpr char kPrefToken[] = "token_for_testing";
MATCHER_P2(MatchMethod, interface, member, "") {
return arg->GetInterface() == interface && arg->GetMember() == member;
}
} // namespace
class SecretPortalKeyProviderTest : public testing::Test {
public:
SecretPortalKeyProviderTest() = default;
void SetUp() override {
SecretPortalKeyProvider::RegisterLocalPrefs(pref_service_.registry());
mock_bus_ = base::MakeRefCounted<dbus::MockBus>(dbus::Bus::Options());
mock_dbus_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
mock_bus_.get(), DBUS_SERVICE_DBUS, dbus::ObjectPath(DBUS_PATH_DBUS));
EXPECT_CALL(*mock_bus_, GetObjectProxy(DBUS_SERVICE_DBUS,
dbus::ObjectPath(DBUS_PATH_DBUS)))
.WillRepeatedly(Return(mock_dbus_proxy_.get()));
response_path_ = dbus::ObjectPath(base::nix::XdgDesktopPortalRequestPath(
kBusName, SecretPortalKeyProvider::kHandleToken));
mock_response_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
mock_bus_.get(), SecretPortalKeyProvider::kServiceSecret,
response_path_);
EXPECT_CALL(
*mock_bus_,
GetObjectProxy(SecretPortalKeyProvider::kServiceSecret, response_path_))
.WillRepeatedly(Return(mock_response_proxy_.get()));
mock_secret_proxy_ = base::MakeRefCounted<dbus::MockObjectProxy>(
mock_bus_.get(), SecretPortalKeyProvider::kServiceSecret,
dbus::ObjectPath(SecretPortalKeyProvider::kObjectPathSecret));
EXPECT_CALL(*mock_bus_,
GetObjectProxy(SecretPortalKeyProvider::kServiceSecret,
dbus::ObjectPath(
SecretPortalKeyProvider::kObjectPathSecret)))
.WillRepeatedly(Return(mock_secret_proxy_.get()));
key_provider_ = base::WrapUnique(
new SecretPortalKeyProvider(&pref_service_, mock_bus_, true));
}
void TearDown() override {
EXPECT_CALL(*mock_bus_, GetDBusTaskRunner())
.WillRepeatedly(
Return(task_environment_.GetMainThreadTaskRunner().get()));
EXPECT_CALL(*mock_bus_, ShutdownAndBlock());
// Shutdown the bus to ensure clean-up
key_provider_.reset();
base::RunLoop run_loop;
run_loop.RunUntilIdle();
}
protected:
// An IO thread is required for FileDescriptorWatcher.
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::MainThreadType::IO};
scoped_refptr<dbus::MockBus> mock_bus_;
scoped_refptr<dbus::MockObjectProxy> mock_dbus_proxy_;
scoped_refptr<dbus::MockObjectProxy> mock_response_proxy_;
scoped_refptr<dbus::MockObjectProxy> mock_secret_proxy_;
dbus::ObjectPath response_path_;
std::unique_ptr<SecretPortalKeyProvider> key_provider_;
TestingPrefServiceSimple pref_service_;
};
TEST_F(SecretPortalKeyProviderTest, GetKey) {
EXPECT_CALL(
*mock_dbus_proxy_,
DoCallMethod(MatchMethod(DBUS_INTERFACE_DBUS, "NameHasOwner"), _, _))
.WillOnce(Invoke([](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback* callback) {
dbus::MessageReader reader(method_call);
std::string service_name;
EXPECT_TRUE(reader.PopString(&service_name));
EXPECT_EQ(service_name, SecretPortalKeyProvider::kServiceSecret);
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendBool(true);
std::move(*callback).Run(response.get());
}));
EXPECT_CALL(*mock_bus_, GetConnectionName()).WillOnce(Return(kBusName));
EXPECT_CALL(*mock_response_proxy_, DoConnectToSignal(_, _, _, _))
.WillOnce(Invoke(
[&](const std::string& interface_name, const std::string& signal_name,
dbus::ObjectProxy::SignalCallback signal_callback,
dbus::ObjectProxy::OnConnectedCallback* on_connected_callback) {
EXPECT_EQ(interface_name,
SecretPortalKeyProvider::kInterfaceRequest);
EXPECT_EQ(signal_name, SecretPortalKeyProvider::kSignalResponse);
std::move(*on_connected_callback)
.Run(interface_name, signal_name, true);
dbus::Signal signal(interface_name, signal_name);
dbus::MessageWriter writer(&signal);
constexpr uint32_t kResponseSuccess = 0;
writer.AppendUint32(kResponseSuccess);
DbusDictionary dict;
dict.Put("token", MakeDbusVariant(DbusString(kPrefToken)));
dict.Write(&writer);
signal_callback.Run(&signal);
}));
EXPECT_CALL(
*mock_secret_proxy_,
DoCallMethod(MatchMethod(SecretPortalKeyProvider::kInterfaceSecret,
SecretPortalKeyProvider::kMethodRetrieveSecret),
_, _))
.WillOnce(Invoke([&](dbus::MethodCall* method_call, int timeout_ms,
dbus::ObjectProxy::ResponseCallback* callback) {
dbus::MessageReader reader(method_call);
base::ScopedFD write_fd;
EXPECT_TRUE(reader.PopFileDescriptor(&write_fd));
EXPECT_EQ(write(write_fd.get(), kSecret, sizeof(kSecret)),
static_cast<ssize_t>(sizeof(kSecret)));
write_fd.reset();
auto response = dbus::Response::CreateEmpty();
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(dbus::ObjectPath(response_path_));
std::move(*callback).Run(response.get());
}));
bool callback_called = false;
std::string key_tag;
std::optional<Encryptor::Key> key;
key_provider_->GetKey(base::BindOnce(
[](bool& callback_called, std::string& key_tag,
std::optional<Encryptor::Key>& key,
const std::string& returned_key_tag,
std::optional<Encryptor::Key> returned_key) {
callback_called = true;
key_tag = returned_key_tag;
key = std::move(returned_key);
},
std::ref(callback_called), std::ref(key_tag), std::ref(key)));
base::RunLoop run_loop;
run_loop.RunUntilIdle();
EXPECT_TRUE(callback_called);
EXPECT_EQ(key_tag, SecretPortalKeyProvider::kKeyTag);
EXPECT_TRUE(key.has_value());
EXPECT_TRUE(pref_service_.HasPrefPath(
SecretPortalKeyProvider::kOsCryptTokenPrefName));
EXPECT_EQ(
pref_service_.GetString(SecretPortalKeyProvider::kOsCryptTokenPrefName),
kPrefToken);
}
} // namespace os_crypt_async

@ -0,0 +1,93 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/os_crypt/async/browser/test_secret_portal.h"
#include "base/files/file_util.h"
#include "base/nix/xdg_util.h"
#include "components/dbus/properties/types.h"
#include "components/dbus/utils/name_has_owner.h"
#include "dbus/message.h"
#include "secret_portal_key_provider.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace os_crypt_async {
TestSecretPortal::TestSecretPortal(bool pre_test) : pre_test_(pre_test) {
dbus::Bus::Options bus_options;
bus_options.bus_type = dbus::Bus::SESSION;
bus_options.connection_type = dbus::Bus::PRIVATE;
bus_ = base::MakeRefCounted<dbus::Bus>(bus_options);
exported_object_ = bus_->GetExportedObject(
dbus::ObjectPath(SecretPortalKeyProvider::kObjectPathSecret));
EXPECT_TRUE(exported_object_->ExportMethodAndBlock(
SecretPortalKeyProvider::kInterfaceSecret,
SecretPortalKeyProvider::kMethodRetrieveSecret,
base::BindRepeating(&TestSecretPortal::RetrieveSecret,
weak_ptr_factory_.GetWeakPtr())));
}
TestSecretPortal::~TestSecretPortal() {
exported_object_ = nullptr;
bus_->ShutdownAndBlock();
}
std::string TestSecretPortal::BusName() const {
return bus_->GetConnectionName();
}
void TestSecretPortal::RetrieveSecret(
dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender) {
dbus::MessageReader reader(method_call);
base::ScopedFD write_fd;
EXPECT_TRUE(reader.PopFileDescriptor(&write_fd));
base::WriteFileDescriptor(write_fd.get(), "secret");
write_fd.reset();
dbus::ObjectPath response_path;
dbus::MessageReader dict_reader(nullptr);
EXPECT_TRUE(reader.PopArray(&dict_reader));
std::optional<std::string> token;
while (dict_reader.HasMoreData()) {
dbus::MessageReader dict_entry_reader(nullptr);
EXPECT_TRUE(dict_reader.PopDictEntry(&dict_entry_reader));
std::string key;
std::string value;
EXPECT_TRUE(dict_entry_reader.PopString(&key));
if (key == "handle_token") {
EXPECT_TRUE(dict_entry_reader.PopVariantOfString(&value));
auto bus_name = method_call->GetSender();
response_path = dbus::ObjectPath(
base::nix::XdgDesktopPortalRequestPath(bus_name, value));
}
if (key == "token") {
EXPECT_TRUE(dict_entry_reader.PopVariantOfString(&value));
EXPECT_EQ(value, "the_token");
token = value;
}
}
auto* exported_response = bus_->GetExportedObject(response_path);
EXPECT_EQ(pre_test_, !token.has_value());
auto response = dbus::Response::FromMethodCall(method_call);
dbus::MessageWriter writer(response.get());
writer.AppendObjectPath(response_path);
std::move(response_sender).Run(std::move(response));
dbus::Signal signal(SecretPortalKeyProvider::kInterfaceRequest,
SecretPortalKeyProvider::kSignalResponse);
dbus::MessageWriter signal_writer(&signal);
signal_writer.AppendUint32(0);
DbusDictionary dict;
dict.Put("token", MakeDbusVariant(DbusString("the_token")));
dict.Write(&signal_writer);
exported_response->SendSignal(&signal);
}
} // namespace os_crypt_async

@ -0,0 +1,46 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_OS_CRYPT_ASYNC_BROWSER_TEST_SECRET_PORTAL_H_
#define COMPONENTS_OS_CRYPT_ASYNC_BROWSER_TEST_SECRET_PORTAL_H_
#include <string>
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "dbus/bus.h"
#include "dbus/exported_object.h"
#include "dbus/message.h"
#include "dbus/object_path.h"
namespace os_crypt_async {
// This class implements the service side of the org.freedesktop.portal.Secret
// interface for testing purposes.
class TestSecretPortal {
public:
// `pre_test` indicates that this is like a browser first-run. If true, a new
// token is given. If false, a handle to an existing token is expected.
explicit TestSecretPortal(bool pre_test);
~TestSecretPortal();
std::string BusName() const;
private:
scoped_refptr<dbus::Bus> bus_;
raw_ptr<dbus::ExportedObject> exported_object_ = nullptr;
void RetrieveSecret(dbus::MethodCall* method_call,
dbus::ExportedObject::ResponseSender response_sender);
const bool pre_test_;
base::WeakPtrFactory<TestSecretPortal> weak_ptr_factory_{this};
};
} // namespace os_crypt_async
#endif // COMPONENTS_OS_CRYPT_ASYNC_BROWSER_TEST_SECRET_PORTAL_H_

@ -619,7 +619,7 @@ class CHROME_DBUS_EXPORT Bus : public base::RefCountedThreadSafe<Bus> {
// Return the unique name of the bus connection if it is connected to
// D-BUS. Otherwise, return an empty string.
std::string GetConnectionName();
virtual std::string GetConnectionName();
// Returns true if the bus is connected to D-Bus.
virtual bool IsConnected();

@ -75,6 +75,7 @@ class MockBus : public Bus {
MOCK_METHOD0(HasDBusThread, bool());
MOCK_METHOD0(AssertOnOriginThread, void());
MOCK_METHOD0(AssertOnDBusThread, void());
MOCK_METHOD0(GetConnectionName, std::string());
MOCK_METHOD0(IsConnected, bool());
protected:

@ -124,6 +124,22 @@ chromium-metrics-reviews@google.com.
<int value="3" label="Succeessfully got salient image url from local cache."/>
</enum>
<enum name="SecretPortalKeyProviderInitStatus">
<int value="0" label="Success"/>
<int value="1" label="NoService"/>
<int value="2" label="PipeFailed"/>
<int value="3" label="InvalidResponseFormat"/>
<int value="4" label="ResponsePathMismatch"/>
<int value="5" label="PipeReadFailed"/>
<int value="6" label="EmptySecret"/>
<int value="7" label="NoResponse"/>
<int value="8" label="SignalReadFailed"/>
<int value="9" label="UserCancelledUnlock"/>
<int value="10" label="DestructedBeforeComplete"/>
<int value="11" label="SignalConnectFailed"/>
<int value="12" label="SignalParseFailed"/>
</enum>
<enum name="SilentPushEvent">
<int value="0" label="New Silent Push request"/>
<int value="1" label="Notification enforcement skipped"/>

@ -6850,6 +6850,40 @@ chromium-metrics-reviews@google.com.
</summary>
</histogram>
<histogram name="OSCrypt.SecretPortalKeyProvider.GotToken" enum="Boolean"
expires_after="2025-07-28">
<owner>thomasanderson@chromium.org</owner>
<owner>thestig@chromium.org</owner>
<summary>
True if a token was received from the service. The token is passed back to
the service on subsequent usage to ensure the same secret will be returned.
Emitted only on Linux when the desktop portal has emitted the request
completed signal after user interaction is finished.
</summary>
</histogram>
<histogram name="OSCrypt.SecretPortalKeyProvider.InitStatus"
enum="SecretPortalKeyProviderInitStatus" expires_after="2025-07-28">
<owner>thomasanderson@chromium.org</owner>
<owner>thestig@chromium.org</owner>
<summary>
The result of the org.freedesktop.portal.Secret OSCrypt Async Key Provider
initialization. This is recorded once during browser startup if
SecretPortalKeyProvider enabled, on Linux only.
</summary>
</histogram>
<histogram name="OSCrypt.SecretPortalKeyProvider.NewInitFailure"
enum="SecretPortalKeyProviderInitStatus" expires_after="2025-07-28">
<owner>thomasanderson@chromium.org</owner>
<owner>thestig@chromium.org</owner>
<summary>
The result of the org.freedesktop.portal.Secret OSCrypt Async Key Provider
initialization if the previous initialization succeeded and the current
initialization failed, and the previous desktop matches the current desktop.
</summary>
</histogram>
<histogram name="OSCrypt.Win.Decrypt.Result" enum="BooleanSuccess"
expires_after="2025-02-10">
<owner>wfh@chromium.org</owner>