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
This commit is contained in:
Shelley Vohr
2026-02-15 19:54:50 +01:00
committed by GitHub
parent dcdbb0397e
commit eb29568e45
19 changed files with 970 additions and 875 deletions

View File

@@ -4,13 +4,19 @@
#include <string>
#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<v8::Local<v8::Value>> 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<gin_helper::Dictionary> 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> 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<SafeStorage>::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<BrowserProcessImpl*>(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<BrowserProcessImpl*>(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<BrowserProcessImpl*>(g_browser_process)
@@ -49,8 +180,8 @@ std::string GetSelectedLinuxBackend() {
}
#endif
v8::Local<v8::Value> EncryptString(v8::Isolate* isolate,
const std::string& plaintext) {
v8::Local<v8::Value> 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<v8::Value> EncryptString(v8::Isolate* isolate,
return electron::Buffer::Copy(isolate, ciphertext).ToLocalChecked();
}
std::string DecryptString(v8::Isolate* isolate, v8::Local<v8::Value> buffer) {
std::string SafeStorage::DecryptString(v8::Isolate* isolate,
v8::Local<v8::Value> 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<v8::Value> buffer) {
return plaintext;
}
} // namespace
v8::Local<v8::Promise> SafeStorage::encryptStringAsync(
v8::Isolate* isolate,
const std::string& plaintext) {
gin_helper::Promise<v8::Local<v8::Value>> promise(isolate);
v8::Local<v8::Promise> 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<v8::Promise> SafeStorage::decryptStringAsync(
v8::Isolate* isolate,
v8::Local<v8::Value> buffer) {
gin_helper::Promise<gin_helper::Dictionary> promise(isolate);
v8::Local<v8::Promise> 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<v8::Object> exports,
v8::Local<v8::Value> unused,
@@ -134,13 +351,7 @@ void Initialize(v8::Local<v8::Object> 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)

View File

@@ -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 <string>
#include <vector>
#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 T>
class Local;
} // namespace v8
namespace gin {
class ObjectTemplateBuilder;
} // namespace gin
namespace gin_helper {
template <typename T>
class Handle;
} // namespace gin_helper
namespace electron::api {
class SafeStorage final : public gin_helper::DeprecatedWrappable<SafeStorage>,
public gin_helper::EventEmitterMixin<SafeStorage>,
private BrowserObserver {
public:
static gin_helper::Handle<SafeStorage> 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<v8::Value> EncryptString(v8::Isolate* isolate,
const std::string& plaintext);
std::string DecryptString(v8::Isolate* isolate, v8::Local<v8::Value> buffer);
v8::Local<v8::Promise> encryptStringAsync(v8::Isolate* isolate,
const std::string& plaintext);
v8::Local<v8::Promise> decryptStringAsync(v8::Isolate* isolate,
v8::Local<v8::Value> buffer);
#if BUILDFLAG(IS_LINUX)
std::string GetSelectedLinuxBackend();
#endif
bool use_password_v10_ = false;
bool is_available_ = false;
std::optional<os_crypt_async::Encryptor> encryptor_;
// Pending encrypt operations waiting for encryptor to be ready.
struct PendingEncrypt {
PendingEncrypt(gin_helper::Promise<v8::Local<v8::Value>> promise,
std::string plaintext);
~PendingEncrypt();
PendingEncrypt(PendingEncrypt&&);
PendingEncrypt& operator=(PendingEncrypt&&);
gin_helper::Promise<v8::Local<v8::Value>> promise;
std::string plaintext;
};
std::vector<PendingEncrypt> pending_encrypts_;
// Pending decrypt operations waiting for encryptor to be ready.
struct PendingDecrypt {
PendingDecrypt(gin_helper::Promise<gin_helper::Dictionary> promise,
std::string ciphertext);
~PendingDecrypt();
PendingDecrypt(PendingDecrypt&&);
PendingDecrypt& operator=(PendingDecrypt&&);
gin_helper::Promise<gin_helper::Dictionary> promise;
std::string ciphertext;
};
std::vector<PendingDecrypt> pending_decrypts_;
};
} // namespace electron::api
void Initialize(v8::Local<v8::Object> exports,
v8::Local<v8::Value> unused,
v8::Local<v8::Context> context,
void* priv);
#endif // ELECTRON_SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_