From eb29568e451a84c14ef8a6f7b0648d86d09785bb Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sun, 15 Feb 2026 19:54:50 +0100 Subject: [PATCH] feat: introduce `os_crypt_async` in `safeStorage` (#49054) * feat: support Freedesktop Secret Service OSCrypt client Refs https://issues.chromium.org/issues/40086962 Refs https://issues.chromium.org/issues/447372315 * chore: rework to async interface * refactor: allow customizing freedesktop config * docs: add more async impl info * refactor: reject when temporarily unavailable * chore: feedback from review * chore: push_back => emplace_back --- BUILD.gn | 5 + docs/api/cookies.md | 12 +- docs/api/safe-storage.md | 55 +- filenames.gni | 1 + lib/browser/api/safe-storage.ts | 4 +- patches/chromium/.patches | 2 +- ...fix_os_crypt_async_cookie_encryption.patch | 790 ------------------ ...nfig_in_freedesktopsecretkeyprovider.patch | 225 +++++ .../browser/api/electron_api_safe_storage.cc | 253 +++++- shell/browser/api/electron_api_safe_storage.h | 126 +++ shell/browser/browser.cc | 3 +- shell/browser/browser_process_impl.cc | 96 ++- shell/browser/electron_browser_main_parts.cc | 4 +- shell/browser/net/network_context_service.cc | 13 + shell/browser/net/network_context_service.h | 5 + .../net/system_network_context_manager.cc | 5 + spec/api-safe-storage-spec.ts | 147 +++- .../crash-cases/safe-storage/index.js | 23 +- spec/fuses-spec.ts | 76 ++ 19 files changed, 970 insertions(+), 875 deletions(-) delete mode 100644 patches/chromium/fix_os_crypt_async_cookie_encryption.patch create mode 100644 patches/chromium/refactor_allow_customizing_config_in_freedesktopsecretkeyprovider.patch create mode 100644 shell/browser/api/electron_api_safe_storage.h diff --git a/BUILD.gn b/BUILD.gn index 9dc7f22a66..c6bd1d6577 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -596,6 +596,7 @@ source_set("electron_lib") { use_libcxx_modules = false deps += [ + "//components/os_crypt/async/browser:keychain_key_provider", "//components/os_crypt/common:keychain_password_mac", "//components/remote_cocoa/app_shim", "//components/remote_cocoa/browser", @@ -658,6 +659,9 @@ source_set("electron_lib") { ":libnotify_loader", "//build/config/linux/gtk", "//components/crash/content/browser", + "//components/os_crypt/async/browser:freedesktop_secret_key_provider", + "//components/os_crypt/async/browser:posix_key_provider", + "//components/os_crypt/async/browser:secret_portal_key_provider", "//dbus", "//device/bluetooth", "//third_party/crashpad/crashpad/client", @@ -698,6 +702,7 @@ source_set("electron_lib") { deps += [ "//components/app_launch_prefetch", "//components/crash/core/app:crash_export_thunks", + "//components/os_crypt/async/browser:dpapi_key_provider", "//third_party/libxml:xml_writer", "//ui/wm", "//ui/wm/public", diff --git a/docs/api/cookies.md b/docs/api/cookies.md index e2bb62a14b..b85d6635d1 100644 --- a/docs/api/cookies.md +++ b/docs/api/cookies.md @@ -107,7 +107,7 @@ the response. cookie and will not be retained between sessions. * `sameSite` string (optional) - The [Same Site](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_cookies) policy to apply to this cookie. Can be `unspecified`, `no_restriction`, `lax` or `strict`. Default is `lax`. -Returns `Promise` - A promise which resolves when the cookie has been set +Returns `Promise` - A promise which resolves when the cookie has been set. Sets a cookie with `details`. @@ -116,16 +116,16 @@ Sets a cookie with `details`. * `url` string - The URL associated with the cookie. * `name` string - The name of cookie to remove. -Returns `Promise` - A promise which resolves when the cookie has been removed +Returns `Promise` - A promise which resolves when the cookie has been removed. -Removes the cookies matching `url` and `name` +Removes the cookies matching `url` and `name`. #### `cookies.flushStore()` -Returns `Promise` - A promise which resolves when the cookie store has been flushed +Returns `Promise` - A promise which resolves when the cookie store has been flushed. -Writes any unwritten cookies data to disk +Writes any unwritten cookies data to disk. -Cookies written by any method will not be written to disk immediately, but will be written every 30 seconds or 512 operations +Cookies written by any method will not be written to disk immediately, but will be written every 30 seconds or 512 operations. Calling this method can cause the cookie to be written to disk immediately. diff --git a/docs/api/safe-storage.md b/docs/api/safe-storage.md index 4f985db5e1..de2324837a 100644 --- a/docs/api/safe-storage.md +++ b/docs/api/safe-storage.md @@ -7,21 +7,44 @@ Process: [Main](../glossary.md#main-process) This module adds extra protection to data being stored on disk by using OS-provided cryptography systems. Current security semantics for each platform are outlined below. +> [!NOTE] +> We recommend using the asynchronous API (`encryptStringAsync`/`decryptStringAsync`) over the synchronous API. +> The async API is non-blocking, supports key rotation, and handles temporary unavailability gracefully. +> The synchronous API may be deprecated in a future version of Electron. + +## Platform-Specific Key Providers + +### Synchronous API + * **macOS**: Encryption keys are stored for your app in [Keychain Access](https://support.apple.com/en-ca/guide/keychain-access/kyca1083/mac) in a way that prevents other applications from loading them without user override. Therefore, content is protected from other users and other apps running in the same userspace. -* **Windows**: Encryption keys are generated via [DPAPI](https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata). -As per the Windows documentation: "Typically, only a user with the same logon credential as the user who encrypted the data can typically -decrypt the data". Therefore, content is protected from other users on the same machine, but not from other apps running in the +* **Windows**: Encryption keys are generated via [DPAPI](https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata). As per the Windows documentation: "Typically, only a user with the same logon credential as the user who encrypted the data can typically decrypt the data". Therefore, content is protected from other users on the same machine, but not from other apps running in the same userspace. * **Linux**: Encryption keys are generated and stored in a secret store that varies depending on your window manager and system setup. Options currently supported are `kwallet`, `kwallet5`, `kwallet6` and `gnome-libsecret`, but more may be available in future versions of Electron. As such, the security semantics of content protected via the `safeStorage` API vary between window managers and secret stores. - * Note that not all Linux setups have an available secret store. If no secret store is available, items stored in using the `safeStorage` API will be unprotected -as they are encrypted via hardcoded plaintext password. You can detect when this happens when `safeStorage.getSelectedStorageBackend()` returns `basic_text`. + * Note that not all Linux setups have an available secret store. If no secret store is available, items stored in using the `safeStorage` API will be unprotected as they are encrypted via hardcoded plaintext password. You can detect when this happens when `safeStorage.getSelectedStorageBackend()` returns `basic_text`. -Note that on Mac, access to the system Keychain is required and +Note that on macOS, access to the system Keychain is required and these calls can block the current thread to collect user input. The same is true for Linux, if a password management tool is available. +### Asynchronous API + +The asynchronous API uses pluggable key providers that vary by platform: + +* **macOS**: Encryption keys are stored and retrieved from [Keychain Access](https://developer.apple.com/documentation/security/keychain-items). This provides the same security model as the synchronous API, protecting content from other users and other apps running in the same userspace. +* **Windows**: Encryption keys are protected via [DPAPI](https://learn.microsoft.com/en-us/windows/win32/api/dpapi). This provides the same security model as the synchronous API, protecting content from other users on the same machine but not from other apps running in the same userspace. +* **Linux**: Multiple key providers may be available depending on the desktop environment: + * [`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html): Uses the Portal Secret D-Bus interface to retrieve application-specific secrets. This is the preferred provider for sandboxed environments like Flatpak. + * [Secret Service API](https://specifications.freedesktop.org/secret-service/latest/): Uses the freedesktop.org Secret Service API (e.g., GNOME Keyring) for key storage. + * A fallback provider is used for environments without a secret service available. + +Unlike the synchronous API, these operations are non-blocking and support additional features like key rotation (indicated by `shouldReEncrypt`) and temporary unavailability handling (indicated by `isTemporarilyUnavailable`). + +## Events + +The `safeStorage` module emits the following events: + ## Methods The `safeStorage` module has the following methods: @@ -34,6 +57,10 @@ On Linux, returns true if the app has emitted the `ready` event and the secret k On MacOS, returns true if Keychain is available. On Windows, returns true once the app has emitted the `ready` event. +### `safeStorage.isAsyncEncryptionAvailable()` + +Returns `Promise` - Whether encryption is available for asynchronous safeStorage operations. + ### `safeStorage.encryptString(plainText)` * `plainText` string @@ -49,7 +76,21 @@ This function will throw an error if encryption fails. Returns `string` - the decrypted string. Decrypts the encrypted buffer obtained with `safeStorage.encryptString` back into a string. -This function will throw an error if decryption fails. +### `safeStorage.encryptStringAsync(plainText)` + +* `plainText` string + +Returns `Promise` - An array of bytes representing the encrypted string. + +### `safeStorage.decryptStringAsync(encrypted)` + +* `encrypted` Buffer + +Returns `Promise` - Resolve with an object containing the following: + +* `shouldReEncrypt` boolean - whether data that has just been returned from the decrypt operation should be + re-encrypted, as the key has been rotated or a new key is available that provides a different security level. If `true`, you should call `decryptStringAsync` again to receive the new decrypted string. +* `result` string - the decrypted string. ### `safeStorage.setUsePlainTextEncryption(usePlainText)` diff --git a/filenames.gni b/filenames.gni index e10b35508d..87f99bf18b 100644 --- a/filenames.gni +++ b/filenames.gni @@ -299,6 +299,7 @@ filenames = { "shell/browser/api/electron_api_push_notifications.cc", "shell/browser/api/electron_api_push_notifications.h", "shell/browser/api/electron_api_safe_storage.cc", + "shell/browser/api/electron_api_safe_storage.h", "shell/browser/api/electron_api_screen.cc", "shell/browser/api/electron_api_screen.h", "shell/browser/api/electron_api_service_worker_context.cc", diff --git a/lib/browser/api/safe-storage.ts b/lib/browser/api/safe-storage.ts index 32fe0482f3..b599e1e75b 100644 --- a/lib/browser/api/safe-storage.ts +++ b/lib/browser/api/safe-storage.ts @@ -1,3 +1,3 @@ -const safeStorage = process._linkedBinding('electron_browser_safe_storage'); +const { safeStorage } = process._linkedBinding('electron_browser_safe_storage'); -module.exports = safeStorage; +export default safeStorage; diff --git a/patches/chromium/.patches b/patches/chromium/.patches index ee41d03e9e..e4556a9642 100644 --- a/patches/chromium/.patches +++ b/patches/chromium/.patches @@ -142,7 +142,7 @@ chore_disable_protocol_handler_dcheck.patch fix_check_for_file_existence_before_setting_mtime.patch fix_linux_tray_id.patch expose_gtk_ui_platform_field.patch -fix_os_crypt_async_cookie_encryption.patch patch_osr_control_screen_info.patch loaf_add_feature_to_enable_sourceurl_for_all_protocols.patch cherry-pick-e045399a1ecb.patch +refactor_allow_customizing_config_in_freedesktopsecretkeyprovider.patch diff --git a/patches/chromium/fix_os_crypt_async_cookie_encryption.patch b/patches/chromium/fix_os_crypt_async_cookie_encryption.patch deleted file mode 100644 index 486d37a5dd..0000000000 --- a/patches/chromium/fix_os_crypt_async_cookie_encryption.patch +++ /dev/null @@ -1,790 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Keeley Hammond -Date: Tue, 13 Jan 2026 13:26:29 -0800 -Subject: fix: revert OS_Crypt Async in Cookie Encryption - -Electron 40/M144 uses os_crypt async by default for cookie store -providers when using cookie encryption. We need time to properly -implement this in Electron and make sure the async logic is -working properly. - -This patch reverts the port of os_crypt async and falls back to -the old sync logic to unlock Electron 40. This patch can be removed -when os_crypt async is added to Electron. - -Revert "Reland "Port net::CookieCryptoDelegate to os_crypt async"" - -This reverts commit f01b115c7e21a09cc762f65bf7fd9c6ea9d9d0f8. - -diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn -index c75d7e336ad00230c2a7852f62c69b8f0cae748d..8e80ebd537871b204f254a4468996350b8f4f231 100644 ---- a/chrome/browser/BUILD.gn -+++ b/chrome/browser/BUILD.gn -@@ -716,6 +716,8 @@ static_library("browser") { - "net/chrome_report_sender.h", - "net/convert_explicitly_allowed_network_ports_pref.cc", - "net/convert_explicitly_allowed_network_ports_pref.h", -+ "net/cookie_encryption_provider_impl.cc", -+ "net/cookie_encryption_provider_impl.h", - "net/default_dns_over_https_config_source.cc", - "net/default_dns_over_https_config_source.h", - "net/dns_over_https_config_source.h", -diff --git a/chrome/browser/extensions/chrome_extension_cookies.cc b/chrome/browser/extensions/chrome_extension_cookies.cc -index fc13abe302557d38cfce798d46551989337abb2c..22eac75cf685039796ecf40e7d86c9f54084a08b 100644 ---- a/chrome/browser/extensions/chrome_extension_cookies.cc -+++ b/chrome/browser/extensions/chrome_extension_cookies.cc -@@ -6,7 +6,6 @@ - - #include - --#include "chrome/browser/browser_process.h" - #include "chrome/browser/content_settings/cookie_settings_factory.h" - #include "chrome/browser/content_settings/host_content_settings_map_factory.h" - #include "chrome/browser/extensions/chrome_extension_cookies_factory.h" -@@ -49,9 +48,7 @@ ChromeExtensionCookies::ChromeExtensionCookies(Profile* profile) - profile_->GetPath().Append(chrome::kExtensionsCookieFilename), - profile_->ShouldRestoreOldSessionCookies(), - profile_->ShouldPersistSessionCookies())); -- creation_config->crypto_delegate = cookie_config::GetCookieCryptoDelegate( -- g_browser_process->os_crypt_async(), -- content::GetUIThreadTaskRunner({})); -+ creation_config->crypto_delegate = cookie_config::GetCookieCryptoDelegate(); - } - creation_config->cookieable_schemes.push_back(extensions::kExtensionScheme); - -diff --git a/chrome/browser/net/chrome_network_service_browsertest.cc b/chrome/browser/net/chrome_network_service_browsertest.cc -index fa37d56b3a3b1e324ca121992fd7b54a945d75f7..05d4d5eaecf119a956210539f601b8f437aaa788 100644 ---- a/chrome/browser/net/chrome_network_service_browsertest.cc -+++ b/chrome/browser/net/chrome_network_service_browsertest.cc -@@ -5,7 +5,6 @@ - #include "base/feature_list.h" - #include "base/files/file_path.h" - #include "base/files/file_util.h" --#include "base/task/sequenced_task_runner.h" - #include "base/test/bind.h" - #include "base/test/scoped_feature_list.h" - #include "base/threading/thread_restrictions.h" -@@ -20,7 +19,6 @@ - #include "chrome/test/base/in_process_browser_test.h" - #include "chrome/test/base/ui_test_utils.h" - #include "components/cookie_config/cookie_store_util.h" --#include "components/os_crypt/async/browser/test_utils.h" - #include "content/public/browser/browser_context.h" - #include "content/public/browser/network_service_instance.h" - #include "content/public/browser/network_service_util.h" -@@ -139,16 +137,10 @@ class ChromeNetworkServiceBrowserTest - IN_PROC_BROWSER_TEST_P(ChromeNetworkServiceBrowserTest, - PRE_PRE_EncryptedCookies) { - // These test is only valid if crypto is enabled on the platform. -- auto os_crypt_async = os_crypt_async::GetTestOSCryptAsyncForTesting( -- /*is_sync_for_unittests=*/true); -- auto crypto_delegate = cookie_config::GetCookieCryptoDelegate( -- os_crypt_async.get(), base::SequencedTaskRunner::GetCurrentDefault()); -+ auto crypto_delegate = cookie_config::GetCookieCryptoDelegate(); - if (!crypto_delegate) { - GTEST_SKIP() << "No crypto on this platform."; - } -- base::RunLoop run_loop; -- crypto_delegate->Init(run_loop.QuitClosure()); -- run_loop.Run(); - std::string ciphertext; - crypto_delegate->EncryptString(kCookieValue, &ciphertext); - ASSERT_NE(ciphertext, kCookieValue) << "Crypto should really encrypt."; -diff --git a/services/network/public/cpp/cookie_encryption_provider_impl.cc b/chrome/browser/net/cookie_encryption_provider_impl.cc -similarity index 71% -rename from services/network/public/cpp/cookie_encryption_provider_impl.cc -rename to chrome/browser/net/cookie_encryption_provider_impl.cc -index 52fedf2057b963951be560a362fec28208c2a4b5..3f770666618f2df56b8cd6855766418d319481f0 100644 ---- a/services/network/public/cpp/cookie_encryption_provider_impl.cc -+++ b/chrome/browser/net/cookie_encryption_provider_impl.cc -@@ -1,19 +1,18 @@ --// Copyright 2025 The Chromium Authors -+// 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 "services/network/public/cpp/cookie_encryption_provider_impl.h" -+#include "chrome/browser/net/cookie_encryption_provider_impl.h" - -+#include "chrome/browser/browser_process.h" - #include "components/os_crypt/async/browser/os_crypt_async.h" - --CookieEncryptionProviderImpl::CookieEncryptionProviderImpl( -- os_crypt_async::OSCryptAsync* os_crypt_async) -- : os_crypt_async_(os_crypt_async) {} -+CookieEncryptionProviderImpl::CookieEncryptionProviderImpl() = default; - - CookieEncryptionProviderImpl::~CookieEncryptionProviderImpl() = default; - - void CookieEncryptionProviderImpl::GetEncryptor(GetEncryptorCallback callback) { -- os_crypt_async_->GetInstance(base::BindOnce( -+ g_browser_process->os_crypt_async()->GetInstance(base::BindOnce( - [](GetEncryptorCallback callback, os_crypt_async::Encryptor encryptor) { - std::move(callback).Run(std::move(encryptor)); - }, -diff --git a/services/network/public/cpp/cookie_encryption_provider_impl.h b/chrome/browser/net/cookie_encryption_provider_impl.h -similarity index 65% -rename from services/network/public/cpp/cookie_encryption_provider_impl.h -rename to chrome/browser/net/cookie_encryption_provider_impl.h -index 8f80cabd7c919c682e603ff6af0c12ae4431e366..68df8a7a04e9a8455b7143432173d9e48dc1ea5e 100644 ---- a/services/network/public/cpp/cookie_encryption_provider_impl.h -+++ b/chrome/browser/net/cookie_encryption_provider_impl.h -@@ -1,27 +1,20 @@ --// Copyright 2025 The Chromium Authors -+// 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 SERVICES_NETWORK_PUBLIC_CPP_COOKIE_ENCRYPTION_PROVIDER_IMPL_H_ --#define SERVICES_NETWORK_PUBLIC_CPP_COOKIE_ENCRYPTION_PROVIDER_IMPL_H_ -+#ifndef CHROME_BROWSER_NET_COOKIE_ENCRYPTION_PROVIDER_IMPL_H_ -+#define CHROME_BROWSER_NET_COOKIE_ENCRYPTION_PROVIDER_IMPL_H_ - --#include "base/component_export.h" --#include "base/memory/raw_ptr.h" - #include "components/os_crypt/async/common/encryptor.h" - #include "mojo/public/cpp/bindings/receiver_set.h" - #include "services/network/public/mojom/cookie_encryption_provider.mojom.h" - --namespace os_crypt_async { --class OSCryptAsync; --} -- - // Implementation of CookieEncryptionProvider interface. This is Windows only - // for now, but will be expanded to other platforms in future. --class COMPONENT_EXPORT(NETWORK_CPP) CookieEncryptionProviderImpl -+class CookieEncryptionProviderImpl - : public network::mojom::CookieEncryptionProvider { - public: -- explicit CookieEncryptionProviderImpl( -- os_crypt_async::OSCryptAsync* os_crypt_async); -+ CookieEncryptionProviderImpl(); - ~CookieEncryptionProviderImpl() override; - - CookieEncryptionProviderImpl(const CookieEncryptionProviderImpl&) = delete; -@@ -37,7 +30,6 @@ class COMPONENT_EXPORT(NETWORK_CPP) CookieEncryptionProviderImpl - - private: - mojo::ReceiverSet receivers_; -- raw_ptr os_crypt_async_; - }; - --#endif // SERVICES_NETWORK_PUBLIC_CPP_COOKIE_ENCRYPTION_PROVIDER_IMPL_H_ -+#endif // CHROME_BROWSER_NET_COOKIE_ENCRYPTION_PROVIDER_IMPL_H_ -diff --git a/chrome/browser/net/cookie_encryption_provider_interactive_uitest.cc b/chrome/browser/net/cookie_encryption_provider_interactive_uitest.cc -index b862afe7663111a6cbd342d33723942770bb0490..9dc46cedb109cea63bf71aa43fc7a2b64730ed12 100644 ---- a/chrome/browser/net/cookie_encryption_provider_interactive_uitest.cc -+++ b/chrome/browser/net/cookie_encryption_provider_interactive_uitest.cc -@@ -13,6 +13,7 @@ - #include "base/test/test_future.h" - #include "build/config/linux/dbus/buildflags.h" - #include "chrome/browser/browser_features.h" -+#include "chrome/browser/net/cookie_encryption_provider_impl.h" - #include "chrome/browser/policy/chrome_browser_policy_connector.h" - #include "chrome/browser/profiles/profile.h" - #include "chrome/browser/ui/browser.h" -@@ -25,7 +26,6 @@ - #include "content/public/test/browser_test.h" - #include "content/public/test/test_launcher.h" - #include "net/cookies/canonical_cookie.h" --#include "services/network/public/cpp/cookie_encryption_provider_impl.h" - #include "services/network/public/mojom/cookie_manager.mojom.h" - #include "services/network/public/mojom/network_context.mojom.h" - #include "testing/gtest/include/gtest/gtest.h" -diff --git a/chrome/browser/net/system_network_context_manager.cc b/chrome/browser/net/system_network_context_manager.cc -index 223c7a55b1db65430d22dcff9898845ccaca68a0..9f7347a39c1a0a982632fc6a6b04240b0a3b9510 100644 ---- a/chrome/browser/net/system_network_context_manager.cc -+++ b/chrome/browser/net/system_network_context_manager.cc -@@ -919,13 +919,8 @@ void SystemNetworkContextManager::DisableQuic() { - void SystemNetworkContextManager:: - AddCookieEncryptionManagerToNetworkContextParams( - network::mojom::NetworkContextParams* network_context_params) { -- if (!cookie_encryption_provider_) { -- cookie_encryption_provider_ = -- std::make_unique( -- g_browser_process->os_crypt_async()); -- } - network_context_params->cookie_encryption_provider = -- cookie_encryption_provider_->BindNewRemote(); -+ cookie_encryption_provider_.BindNewRemote(); - } - - void SystemNetworkContextManager::AddSSLConfigToNetworkContextParams( -diff --git a/chrome/browser/net/system_network_context_manager.h b/chrome/browser/net/system_network_context_manager.h -index 611833bce86135d792670a2cbfbfc661bcedf8dd..6d39b73f77d294ec21aa2d9c328e7f1fa9aad47d 100644 ---- a/chrome/browser/net/system_network_context_manager.h -+++ b/chrome/browser/net/system_network_context_manager.h -@@ -14,6 +14,7 @@ - #include "base/memory/raw_ptr.h" - #include "base/memory/scoped_refptr.h" - #include "chrome/browser/net/cert_verifier_service_time_updater.h" -+#include "chrome/browser/net/cookie_encryption_provider_impl.h" - #include "chrome/browser/net/proxy_config_monitor.h" - #include "chrome/browser/net/stub_resolver_config_reader.h" - #include "chrome/browser/ssl/ssl_config_service_manager.h" -@@ -23,7 +24,6 @@ - #include "mojo/public/cpp/bindings/pending_receiver.h" - #include "mojo/public/cpp/bindings/remote.h" - #include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom-forward.h" --#include "services/network/public/cpp/cookie_encryption_provider_impl.h" - #include "services/network/public/mojom/host_resolver.mojom-forward.h" - #include "services/network/public/mojom/network_context.mojom.h" - #include "services/network/public/mojom/network_service.mojom.h" -@@ -303,7 +303,7 @@ class SystemNetworkContextManager { - GssapiLibraryLoadObserver gssapi_library_loader_observer_{this}; - #endif // BUILDFLAG(IS_LINUX) - -- std::unique_ptr cookie_encryption_provider_; -+ CookieEncryptionProviderImpl cookie_encryption_provider_; - - std::unique_ptr cert_verifier_time_updater_; - }; -diff --git a/components/cookie_config/BUILD.gn b/components/cookie_config/BUILD.gn -index e348b0d1a59470c5cf153ae02e420b9dd6bd1892..a7a51003386fe7b62aaf5b7008c63acefd428942 100644 ---- a/components/cookie_config/BUILD.gn -+++ b/components/cookie_config/BUILD.gn -@@ -13,7 +13,7 @@ component("cookie_config") { - public_deps = [ "//base" ] - - deps = [ -- "//components/os_crypt/async/browser", -+ "//components/os_crypt/sync", - "//net:extras", - ] - } -diff --git a/components/cookie_config/DEPS b/components/cookie_config/DEPS -index 2c847bf159af83cd12bb343deff0cae9957a4183..a428c0b502bee622fbc7eff7d83a2e8500c058df 100644 ---- a/components/cookie_config/DEPS -+++ b/components/cookie_config/DEPS -@@ -1,4 +1,4 @@ - include_rules = [ -- "+components/os_crypt/async", -+ "+components/os_crypt/sync", - "+net/extras/sqlite", - ] -diff --git a/components/cookie_config/cookie_store_util.cc b/components/cookie_config/cookie_store_util.cc -index 55742de998756cbcd686d13a77b2a695eda06884..e7efdfe3a5ecae3b5461bba469f0377b3c920b21 100644 ---- a/components/cookie_config/cookie_store_util.cc -+++ b/components/cookie_config/cookie_store_util.cc -@@ -5,12 +5,8 @@ - #include "components/cookie_config/cookie_store_util.h" - - #include "base/functional/callback.h" --#include "base/memory/scoped_refptr.h" --#include "base/memory/weak_ptr.h" --#include "base/task/sequenced_task_runner.h" - #include "build/build_config.h" --#include "components/os_crypt/async/browser/os_crypt_async.h" --#include "components/os_crypt/async/common/encryptor.h" -+#include "components/os_crypt/sync/os_crypt.h" - #include "net/extras/sqlite/cookie_crypto_delegate.h" - - namespace cookie_config { -@@ -19,123 +15,40 @@ namespace cookie_config { - BUILDFLAG(IS_CHROMEOS) - namespace { - --void OnOsCryptReadyOnUi( -- base::OnceCallback callback, -- scoped_refptr task_runner, -- os_crypt_async::Encryptor encryptor) { -- task_runner->PostTask( -- FROM_HERE, base::BindOnce(std::move(callback), std::move(encryptor))); --} -- --void InitOnUi(base::OnceCallback callback, -- os_crypt_async::OSCryptAsync* os_crypt_async, -- scoped_refptr task_runner) { -- os_crypt_async->GetInstance( -- base::BindOnce(&OnOsCryptReadyOnUi, std::move(callback), -- std::move(task_runner)), -- os_crypt_async::Encryptor::Option::kEncryptSyncCompat); --} -- - // Use the operating system's mechanisms to encrypt cookies before writing - // them to persistent store. Currently this only is done with desktop OS's - // because ChromeOS and Android already protect the entire profile contents. - class CookieOSCryptoDelegate : public net::CookieCryptoDelegate { - public: -- CookieOSCryptoDelegate( -- os_crypt_async::OSCryptAsync* os_crypt_async, -- scoped_refptr ui_task_runner); -- -- CookieOSCryptoDelegate(const CookieOSCryptoDelegate&) = delete; -- CookieOSCryptoDelegate& operator=(const CookieOSCryptoDelegate&) = delete; -- -- ~CookieOSCryptoDelegate() override; -- -- // net::CookieCryptoDelegate implementation: - void Init(base::OnceClosure callback) override; - bool EncryptString(const std::string& plaintext, - std::string* ciphertext) override; - bool DecryptString(const std::string& ciphertext, - std::string* plaintext) override; -- -- private: -- void OnOsCryptReady(os_crypt_async::Encryptor encryptor); -- -- raw_ptr os_crypt_async_; -- scoped_refptr ui_task_runner_; -- std::optional encryptor_; -- -- bool initializing_ = false; -- std::vector init_callbacks_; -- -- base::WeakPtrFactory weak_ptr_factory_{this}; - }; - --CookieOSCryptoDelegate::CookieOSCryptoDelegate( -- os_crypt_async::OSCryptAsync* os_crypt_async, -- scoped_refptr ui_task_runner) -- : os_crypt_async_(os_crypt_async), ui_task_runner_(ui_task_runner) {} -- --CookieOSCryptoDelegate::~CookieOSCryptoDelegate() = default; -- - void CookieOSCryptoDelegate::Init(base::OnceClosure callback) { -- if (encryptor_.has_value()) { -- std::move(callback).Run(); -- return; -- } -- -- init_callbacks_.emplace_back(std::move(callback)); -- if (initializing_) { -- return; -- } -- initializing_ = true; -- -- // PostTaskAndReplyWithResult can't be used here because -- // OSCryptAsync::GetInstance() is async. -- ui_task_runner_->PostTask( -- FROM_HERE, -- base::BindOnce(&InitOnUi, -- base::BindOnce(&CookieOSCryptoDelegate::OnOsCryptReady, -- weak_ptr_factory_.GetWeakPtr()), -- os_crypt_async_, -- base::SequencedTaskRunner::GetCurrentDefault())); -- os_crypt_async_ = nullptr; -+ std::move(callback).Run(); - } - - bool CookieOSCryptoDelegate::EncryptString(const std::string& plaintext, - std::string* ciphertext) { -- CHECK(encryptor_) << "EncryptString called before Init completed"; -- return encryptor_->EncryptString(plaintext, ciphertext); -+ return OSCrypt::EncryptString(plaintext, ciphertext); - } - - bool CookieOSCryptoDelegate::DecryptString(const std::string& ciphertext, - std::string* plaintext) { -- CHECK(encryptor_) << "DecryptString called before Init completed"; -- return encryptor_->DecryptString(ciphertext, plaintext); --} -- --void CookieOSCryptoDelegate::OnOsCryptReady( -- os_crypt_async::Encryptor encryptor) { -- encryptor_ = std::move(encryptor); -- initializing_ = false; -- for (auto& callback : init_callbacks_) { -- std::move(callback).Run(); -- } -- init_callbacks_.clear(); -+ return OSCrypt::DecryptString(ciphertext, plaintext); - } - - } // namespace - --std::unique_ptr GetCookieCryptoDelegate( -- os_crypt_async::OSCryptAsync* os_crypt_async, -- scoped_refptr ui_task_runner) { -- return std::make_unique(os_crypt_async, -- ui_task_runner); -+std::unique_ptr GetCookieCryptoDelegate() { -+ return std::make_unique(); - } - #else // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || - // BUILDFLAG(IS_CHROMEOS) --std::unique_ptr GetCookieCryptoDelegate( -- os_crypt_async::OSCryptAsync* os_crypt_async, -- scoped_refptr ui_task_runner) { -+std::unique_ptr GetCookieCryptoDelegate() { - return nullptr; - } - #endif // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || -diff --git a/components/cookie_config/cookie_store_util.h b/components/cookie_config/cookie_store_util.h -index 9d142e9f13fb0d30d5795c2a82f2cbc5274d381c..1e1b7ebc234d7e3f981e023fe49cd0b13ed62c6e 100644 ---- a/components/cookie_config/cookie_store_util.h -+++ b/components/cookie_config/cookie_store_util.h -@@ -8,28 +8,17 @@ - #include - - #include "base/component_export.h" --#include "base/memory/scoped_refptr.h" -- --namespace base { --class SequencedTaskRunner; --} - - namespace net { - class CookieCryptoDelegate; - } // namespace net - --namespace os_crypt_async { --class OSCryptAsync; --} // namespace os_crypt_async -- - namespace cookie_config { - - // Factory method for returning a CookieCryptoDelegate if one is appropriate for - // this platform. - COMPONENT_EXPORT(COMPONENTS_COOKIE_CONFIG) --std::unique_ptr GetCookieCryptoDelegate( -- os_crypt_async::OSCryptAsync* os_crypt_async, -- scoped_refptr ui_task_runner); -+std::unique_ptr GetCookieCryptoDelegate(); - - } // namespace cookie_config - -diff --git a/components/os_crypt/sync/BUILD.gn b/components/os_crypt/sync/BUILD.gn -index bb308187837371ecfa2482affaf35ac7ed98c1f3..1e554fe95b0521a883ced83fc67f5d52a3d45759 100644 ---- a/components/os_crypt/sync/BUILD.gn -+++ b/components/os_crypt/sync/BUILD.gn -@@ -12,7 +12,13 @@ component("sync") { - visibility = [ - "//electron:*", - "//chrome/browser", -+ "//chrome/browser/prefs:impl", -+ "//chrome/browser/ui", -+ "//chrome/browser/web_applications", - "//chrome/test:test_support", -+ "//components/autofill/content/browser", -+ "//components/cookie_config", -+ "//components/gcm_driver", - "//components/os_crypt/async/browser:dpapi_key_provider", - "//components/os_crypt/async/browser:freedesktop_secret_key_provider", - "//components/os_crypt/async/browser:keychain_key_provider", -@@ -22,18 +28,24 @@ component("sync") { - "//components/os_crypt/async/common:unit_tests", - "//components/os_crypt/sync:test_support", - "//components/os_crypt/sync:unit_tests", -+ "//components/password_manager/core/browser", -+ "//components/password_manager/core/browser:hash_password_manager", -+ "//components/password_manager/core/browser:unit_tests", -+ "//components/password_manager/core/browser/password_store:password_store_impl", -+ "//components/password_manager/core/browser/password_store:unit_tests", - "//components/signin/core/browser", - "//components/sync:unit_tests", - "//components/sync/nigori", - "//components/sync/service", -+ "//components/trusted_vault", -+ "//components/trusted_vault:unit_tests", -+ "//content/browser", - "//headless:headless_non_renderer", -+ "//headless:headless_shell_lib", - "//ios/chrome/browser/web/model:web_internal", - "//services/network:network_service", - "//services/test/echo:lib", - ] -- if (is_mac) { -- visibility += [ "//headless:headless_shell_lib" ] -- } - - sources = [ - "os_crypt.h", -diff --git a/headless/BUILD.gn b/headless/BUILD.gn -index 0d07069219883d28af7add90ad4509a94109603f..b732da23aa014aaa3525bbadaec97178d7844e04 100644 ---- a/headless/BUILD.gn -+++ b/headless/BUILD.gn -@@ -373,7 +373,6 @@ component("headless_non_renderer") { - "//components/keyed_service/content", - "//components/origin_trials:browser", - "//components/origin_trials:common", -- "//components/os_crypt/async/browser", - "//components/os_crypt/sync", - "//components/policy:generated", - "//components/policy/content", -diff --git a/headless/lib/browser/DEPS b/headless/lib/browser/DEPS -index 75d0960a5964fabf518d0b8b2f67e29e9b3d6fe6..8261f1ab27597459726063cc6faa2a5ed0bfce17 100644 ---- a/headless/lib/browser/DEPS -+++ b/headless/lib/browser/DEPS -@@ -44,7 +44,6 @@ specific_include_rules = { - "headless_browser_impl.*": [ - "+services/device/public/cpp/geolocation/system_geolocation_source_apple.h", - "+services/device/public/cpp/geolocation/geolocation_system_permission_manager.h", -- "+components/os_crypt/async", - "+components/password_manager/core/browser/password_manager_switches.h", - "+components/policy", - "+components/prefs", -@@ -53,9 +52,6 @@ specific_include_rules = { - "+components/metrics", - "+components/variations", - ], -- "headless_request_context_manager.cc": [ -- "+components/os_crypt/async/browser", -- ], - "headless_browser_impl_unittest.cc": [ - "+third_party/blink/public/common/features.h", - ], -diff --git a/headless/lib/browser/headless_browser_context_impl.cc b/headless/lib/browser/headless_browser_context_impl.cc -index f664e9994a3c38ef2aa30773f6ca4668451dd76c..ad83a721a8bf17225af7d2c5954ecdd82cf8e1dc 100644 ---- a/headless/lib/browser/headless_browser_context_impl.cc -+++ b/headless/lib/browser/headless_browser_context_impl.cc -@@ -77,7 +77,7 @@ HeadlessBrowserContextImpl::HeadlessBrowserContextImpl( - ? base::FilePath() - : path_; - request_context_manager_ = std::make_unique( -- context_options_.get(), user_data_path, browser->os_crypt_async()); -+ context_options_.get(), user_data_path); - profile_metrics::SetBrowserProfileType( - this, IsOffTheRecord() ? profile_metrics::BrowserProfileType::kIncognito - : profile_metrics::BrowserProfileType::kRegular); -diff --git a/headless/lib/browser/headless_browser_impl.cc b/headless/lib/browser/headless_browser_impl.cc -index f0c79ccd63e102c4ef51535f476ceddc6c5156a9..c1e9430b3f5b67338f204ca5563a02c2da87cd49 100644 ---- a/headless/lib/browser/headless_browser_impl.cc -+++ b/headless/lib/browser/headless_browser_impl.cc -@@ -16,8 +16,6 @@ - #include "base/task/single_thread_task_runner.h" - #include "build/config/linux/dbus/buildflags.h" - #include "components/embedder_support/user_agent_utils.h" --#include "components/os_crypt/async/browser/os_crypt_async.h" --#include "components/os_crypt/async/common/encryptor.h" - #include "components/version_info/version_info.h" - #include "content/public/browser/browser_task_traits.h" - #include "content/public/browser/browser_thread.h" -@@ -212,8 +210,7 @@ void HeadlessBrowserImpl::SetDefaultBrowserContext( - if (default_browser_context_ && !system_request_context_manager_) { - system_request_context_manager_ = - HeadlessRequestContextManager::CreateSystemContext( -- HeadlessBrowserContextImpl::From(browser_context)->options(), -- os_crypt_async()); -+ HeadlessBrowserContextImpl::From(browser_context)->options()); - } - } - -@@ -269,8 +266,6 @@ bool HeadlessBrowserImpl::ShouldStartDevToolsServer() { - } - - void HeadlessBrowserImpl::PreMainMessageLoopRun() { -- CreateOSCryptAsync(); -- - platform_delegate_->Initialize(options_.value()); - - // We don't support the tethering domain on this agent host. -@@ -287,7 +282,6 @@ void HeadlessBrowserImpl::WillRunMainMessageLoop(base::RunLoop& run_loop) { - } - - void HeadlessBrowserImpl::PostMainMessageLoopRun() { -- os_crypt_async_.reset(); - #if defined(HEADLESS_USE_PREFS) - if (local_state_) { - local_state_->CommitPendingWrite(); -diff --git a/headless/lib/browser/headless_browser_impl.h b/headless/lib/browser/headless_browser_impl.h -index 1d9ba1861de0065cb059710fab7b619c0df55216..69056c94a348566e2d080307c794e5dd28322dff 100644 ---- a/headless/lib/browser/headless_browser_impl.h -+++ b/headless/lib/browser/headless_browser_impl.h -@@ -31,9 +31,11 @@ class PolicyService; - class PrefService; - #endif - --namespace os_crypt_async { --class OSCryptAsync; --} -+#if BUILDFLAG(IS_MAC) -+namespace device { -+class GeolocationSystemPermissionManager; -+} // namespace device -+#endif - - namespace ui { - class Compositor; -@@ -99,10 +101,6 @@ class HEADLESS_EXPORT HeadlessBrowserImpl : public HeadlessBrowser { - - int exit_code() const { return exit_code_; } - -- os_crypt_async::OSCryptAsync* os_crypt_async() { -- return os_crypt_async_.get(); -- } -- - #if defined(HEADLESS_USE_PREFS) - void CreatePrefService(); - PrefService* GetPrefs(); -@@ -121,8 +119,6 @@ class HEADLESS_EXPORT HeadlessBrowserImpl : public HeadlessBrowser { - - int exit_code_ = 0; - -- std::unique_ptr os_crypt_async_; -- - base::flat_map> - browser_contexts_; - raw_ptr -diff --git a/headless/lib/browser/headless_request_context_manager.cc b/headless/lib/browser/headless_request_context_manager.cc -index 6c4ce0a6fa6624cace08bfdb2c62b12836a744fa..fe1a11f94a709400434fb41a5bdcdb8f4d47a959 100644 ---- a/headless/lib/browser/headless_request_context_manager.cc -+++ b/headless/lib/browser/headless_request_context_manager.cc -@@ -11,7 +11,6 @@ - #include "base/task/single_thread_task_runner.h" - #include "build/build_config.h" - #include "components/embedder_support/switches.h" --#include "components/os_crypt/async/browser/os_crypt_async.h" - #include "content/public/browser/browser_thread.h" - #include "content/public/browser/network_service_instance.h" - #include "headless/lib/browser/headless_browser_context_options.h" -@@ -138,10 +137,9 @@ class HeadlessProxyConfigMonitor - // static - std::unique_ptr - HeadlessRequestContextManager::CreateSystemContext( -- const HeadlessBrowserContextOptions* options, -- os_crypt_async::OSCryptAsync* os_crypt_async) { -+ const HeadlessBrowserContextOptions* options) { - auto manager = std::make_unique( -- options, base::FilePath(), os_crypt_async); -+ options, base::FilePath()); - - base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); - auto auth_params = ::network::mojom::HttpAuthDynamicParams::New(); -@@ -172,8 +170,7 @@ HeadlessRequestContextManager::CreateSystemContext( - - HeadlessRequestContextManager::HeadlessRequestContextManager( - const HeadlessBrowserContextOptions* options, -- base::FilePath user_data_path, -- os_crypt_async::OSCryptAsync* os_crypt_async) -+ base::FilePath user_data_path) - : - // On Windows, Cookie encryption requires access to local_state prefs. - #if BUILDFLAG(IS_WIN) && !defined(HEADLESS_USE_PREFS) -@@ -183,7 +180,6 @@ HeadlessRequestContextManager::HeadlessRequestContextManager( - !base::CommandLine::ForCurrentProcess()->HasSwitch( - switches::kDisableCookieEncryption)), - #endif -- os_crypt_async_(os_crypt_async), - user_data_path_(std::move(user_data_path)), - disk_cache_dir_(options->disk_cache_dir()), - accept_language_(options->accept_language()), -@@ -192,10 +188,6 @@ HeadlessRequestContextManager::HeadlessRequestContextManager( - options->proxy_config() - ? std::make_unique(*options->proxy_config()) - : nullptr) { -- if (cookie_encryption_enabled_) { -- cookie_encryption_provider_ = -- std::make_unique(os_crypt_async_.get()); -- } - if (!proxy_config_) { - base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); - if (command_line->HasSwitch(switches::kNoSystemProxyConfigService)) { -@@ -240,10 +232,6 @@ void HeadlessRequestContextManager::ConfigureNetworkContextParamsInternal( - - if (!user_data_path_.empty()) { - context_params->enable_encrypted_cookies = cookie_encryption_enabled_; -- if (cookie_encryption_enabled_) { -- context_params->cookie_encryption_provider = -- cookie_encryption_provider_->BindNewRemote(); -- } - context_params->file_paths = - ::network::mojom::NetworkContextFilePaths::New(); - context_params->file_paths->data_directory = -diff --git a/headless/lib/browser/headless_request_context_manager.h b/headless/lib/browser/headless_request_context_manager.h -index 91d74eaadd9f4d451e809b38a2f999b298068820..e45427ce90f909e609688ab59f4581b185b6757e 100644 ---- a/headless/lib/browser/headless_request_context_manager.h -+++ b/headless/lib/browser/headless_request_context_manager.h -@@ -13,13 +13,8 @@ - #include "content/public/browser/browser_context.h" - #include "mojo/public/cpp/bindings/pending_remote.h" - #include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom-forward.h" --#include "services/network/public/cpp/cookie_encryption_provider_impl.h" - #include "services/network/public/mojom/network_context.mojom.h" - --namespace os_crypt_async { --class OSCryptAsync; --} -- - namespace headless { - - class HeadlessBrowserContextOptions; -@@ -28,12 +23,10 @@ class HeadlessProxyConfigMonitor; - class HeadlessRequestContextManager { - public: - static std::unique_ptr CreateSystemContext( -- const HeadlessBrowserContextOptions* options, -- os_crypt_async::OSCryptAsync* os_crypt_async); -+ const HeadlessBrowserContextOptions* options); - - HeadlessRequestContextManager(const HeadlessBrowserContextOptions* options, -- base::FilePath user_data_path, -- os_crypt_async::OSCryptAsync* os_crypt_async); -+ base::FilePath user_data_path); - - HeadlessRequestContextManager(const HeadlessRequestContextManager&) = delete; - HeadlessRequestContextManager& operator=( -@@ -56,15 +49,12 @@ class HeadlessRequestContextManager { - - const bool cookie_encryption_enabled_; - -- const raw_ptr os_crypt_async_; -- - base::FilePath user_data_path_; - base::FilePath disk_cache_dir_; - std::string accept_language_; - std::string user_agent_; - std::unique_ptr proxy_config_; - std::unique_ptr proxy_config_monitor_; -- std::unique_ptr cookie_encryption_provider_; - - mojo::PendingRemote<::network::mojom::NetworkContext> system_context_; - }; -diff --git a/services/network/network_context.cc b/services/network/network_context.cc -index f9e704f9dc76f802b330487238717a6df3ba7b36..1702b7f7603d98e2f08a8af7310daa1fb3250d54 100644 ---- a/services/network/network_context.cc -+++ b/services/network/network_context.cc -@@ -3274,12 +3274,7 @@ NetworkContext::MakeSessionCleanupCookieStore() const { - crypto_delegate = std::make_unique( - std::move(params_->cookie_encryption_provider)); - } else { --#if !BUILDFLAG(IS_ANDROID) -- // A cookie crypto delegate should not be created on Android to -- // match the behavior of cookie_config::GetCookieCryptoDelegate(). -- // See https://crbug.com/449652881 -- NOTREACHED(); --#endif -+ crypto_delegate = cookie_config::GetCookieCryptoDelegate(); - } - } - -diff --git a/services/network/public/cpp/BUILD.gn b/services/network/public/cpp/BUILD.gn -index eb6d8e40d27b7d1027e9afcace37aad487c333d7..3916ffd9787183bdd1e04dce1fe8e9dafd16b338 100644 ---- a/services/network/public/cpp/BUILD.gn -+++ b/services/network/public/cpp/BUILD.gn -@@ -69,8 +69,6 @@ component("cpp") { - "content_decoding_interceptor.h", - "content_language_parser.cc", - "content_language_parser.h", -- "cookie_encryption_provider_impl.cc", -- "cookie_encryption_provider_impl.h", - "cors/cors.cc", - "cors/cors.h", - "cors/origin_access_list.cc", -@@ -191,8 +189,6 @@ component("cpp") { - deps = [ - "//base", - "//components/link_header_util", -- "//components/os_crypt/async/browser", -- "//components/os_crypt/async/common", - "//components/prefs", - "//ipc", - "//net", -diff --git a/services/network/public/mojom/network_context.mojom b/services/network/public/mojom/network_context.mojom -index 0a837fbd18a0e597805b418a7f3022c499fb0c41..e511f65399c20cb9889c56a1c2c9e97eb84b3bf2 100644 ---- a/services/network/public/mojom/network_context.mojom -+++ b/services/network/public/mojom/network_context.mojom -@@ -576,9 +576,10 @@ struct NetworkContextParams { - bool acam_preflight_spec_conformant = true; - - // Sets the cookie encryption provider to be used by this network context if -- // `enable_encrypted_cookies` is enabled. -- // The `GetEncryptor` method on the supplied `cookie_encryption_provider` is -- // called to obtain a valid set of keys for cookie encryption. -+ // `enable_encrypted_cookies` is also enabled. -+ // If both are set then the `GetEncryptor` method on the supplied -+ // `cookie_encryption_provider` is called to obtain a valid set of keys for -+ // cookie encryption. - pending_remote? cookie_encryption_provider; - - // Enables Device Bound Session Credential for this network context. diff --git a/patches/chromium/refactor_allow_customizing_config_in_freedesktopsecretkeyprovider.patch b/patches/chromium/refactor_allow_customizing_config_in_freedesktopsecretkeyprovider.patch new file mode 100644 index 0000000000..b30fa272ff --- /dev/null +++ b/patches/chromium/refactor_allow_customizing_config_in_freedesktopsecretkeyprovider.patch @@ -0,0 +1,225 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Shelley Vohr +Date: Tue, 20 Jan 2026 10:20:39 +0000 +Subject: refactor: allow customizing config in FreedesktopSecretKeyProvider + +This commit allows customizing components of the FreedesktopSecretKeyProvider +via config, specifically: +* App name +* KWallet folder name +* KWallet key name + +This allows FreedesktopSecretKeyProvider to be used by multiple apps without +naming conflicts. This should be upstreamed to Chromium if possible. + +diff --git a/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc b/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc +index c45f79eea18190a9216fd5ff1b3cf9d0d86ec059..356c6931017c83f7a89c5125f0bb90c8bc58569d 100644 +--- a/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc ++++ b/components/os_crypt/async/browser/freedesktop_secret_key_provider.cc +@@ -36,6 +36,30 @@ namespace os_crypt_async { + + namespace { + ++const char* GetDefaultAppName() { ++#if BUILDFLAG(GOOGLE_CHROME_BRANDING) ++ return "chrome"; ++#else ++ return "chromium"; ++#endif ++} ++ ++const char* GetDefaultKWalletFolder() { ++#if BUILDFLAG(GOOGLE_CHROME_BRANDING) ++ return "Chrome Keys"; ++#else ++ return "Chromium Keys"; ++#endif ++} ++ ++const char* GetDefaultKeyName() { ++#if BUILDFLAG(GOOGLE_CHROME_BRANDING) ++ return "Chrome Safe Storage"; ++#else ++ return "Chromium Safe Storage"; ++#endif ++} ++ + constexpr char kUmaInitStatus[] = + "OSCrypt.FreedesktopSecretKeyProvider.InitStatus"; + constexpr char kUmaErrorDetail[] = +@@ -116,6 +140,24 @@ const char* InitStatusToString( + + } // namespace + ++FreedesktopSecretKeyProvider::Config::Config() ++ : app_name(GetDefaultAppName()), ++ kwallet_folder(GetDefaultKWalletFolder()), ++ key_name(GetDefaultKeyName()) {} ++ ++FreedesktopSecretKeyProvider::Config::~Config() = default; ++ ++FreedesktopSecretKeyProvider::Config::Config(const Config&) = default; ++ ++FreedesktopSecretKeyProvider::Config& ++FreedesktopSecretKeyProvider::Config::operator=(const Config&) = default; ++ ++// static ++FreedesktopSecretKeyProvider::Config ++FreedesktopSecretKeyProvider::GetDefaultConfig() { ++ return Config(); ++} ++ + // A helper class to handle a Secret Service prompt. It is templated on the + // return type expected from the prompt. + template +@@ -246,8 +288,19 @@ FreedesktopSecretKeyProvider::FreedesktopSecretKeyProvider( + const std::string& password_store, + const std::string& product_name, + scoped_refptr bus) ++ : FreedesktopSecretKeyProvider(password_store, ++ product_name, ++ GetDefaultConfig(), ++ std::move(bus)) {} ++ ++FreedesktopSecretKeyProvider::FreedesktopSecretKeyProvider( ++ const std::string& password_store, ++ const std::string& product_name, ++ const Config& config, ++ scoped_refptr bus) + : password_store_(password_store), + product_name_(product_name), ++ config_(config), + bus_(std::move(bus)) { + if (!bus_) { + bus_ = dbus_thread_linux::GetSharedSessionBus(); +@@ -479,7 +532,7 @@ void FreedesktopSecretKeyProvider::OnOpenSession( + session_opened_ = true; + + std::map search_attrs{ +- {kApplicationAttributeKey, kAppName}}; ++ {kApplicationAttributeKey, config_.app_name}}; + + dbus_utils::CallMethod<"a{ss}", "ao">( + default_collection_proxy_, kSecretCollectionInterface, kMethodSearchItems, +@@ -668,7 +721,7 @@ void FreedesktopSecretKeyProvider::OnKWalletOpen(int32_t handle) { + kwallet_proxy_, kKWalletInterface, kKWalletMethodHasFolder, + base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletHasFolder, + weak_ptr_factory_.GetWeakPtr()), +- kwallet_handle_, kKWalletFolder, product_name_); ++ kwallet_handle_, config_.kwallet_folder, product_name_); + } + + void FreedesktopSecretKeyProvider::OnKWalletHasFolder( +@@ -685,13 +738,13 @@ void FreedesktopSecretKeyProvider::OnKWalletHasFolder( + kwallet_proxy_, kKWalletInterface, kKWalletMethodHasEntry, + base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletHasEntry, + weak_ptr_factory_.GetWeakPtr()), +- kwallet_handle_, kKWalletFolder, kKeyName, product_name_); ++ kwallet_handle_, config_.kwallet_folder, config_.key_name, product_name_); + } else { + dbus_utils::CallMethod<"iss", "b">( + kwallet_proxy_, kKWalletInterface, kKWalletMethodCreateFolder, + base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletCreateFolder, + weak_ptr_factory_.GetWeakPtr()), +- kwallet_handle_, kKWalletFolder, product_name_); ++ kwallet_handle_, config_.kwallet_folder, product_name_); + } + } + +@@ -725,7 +778,7 @@ void FreedesktopSecretKeyProvider::OnKWalletHasEntry( + kwallet_proxy_, kKWalletInterface, kKWalletMethodReadPassword, + base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletReadPassword, + weak_ptr_factory_.GetWeakPtr()), +- kwallet_handle_, kKWalletFolder, kKeyName, product_name_); ++ kwallet_handle_, config_.kwallet_folder, config_.key_name, product_name_); + } else { + GenerateAndWriteKWalletPassword(); + } +@@ -761,7 +814,7 @@ void FreedesktopSecretKeyProvider::GenerateAndWriteKWalletPassword() { + kwallet_proxy_, kKWalletInterface, kKWalletMethodWritePassword, + base::BindOnce(&FreedesktopSecretKeyProvider::OnKWalletWritePassword, + weak_ptr_factory_.GetWeakPtr(), secret), +- kwallet_handle_, kKWalletFolder, kKeyName, secret->as_string(), ++ kwallet_handle_, config_.kwallet_folder, config_.key_name, secret->as_string(), + product_name_); + } + +@@ -789,14 +842,14 @@ void FreedesktopSecretKeyProvider::OnKWalletWritePassword( + void FreedesktopSecretKeyProvider::CreateItem( + scoped_refptr secret) { + std::map attributes{ +- {kApplicationAttributeKey, kAppName}, ++ {kApplicationAttributeKey, config_.app_name}, + {kSchemaAttributeKey, kSchemaAttributeValue}}; + + std::map props; + props.emplace(kSecretItemAttributesProperty, + dbus_utils::Variant::Wrap<"a{ss}">(std::move(attributes))); + props.emplace(kSecretItemLabelProperty, +- dbus_utils::Variant::Wrap<"s">(kKeyName)); ++ dbus_utils::Variant::Wrap<"s">(config_.key_name)); + + std::vector secret_bytes(secret->begin(), secret->end()); + auto secret_struct = +diff --git a/components/os_crypt/async/browser/freedesktop_secret_key_provider.h b/components/os_crypt/async/browser/freedesktop_secret_key_provider.h +index bc2c74090d3db088b97132c5cd83950510fe85b4..38f6384083537f60d12f016fbb67adc694e6f457 100644 +--- a/components/os_crypt/async/browser/freedesktop_secret_key_provider.h ++++ b/components/os_crypt/async/browser/freedesktop_secret_key_provider.h +@@ -81,11 +81,32 @@ class FreedesktopSecretKeyProvider : public KeyProvider { + kMaxValue = kExtraDataInResponse, + }; + ++ struct Config { ++ Config(); ++ ~Config(); ++ Config(const Config&); ++ Config& operator=(const Config&); ++ ++ // The application name used for D-Bus attributes. ++ std::string app_name; ++ // The folder name used in KWallet. ++ std::string kwallet_folder; ++ // The key name used for storing the encryption key. ++ std::string key_name; ++ }; ++ + FreedesktopSecretKeyProvider(const std::string& password_store, + const std::string& product_name, + scoped_refptr bus); ++ FreedesktopSecretKeyProvider(const std::string& password_store, ++ const std::string& product_name, ++ const Config& config, ++ scoped_refptr bus); + ~FreedesktopSecretKeyProvider() override; + ++ // Returns the default configuration with platform-specific defaults. ++ static Config GetDefaultConfig(); ++ + // KeyProvider: + void GetKey(KeyCallback callback) override; + bool UseForEncryption() override; +@@ -172,16 +193,6 @@ class FreedesktopSecretKeyProvider : public KeyProvider { + static constexpr int kKWalletInvalidHandle = -1; + static constexpr int kKWalletInvalidTransactionId = -1; + +-#if BUILDFLAG(GOOGLE_CHROME_BRANDING) +- static constexpr char kKWalletFolder[] = "Chrome Keys"; +- static constexpr char kKeyName[] = "Chrome Safe Storage"; +- static constexpr char kAppName[] = "chrome"; +-#else +- static constexpr char kKWalletFolder[] = "Chromium Keys"; +- static constexpr char kKeyName[] = "Chromium Safe Storage"; +- static constexpr char kAppName[] = "chromium"; +-#endif +- + void InitializeFreedesktopSecretService(); + void OnServiceStarted(std::optional service_started); + void OnReadAliasDefault(dbus_utils::CallMethodResultSig<"o"> collection_path); +@@ -238,6 +249,7 @@ class FreedesktopSecretKeyProvider : public KeyProvider { + + const std::string password_store_; + const std::string product_name_; ++ const Config config_; + scoped_refptr bus_; + KeyCallback key_callback_; + diff --git a/shell/browser/api/electron_api_safe_storage.cc b/shell/browser/api/electron_api_safe_storage.cc index ae48684472..f52b4cee1e 100644 --- a/shell/browser/api/electron_api_safe_storage.cc +++ b/shell/browser/api/electron_api_safe_storage.cc @@ -4,13 +4,19 @@ #include +#include "shell/browser/api/electron_api_safe_storage.h" + +#include "base/functional/bind.h" +#include "base/run_loop.h" +#include "components/os_crypt/async/browser/os_crypt_async.h" #include "components/os_crypt/sync/os_crypt.h" +#include "gin/object_template_builder.h" #include "shell/browser/browser.h" #include "shell/browser/browser_process_impl.h" #include "shell/browser/javascript_environment.h" #include "shell/common/gin_converters/base_converter.h" #include "shell/common/gin_converters/callback_converter.h" -#include "shell/common/gin_helper/dictionary.h" +#include "shell/common/gin_helper/handle.h" #include "shell/common/node_includes.h" #include "shell/common/node_util.h" @@ -18,17 +24,130 @@ namespace { const char* kEncryptionVersionPrefixV10 = "v10"; const char* kEncryptionVersionPrefixV11 = "v11"; -bool use_password_v10 = false; -bool IsEncryptionAvailable() { +} // namespace + +namespace electron::api { + +gin::DeprecatedWrapperInfo SafeStorage::kWrapperInfo = { + gin::kEmbedderNativeGin}; + +SafeStorage::PendingEncrypt::PendingEncrypt( + gin_helper::Promise> promise, + std::string plaintext) + : promise(std::move(promise)), plaintext(std::move(plaintext)) {} +SafeStorage::PendingEncrypt::~PendingEncrypt() = default; +SafeStorage::PendingEncrypt::PendingEncrypt(PendingEncrypt&&) = default; +SafeStorage::PendingEncrypt& SafeStorage::PendingEncrypt::operator=( + PendingEncrypt&&) = default; + +SafeStorage::PendingDecrypt::PendingDecrypt( + gin_helper::Promise promise, + std::string ciphertext) + : promise(std::move(promise)), ciphertext(std::move(ciphertext)) {} +SafeStorage::PendingDecrypt::~PendingDecrypt() = default; +SafeStorage::PendingDecrypt::PendingDecrypt(PendingDecrypt&&) = default; +SafeStorage::PendingDecrypt& SafeStorage::PendingDecrypt::operator=( + PendingDecrypt&&) = default; + +gin_helper::Handle SafeStorage::Create(v8::Isolate* isolate) { + return gin_helper::CreateHandle(isolate, new SafeStorage(isolate)); +} + +SafeStorage::SafeStorage(v8::Isolate* isolate) { + if (electron::Browser::Get()->is_ready()) { + OnFinishLaunching({}); + } else { + Browser::Get()->AddObserver(this); + } +} + +SafeStorage::~SafeStorage() { + Browser::Get()->RemoveObserver(this); +} + +gin::ObjectTemplateBuilder SafeStorage::GetObjectTemplateBuilder( + v8::Isolate* isolate) { + return gin_helper::DeprecatedWrappable::GetObjectTemplateBuilder( + isolate) + .SetMethod("isEncryptionAvailable", &SafeStorage::IsEncryptionAvailable) + .SetMethod("isAsyncEncryptionAvailable", + &SafeStorage::IsAsyncEncryptionAvailable) + .SetMethod("setUsePlainTextEncryption", &SafeStorage::SetUsePasswordV10) + .SetMethod("encryptString", &SafeStorage::EncryptString) + .SetMethod("decryptString", &SafeStorage::DecryptString) + .SetMethod("encryptStringAsync", &SafeStorage::encryptStringAsync) + .SetMethod("decryptStringAsync", &SafeStorage::decryptStringAsync) #if BUILDFLAG(IS_LINUX) - // Calling IsEncryptionAvailable() before the app is ready results in a crash - // on Linux. - // Refs: https://github.com/electron/electron/issues/32206. + .SetMethod("getSelectedStorageBackend", + &SafeStorage::GetSelectedLinuxBackend) +#endif + ; +} + +void SafeStorage::OnFinishLaunching(base::DictValue launch_info) { + g_browser_process->os_crypt_async()->GetInstance( + base::BindOnce(&SafeStorage::OnOsCryptReady, base::Unretained(this)), + os_crypt_async::Encryptor::Option::kEncryptSyncCompat); +} + +void SafeStorage::OnOsCryptReady(os_crypt_async::Encryptor encryptor) { + encryptor_ = std::move(encryptor); + is_available_ = true; + + for (auto& pending : pending_encrypts_) { + std::string ciphertext; + bool encrypted = encryptor_->EncryptString(pending.plaintext, &ciphertext); + if (encrypted) { + pending.promise.Resolve( + electron::Buffer::Copy(pending.promise.isolate(), ciphertext) + .ToLocalChecked()); + } else { + pending.promise.RejectWithErrorMessage( + "Error while encrypting the text provided to " + "safeStorage.encryptStringAsync."); + } + } + pending_encrypts_.clear(); + + for (auto& pending : pending_decrypts_) { + std::string plaintext; + os_crypt_async::Encryptor::DecryptFlags flags; + bool decrypted = + encryptor_->DecryptString(pending.ciphertext, &plaintext, &flags); + + if (decrypted) { + v8::Isolate* isolate = pending.promise.isolate(); + v8::HandleScope handle_scope(isolate); + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + + dict.Set("shouldReEncrypt", flags.should_reencrypt); + dict.Set("result", plaintext); + + pending.promise.Resolve(dict); + } else if (flags.temporarily_unavailable) { + pending.promise.RejectWithErrorMessage( + "safeStorage.decryptStringAsync is temporarily unavailable. " + "Please try again."); + } else { + pending.promise.RejectWithErrorMessage( + "Error while decrypting the ciphertext provided to " + "safeStorage.decryptStringAsync."); + } + } + pending_decrypts_.clear(); +} + +const char* SafeStorage::GetTypeName() { + return "SafeStorage"; +} + +bool SafeStorage::IsEncryptionAvailable() { if (!electron::Browser::Get()->is_ready()) return false; +#if BUILDFLAG(IS_LINUX) return OSCrypt::IsEncryptionAvailable() || - (use_password_v10 && + (use_password_v10_ && static_cast(g_browser_process) ->linux_storage_backend() == "basic_text"); #else @@ -36,12 +155,24 @@ bool IsEncryptionAvailable() { #endif } -void SetUsePasswordV10(bool use) { - use_password_v10 = use; +bool SafeStorage::IsAsyncEncryptionAvailable() { + if (!electron::Browser::Get()->is_ready()) + return false; +#if BUILDFLAG(IS_LINUX) + return is_available_ || (use_password_v10_ && + static_cast(g_browser_process) + ->linux_storage_backend() == "basic_text"); +#else + return is_available_; +#endif +} + +void SafeStorage::SetUsePasswordV10(bool use) { + use_password_v10_ = use; } #if BUILDFLAG(IS_LINUX) -std::string GetSelectedLinuxBackend() { +std::string SafeStorage::GetSelectedLinuxBackend() { if (!electron::Browser::Get()->is_ready()) return "unknown"; return static_cast(g_browser_process) @@ -49,8 +180,8 @@ std::string GetSelectedLinuxBackend() { } #endif -v8::Local EncryptString(v8::Isolate* isolate, - const std::string& plaintext) { +v8::Local SafeStorage::EncryptString(v8::Isolate* isolate, + const std::string& plaintext) { if (!IsEncryptionAvailable()) { if (!electron::Browser::Get()->is_ready()) { gin_helper::ErrorThrower(isolate).ThrowError( @@ -77,7 +208,8 @@ v8::Local EncryptString(v8::Isolate* isolate, return electron::Buffer::Copy(isolate, ciphertext).ToLocalChecked(); } -std::string DecryptString(v8::Isolate* isolate, v8::Local buffer) { +std::string SafeStorage::DecryptString(v8::Isolate* isolate, + v8::Local buffer) { if (!IsEncryptionAvailable()) { if (!electron::Browser::Get()->is_ready()) { gin_helper::ErrorThrower(isolate).ThrowError( @@ -126,7 +258,92 @@ std::string DecryptString(v8::Isolate* isolate, v8::Local buffer) { return plaintext; } -} // namespace +v8::Local SafeStorage::encryptStringAsync( + v8::Isolate* isolate, + const std::string& plaintext) { + gin_helper::Promise> promise(isolate); + v8::Local handle = promise.GetHandle(); + + if (!electron::Browser::Get()->is_ready()) { + promise.RejectWithErrorMessage( + "safeStorage cannot be used before app is ready"); + return handle; + } + + if (is_available_) { + std::string ciphertext; + bool encrypted = encryptor_->EncryptString(plaintext, &ciphertext); + if (encrypted) { + promise.Resolve( + electron::Buffer::Copy(isolate, ciphertext).ToLocalChecked()); + } else { + promise.RejectWithErrorMessage( + "Error while encrypting the text provided to " + "safeStorage.encryptStringAsync."); + } + return handle; + } + + pending_encrypts_.emplace_back(std::move(promise), std::move(plaintext)); + return handle; +} + +v8::Local SafeStorage::decryptStringAsync( + v8::Isolate* isolate, + v8::Local buffer) { + gin_helper::Promise promise(isolate); + v8::Local handle = promise.GetHandle(); + + if (!electron::Browser::Get()->is_ready()) { + promise.RejectWithErrorMessage( + "safeStorage cannot be used before app is ready"); + return handle; + } + + if (!node::Buffer::HasInstance(buffer)) { + promise.RejectWithErrorMessage( + "Expected the first argument of decryptStringAsync() to be a buffer"); + return handle; + } + + const char* data = node::Buffer::Data(buffer); + auto size = node::Buffer::Length(buffer); + std::string ciphertext(data, size); + + if (ciphertext.empty()) { + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + dict.Set("shouldReEncrypt", false); + dict.Set("result", ""); + promise.Resolve(dict); + return handle; + } + + if (is_available_) { + std::string plaintext; + os_crypt_async::Encryptor::DecryptFlags flags; + bool decrypted = encryptor_->DecryptString(ciphertext, &plaintext, &flags); + if (decrypted) { + auto dict = gin_helper::Dictionary::CreateEmpty(isolate); + dict.Set("shouldReEncrypt", flags.should_reencrypt); + dict.Set("result", plaintext); + promise.Resolve(dict); + } else if (flags.temporarily_unavailable) { + promise.RejectWithErrorMessage( + "safeStorage.decryptStringAsync is temporarily unavailable. " + "Please try again."); + } else { + promise.RejectWithErrorMessage( + "Error while decrypting the ciphertext provided to " + "safeStorage.decryptStringAsync."); + } + return handle; + } + + pending_decrypts_.emplace_back(std::move(promise), std::move(ciphertext)); + return handle; +} + +} // namespace electron::api void Initialize(v8::Local exports, v8::Local unused, @@ -134,13 +351,7 @@ void Initialize(v8::Local exports, void* priv) { v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate(); gin_helper::Dictionary dict(isolate, exports); - dict.SetMethod("decryptString", &DecryptString); - dict.SetMethod("encryptString", &EncryptString); -#if BUILDFLAG(IS_LINUX) - dict.SetMethod("getSelectedStorageBackend", &GetSelectedLinuxBackend); -#endif - dict.SetMethod("isEncryptionAvailable", &IsEncryptionAvailable); - dict.SetMethod("setUsePlainTextEncryption", &SetUsePasswordV10); + dict.Set("safeStorage", electron::api::SafeStorage::Create(isolate)); } NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_safe_storage, Initialize) diff --git a/shell/browser/api/electron_api_safe_storage.h b/shell/browser/api/electron_api_safe_storage.h new file mode 100644 index 0000000000..2ffe139872 --- /dev/null +++ b/shell/browser/api/electron_api_safe_storage.h @@ -0,0 +1,126 @@ +// Copyright (c) 2021 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_ +#define ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_ + +#include +#include + +#include "build/build_config.h" +#include "components/os_crypt/async/common/encryptor.h" +#include "shell/browser/browser_observer.h" +#include "shell/browser/event_emitter_mixin.h" +#include "shell/common/gin_helper/dictionary.h" +#include "shell/common/gin_helper/promise.h" +#include "shell/common/gin_helper/wrappable.h" + +namespace v8 { +class Context; +class Isolate; +class Object; +class Value; +template +class Local; +} // namespace v8 + +namespace gin { +class ObjectTemplateBuilder; +} // namespace gin + +namespace gin_helper { +template +class Handle; +} // namespace gin_helper + +namespace electron::api { + +class SafeStorage final : public gin_helper::DeprecatedWrappable, + public gin_helper::EventEmitterMixin, + private BrowserObserver { + public: + static gin_helper::Handle Create(v8::Isolate* isolate); + + // gin_helper::Wrappable + static gin::DeprecatedWrapperInfo kWrapperInfo; + gin::ObjectTemplateBuilder GetObjectTemplateBuilder( + v8::Isolate* isolate) override; + const char* GetTypeName() override; + + // disable copy + SafeStorage(const SafeStorage&) = delete; + SafeStorage& operator=(const SafeStorage&) = delete; + + protected: + explicit SafeStorage(v8::Isolate* isolate); + ~SafeStorage() override; + + private: + // BrowserObserver: + void OnFinishLaunching(base::DictValue launch_info) override; + + void OnOsCryptReady(os_crypt_async::Encryptor encryptor); + + bool IsEncryptionAvailable(); + + bool IsAsyncEncryptionAvailable(); + + void SetUsePasswordV10(bool use); + + v8::Local EncryptString(v8::Isolate* isolate, + const std::string& plaintext); + + std::string DecryptString(v8::Isolate* isolate, v8::Local buffer); + + v8::Local encryptStringAsync(v8::Isolate* isolate, + const std::string& plaintext); + + v8::Local decryptStringAsync(v8::Isolate* isolate, + v8::Local buffer); + +#if BUILDFLAG(IS_LINUX) + std::string GetSelectedLinuxBackend(); +#endif + + bool use_password_v10_ = false; + + bool is_available_ = false; + + std::optional encryptor_; + + // Pending encrypt operations waiting for encryptor to be ready. + struct PendingEncrypt { + PendingEncrypt(gin_helper::Promise> promise, + std::string plaintext); + ~PendingEncrypt(); + PendingEncrypt(PendingEncrypt&&); + PendingEncrypt& operator=(PendingEncrypt&&); + + gin_helper::Promise> promise; + std::string plaintext; + }; + std::vector pending_encrypts_; + + // Pending decrypt operations waiting for encryptor to be ready. + struct PendingDecrypt { + PendingDecrypt(gin_helper::Promise promise, + std::string ciphertext); + ~PendingDecrypt(); + PendingDecrypt(PendingDecrypt&&); + PendingDecrypt& operator=(PendingDecrypt&&); + + gin_helper::Promise promise; + std::string ciphertext; + }; + std::vector pending_decrypts_; +}; + +} // namespace electron::api + +void Initialize(v8::Local exports, + v8::Local unused, + v8::Local context, + void* priv); + +#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_ diff --git a/shell/browser/browser.cc b/shell/browser/browser.cc index 10f0609bea..52ae9e8940 100644 --- a/shell/browser/browser.cc +++ b/shell/browser/browser.cc @@ -192,9 +192,8 @@ void Browser::DidFinishLaunching(base::DictValue launch_info) { } is_ready_ = true; - if (ready_promise_) { + if (ready_promise_) ready_promise_->Resolve(); - } for (BrowserObserver& observer : observers_) observer.OnFinishLaunching(launch_info.Clone()); diff --git a/shell/browser/browser_process_impl.cc b/shell/browser/browser_process_impl.cc index 1456d43ccf..c33b9caae9 100644 --- a/shell/browser/browser_process_impl.cc +++ b/shell/browser/browser_process_impl.cc @@ -10,6 +10,7 @@ #include "base/command_line.h" #include "base/files/file_path.h" +#include "base/functional/bind.h" #include "base/notimplemented.h" #include "base/path_service.h" #include "chrome/browser/browser_process.h" @@ -46,6 +47,28 @@ #include "chrome/browser/printing/print_job_manager.h" #endif +#if BUILDFLAG(IS_LINUX) +#include "chrome/browser/browser_features.h" +#include "components/os_crypt/async/browser/freedesktop_secret_key_provider.h" +#include "components/os_crypt/async/browser/secret_portal_key_provider.h" +#include "components/password_manager/core/browser/password_manager_switches.h" // nogncheck +#include "shell/common/application_info.h" + +#endif + +#if BUILDFLAG(IS_WIN) +#include "components/os_crypt/async/browser/dpapi_key_provider.h" +#endif + +#if BUILDFLAG(IS_MAC) +#include "chrome/common/chrome_features.h" +#include "components/os_crypt/async/browser/keychain_key_provider.h" +#endif + +#if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC) +#include "components/os_crypt/async/browser/posix_key_provider.h" +#endif + BrowserProcessImpl::BrowserProcessImpl() { g_browser_process = this; } @@ -116,10 +139,16 @@ void BrowserProcessImpl::PostEarlyInitialization() { PrefServiceFactory prefs_factory; auto pref_registry = base::MakeRefCounted(); PrefProxyConfigTrackerImpl::RegisterPrefs(pref_registry.get()); + #if BUILDFLAG(IS_WIN) OSCrypt::RegisterLocalPrefs(pref_registry.get()); #endif +#if BUILDFLAG(IS_LINUX) + os_crypt_async::SecretPortalKeyProvider::RegisterLocalPrefs( + pref_registry.get()); +#endif + in_memory_pref_store_ = base::MakeRefCounted(); ApplyProxyModeFromCommandLine(in_memory_pref_store()); prefs_factory.set_command_line_prefs(in_memory_pref_store()); @@ -419,16 +448,63 @@ void BrowserProcessImpl::CreateNetworkQualityObserver() { } void BrowserProcessImpl::CreateOSCryptAsync() { - // source: https://chromium-review.googlesource.com/c/chromium/src/+/4455776 + std::vector>> + providers; - // For now, initialize OSCryptAsync with no providers. This delegates all - // encryption operations to OSCrypt. - // TODO(crbug.com/1373092): Add providers behind features, as support for them - // is added. - os_crypt_async_ = std::make_unique( - std::vector< - std::pair>>()); +#if BUILDFLAG(IS_WIN) + // The DPAPI key provider requires OSCrypt::Init to have already been called + // to initialize the key storage. This happens in + // BrowserMainPartsWin::PreCreateMainMessageLoop. + providers.emplace_back(std::make_pair( + /*precedence=*/10u, + std::make_unique(local_state()))); +#endif // BUILDFLAG(IS_WIN) - // Trigger async initialization of OSCrypt key providers. - os_crypt_async_->GetInstance(base::DoNothing()); +#if BUILDFLAG(IS_LINUX) + base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess(); + const auto password_store = + cmd_line->GetSwitchValueASCII(password_manager::kPasswordStore); + + if (base::FeatureList::IsEnabled(features::kDbusSecretPortal)) { + // Use a higher priority than the FreedesktopSecretKeyProvider. + providers.emplace_back( + /*precedence=*/15u, + std::make_unique( + local_state(), + base::FeatureList::IsEnabled( + features::kSecretPortalKeyProviderUseForEncryption))); + } + + auto freedesktop_config = + os_crypt_async::FreedesktopSecretKeyProvider::GetDefaultConfig(); + + const std::string app_name = electron::GetApplicationName(); + freedesktop_config.app_name = app_name; + freedesktop_config.kwallet_folder = app_name + " Keys"; + freedesktop_config.key_name = app_name + " Safe Storage"; + + providers.emplace_back( + /*precedence=*/10u, + std::make_unique( + password_store, electron::GetApplicationName(), freedesktop_config, + nullptr)); +#endif // BUILDFLAG(IS_LINUX) + +#if BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC) + // On other POSIX systems, this is the only key provider. On Linux, it is used + // as a fallback. + providers.emplace_back( + /*precedence=*/5u, std::make_unique()); +#endif // BUILDFLAG(IS_POSIX) && !BUILDFLAG(IS_MAC) + +#if BUILDFLAG(IS_MAC) + if (base::FeatureList::IsEnabled(features::kUseKeychainKeyProvider)) { + providers.emplace_back(std::make_pair( + /*precedence=*/10u, + std::make_unique())); + } +#endif // BUILDFLAG(IS_MAC) + + os_crypt_async_ = + std::make_unique(std::move(providers)); } diff --git a/shell/browser/electron_browser_main_parts.cc b/shell/browser/electron_browser_main_parts.cc index 11816ab9dc..834a4ca059 100644 --- a/shell/browser/electron_browser_main_parts.cc +++ b/shell/browser/electron_browser_main_parts.cc @@ -469,6 +469,8 @@ int ElectronBrowserMainParts::PreMainMessageLoopRun() { DevToolsManagerDelegate::StartHttpHandler(); } + fake_browser_process_->PreMainMessageLoopRun(); + #if !BUILDFLAG(IS_MAC) // The corresponding call in macOS is in ElectronApplicationDelegate. Browser::Get()->WillFinishLaunching(); @@ -478,8 +480,6 @@ int ElectronBrowserMainParts::PreMainMessageLoopRun() { // Notify observers that main thread message loop was initialized. Browser::Get()->PreMainMessageLoopRun(); - fake_browser_process_->PreMainMessageLoopRun(); - #if BUILDFLAG(IS_WIN) ui::SelectFileDialog::SetFactory( std::make_unique()); diff --git a/shell/browser/net/network_context_service.cc b/shell/browser/net/network_context_service.cc index 7c465c9991..9fc2a1b36a 100644 --- a/shell/browser/net/network_context_service.cc +++ b/shell/browser/net/network_context_service.cc @@ -14,6 +14,7 @@ #include "net/http/http_util.h" #include "net/net_buildflags.h" #include "services/network/network_service.h" +#include "services/network/public/cpp/cookie_encryption_provider_impl.h" #include "services/network/public/cpp/cors/origin_access_list.h" #include "shell/browser/browser_process_impl.h" #include "shell/browser/electron_browser_client.h" @@ -114,6 +115,18 @@ void NetworkContextService::ConfigureNetworkContextParams( network_context_params->enable_encrypted_cookies = electron::fuses::IsCookieEncryptionEnabled(); + // If cookie encryption is enabled, we need to provide a cookie encryption + // provider for the network service to use. + if (network_context_params->enable_encrypted_cookies) { + if (!cookie_encryption_provider_) { + cookie_encryption_provider_ = + std::make_unique( + g_browser_process->os_crypt_async()); + } + network_context_params->cookie_encryption_provider = + cookie_encryption_provider_->BindNewRemote(); + } + network_context_params->file_paths->transport_security_persister_file_name = base::FilePath(chrome::kTransportSecurityPersisterFilename); } diff --git a/shell/browser/net/network_context_service.h b/shell/browser/net/network_context_service.h index 42efd6c212..1c3e4f9bea 100644 --- a/shell/browser/net/network_context_service.h +++ b/shell/browser/net/network_context_service.h @@ -5,12 +5,16 @@ #ifndef ELECTRON_SHELL_BROWSER_NET_NETWORK_CONTEXT_SERVICE_H_ #define ELECTRON_SHELL_BROWSER_NET_NETWORK_CONTEXT_SERVICE_H_ +#include + #include "base/memory/raw_ptr.h" #include "chrome/browser/net/proxy_config_monitor.h" #include "components/keyed_service/core/keyed_service.h" #include "services/cert_verifier/public/mojom/cert_verifier_service_factory.mojom-forward.h" #include "services/network/public/mojom/network_context.mojom-forward.h" +class CookieEncryptionProviderImpl; + namespace base { class FilePath; } // namespace base @@ -46,6 +50,7 @@ class NetworkContextService : public KeyedService { raw_ptr browser_context_; ProxyConfigMonitor proxy_config_monitor_; + std::unique_ptr cookie_encryption_provider_; }; } // namespace electron diff --git a/shell/browser/net/system_network_context_manager.cc b/shell/browser/net/system_network_context_manager.cc index 59594726ed..af3583e99d 100644 --- a/shell/browser/net/system_network_context_manager.cc +++ b/shell/browser/net/system_network_context_manager.cc @@ -280,7 +280,12 @@ void SystemNetworkContextManager::OnNetworkServiceCreated( // process, send it the required key. if (content::IsOutOfProcessNetworkService() && electron::fuses::IsCookieEncryptionEnabled()) { + // On Windows, OSCrypt Async manages the encryption key via the DPAPI key + // provider, and there is no need to send the key separately to OSCrypt + // sync. +#if !BUILDFLAG(IS_WIN) network_service->SetEncryptionKey(OSCrypt::GetRawEncryptionKey()); +#endif } } diff --git a/spec/api-safe-storage-spec.ts b/spec/api-safe-storage-spec.ts index b8d9c1358a..7e3994d24a 100644 --- a/spec/api-safe-storage-spec.ts +++ b/spec/api-safe-storage-spec.ts @@ -1,6 +1,8 @@ import { safeStorage } from 'electron/main'; +import * as chai from 'chai'; import { expect } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; import * as cp from 'node:child_process'; import { once } from 'node:events'; @@ -9,23 +11,7 @@ import * as path from 'node:path'; import { ifdescribe } from './lib/spec-helpers'; -describe('safeStorage module', () => { - it('safeStorage before and after app is ready', async () => { - const appPath = path.join(__dirname, 'fixtures', 'crash-cases', 'safe-storage'); - const appProcess = cp.spawn(process.execPath, [appPath]); - - let output = ''; - appProcess.stdout.on('data', data => { output += data; }); - appProcess.stderr.on('data', data => { output += data; }); - - const code = (await once(appProcess, 'exit'))[0] ?? 1; - - if (code !== 0 && output) { - console.log(output); - } - expect(code).to.equal(0); - }); -}); +chai.use(chaiAsPromised); describe('safeStorage module', () => { before(() => { @@ -94,6 +80,133 @@ describe('safeStorage module', () => { }); }); + describe('SafeStorage.isAsyncEncryptionAvailable()', () => { + it('should return true when async encryption is available', () => { + expect(safeStorage.isAsyncEncryptionAvailable()).to.equal(true); + }); + }); + + describe('SafeStorage.encryptStringAsync()', () => { + it('should return a promise', () => { + const result = safeStorage.encryptStringAsync('plaintext'); + expect(result).to.be.a('promise'); + }); + + it('valid input should correctly encrypt string', async () => { + const plaintext = 'plaintext'; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + expect(Buffer.isBuffer(encrypted)).to.equal(true); + }); + + it('UTF-16 characters can be encrypted', async () => { + const plaintext = '€ - utf symbol'; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + expect(Buffer.isBuffer(encrypted)).to.equal(true); + }); + + it('empty string can be encrypted', async () => { + const plaintext = ''; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + expect(Buffer.isBuffer(encrypted)).to.equal(true); + }); + + it('long strings can be encrypted', async () => { + const plaintext = 'a'.repeat(10000); + const encrypted = await safeStorage.encryptStringAsync(plaintext); + expect(Buffer.isBuffer(encrypted)).to.equal(true); + }); + + it('special characters can be encrypted', async () => { + const plaintext = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\`~\n\t\r'; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + expect(Buffer.isBuffer(encrypted)).to.equal(true); + }); + }); + + describe('SafeStorage.decryptStringAsync()', () => { + it('should return a promise', () => { + const encrypted = safeStorage.encryptString('plaintext'); + const result = safeStorage.decryptStringAsync(encrypted); + expect(result).to.be.a('promise'); + }); + + it('valid input should correctly decrypt string', async () => { + const encrypted = await safeStorage.encryptStringAsync('plaintext'); + const decryptResult = await safeStorage.decryptStringAsync(encrypted); + expect(decryptResult).to.have.property('result'); + expect(decryptResult).to.have.property('shouldReEncrypt'); + expect(decryptResult.result).to.equal('plaintext'); + expect(decryptResult.shouldReEncrypt).to.be.a('boolean'); + }); + + it('UTF-16 characters can be decrypted', async () => { + const plaintext = '€ - utf symbol'; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + const decryptResult = await safeStorage.decryptStringAsync(encrypted); + expect(decryptResult.result).to.equal(plaintext); + }); + + it('empty string can be decrypted', async () => { + const plaintext = ''; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + const decryptResult = await safeStorage.decryptStringAsync(encrypted); + expect(decryptResult.result).to.equal(plaintext); + }); + + it('long strings can be decrypted', async () => { + const plaintext = 'a'.repeat(10000); + const encrypted = await safeStorage.encryptStringAsync(plaintext); + const decryptResult = await safeStorage.decryptStringAsync(encrypted); + expect(decryptResult.result).to.equal(plaintext); + }); + + it('special characters can be decrypted', async () => { + const plaintext = '!@#$%^&*()_+-=[]{}|;:\'",.<>?/\\`~\n\t\r'; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + const decryptResult = await safeStorage.decryptStringAsync(encrypted); + expect(decryptResult.result).to.equal(plaintext); + }); + + it('unencrypted input should reject', async () => { + const plaintextBuffer = Buffer.from('I am unencoded!', 'utf-8'); + await expect(safeStorage.decryptStringAsync(plaintextBuffer)).to.be.rejectedWith(Error); + }); + + it('non-buffer input should reject', async () => { + const notABuffer = {} as any; + await expect(safeStorage.decryptStringAsync(notABuffer)).to.be.rejectedWith(Error); + }); + + it('can decrypt data encrypted with sync method', async () => { + const plaintext = 'sync-to-async test'; + const encrypted = safeStorage.encryptString(plaintext); + const decryptResult = await safeStorage.decryptStringAsync(encrypted); + expect(decryptResult.result).to.equal(plaintext); + }); + }); + + describe('SafeStorage sync and async interoperability', () => { + it('sync decrypt can handle async encrypted data', async () => { + const plaintext = 'async-to-sync test'; + const encrypted = await safeStorage.encryptStringAsync(plaintext); + const decrypted = safeStorage.decryptString(encrypted); + expect(decrypted).to.equal(plaintext); + }); + + it('multiple concurrent async operations work correctly', async () => { + const plaintexts = ['text1', 'text2', 'text3', 'text4', 'text5']; + + const encryptPromises = plaintexts.map(pt => safeStorage.encryptStringAsync(pt)); + const encryptedBuffers = await Promise.all(encryptPromises); + + const decryptPromises = encryptedBuffers.map(buf => safeStorage.decryptStringAsync(buf)); + const decryptResults = await Promise.all(decryptPromises); + const decryptedTexts = decryptResults.map(result => result.result); + + expect(decryptedTexts).to.deep.equal(plaintexts); + }); + }); + describe('safeStorage persists encryption key across app relaunch', () => { it('can decrypt after closing and reopening app', async () => { const fixturesPath = path.resolve(__dirname, 'fixtures'); diff --git a/spec/fixtures/crash-cases/safe-storage/index.js b/spec/fixtures/crash-cases/safe-storage/index.js index 08bdede4da..8418cef674 100644 --- a/spec/fixtures/crash-cases/safe-storage/index.js +++ b/spec/fixtures/crash-cases/safe-storage/index.js @@ -4,25 +4,14 @@ const { expect } = require('chai'); (async () => { if (!app.isReady()) { - // isEncryptionAvailable() returns false before the app is ready on - // Linux: https://github.com/electron/electron/issues/32206 - // and - // Windows: https://github.com/electron/electron/issues/33640. - expect(safeStorage.isEncryptionAvailable()).to.equal(process.platform === 'darwin'); - if (safeStorage.isEncryptionAvailable()) { - const plaintext = 'plaintext'; - const ciphertext = safeStorage.encryptString(plaintext); - expect(Buffer.isBuffer(ciphertext)).to.equal(true); - expect(safeStorage.decryptString(ciphertext)).to.equal(plaintext); - } else { - expect(() => safeStorage.encryptString('plaintext')).to.throw(/safeStorage cannot be used before app is ready/); - expect(() => safeStorage.decryptString(Buffer.from(''))).to.throw(/safeStorage cannot be used before app is ready/); - } + expect(safeStorage.isEncryptionAvailable()).to.equal(false); + + expect(() => safeStorage.encryptString('plaintext')).to.throw(/safeStorage cannot be used before app is ready/); + expect(() => safeStorage.decryptString(Buffer.from(''))).to.throw(/safeStorage cannot be used before app is ready/); } + await app.whenReady(); - // isEncryptionAvailable() will always return false on CI due to a mocked - // dbus as mentioned above. - expect(safeStorage.isEncryptionAvailable()).to.equal(process.platform !== 'linux'); + if (safeStorage.isEncryptionAvailable()) { const plaintext = 'plaintext'; const ciphertext = safeStorage.encryptString(plaintext); diff --git a/spec/fuses-spec.ts b/spec/fuses-spec.ts index 29409f7cc0..63b1c057ea 100644 --- a/spec/fuses-spec.ts +++ b/spec/fuses-spec.ts @@ -35,4 +35,80 @@ describe('fuses', () => { return await bw.webContents.executeJavaScript("ajax('file:///etc/passwd')"); }, path.join(__dirname, 'fixtures', 'pages', 'fetch.html'))).to.eventually.be.rejectedWith('Failed to fetch'); }); + + describe('cookie_encryption', () => { + it('allows setting and retrieving cookies when enabled', async () => { + const rc = await startRemoteControlApp(['--set-fuse-cookie_encryption=1']); + const result = await rc.remotely(async () => { + const { session } = require('electron'); + const ses = session.defaultSession; + const testUrl = 'https://example.com'; + + await ses.clearStorageData({ storages: ['cookies'] }); + + await ses.cookies.set({ + url: testUrl, + name: 'test_cookie', + value: 'encrypted_value_12345', + expirationDate: Math.floor(Date.now() / 1000) + 3600 + }); + + await ses.cookies.set({ + url: testUrl, + name: 'secure_cookie', + value: 'secret_data_67890', + secure: true, + httpOnly: true, + expirationDate: Math.floor(Date.now() / 1000) + 7200 + }); + + const cookies = await ses.cookies.get({ url: testUrl }); + const testCookie = cookies.find((c: Electron.Cookie) => c.name === 'test_cookie'); + const secureCookie = cookies.find((c: Electron.Cookie) => c.name === 'secure_cookie'); + + return { + cookieCount: cookies.length, + testCookieValue: testCookie?.value, + secureCookieValue: secureCookie?.value, + secureCookieIsSecure: secureCookie?.secure, + secureCookieIsHttpOnly: secureCookie?.httpOnly + }; + }); + + expect(result.cookieCount).to.equal(2); + expect(result.testCookieValue).to.equal('encrypted_value_12345'); + expect(result.secureCookieValue).to.equal('secret_data_67890'); + expect(result.secureCookieIsSecure).to.be.true(); + expect(result.secureCookieIsHttpOnly).to.be.true(); + }); + + it('persists cookies across sessions when enabled', async () => { + const rc = await startRemoteControlApp(['--set-fuse-cookie_encryption=1']); + + await rc.remotely(async () => { + const { session } = require('electron'); + await session.defaultSession.clearStorageData({ storages: ['cookies'] }); + await session.defaultSession.cookies.set({ + url: 'https://example.com', + name: 'persistent_cookie', + value: 'persist_me', + expirationDate: Math.floor(Date.now() / 1000) + 86400 + }); + }); + + await rc.remotely(async () => { + const { session } = require('electron'); + await session.defaultSession.cookies.flushStore(); + }); + + const result = await rc.remotely(async () => { + const { session } = require('electron'); + const cookies = await session.defaultSession.cookies.get({ url: 'https://example.com' }); + const cookie = cookies.find((c: Electron.Cookie) => c.name === 'persistent_cookie'); + return cookie?.value; + }); + + expect(result).to.equal('persist_me'); + }); + }); });