Files
electron/shell/browser/api/electron_api_safe_storage.cc
Samuel Attard dbcf0fb5f0 fix: lazily initialize safeStorage async encryptor (#50419)
* fix: lazily initialize safeStorage async encryptor

The SafeStorage constructor previously registered a browser observer that
called os_crypt_async()->GetInstance() on app-ready. Because ESM named
imports (import { x } from 'electron') eagerly evaluate all electron
module getters, simply importing electron in an ESM entrypoint would
construct SafeStorage and touch the OS keychain on app-ready, even when
safeStorage was never used.

This showed up as a macOS CI hang: the esm-spec import-meta fixture
triggers a keychain access prompt that blocks the test runner until
timeout.

Now the async encryptor is requested lazily on the first call to
encryptStringAsync, decryptStringAsync, or isAsyncEncryptionAvailable.
isAsyncEncryptionAvailable now returns a Promise that resolves once
initialization completes, matching what the docs already stated.

* chore: lint

* fix: add HandleScope in OnOsCryptReady for pending operations

OnOsCryptReady fires asynchronously from a posted task without an active
V8 HandleScope. Previously this was harmless because eager init meant the
pending queues were always empty when it fired. With lazy init, operations
queue up first, then the callback processes them and needs to create V8
handles (Buffer::Copy, Dictionary::CreateEmpty, Promise::Resolve).
2026-03-23 10:47:14 -07:00

381 lines
12 KiB
C++

// 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.
#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/handle.h"
#include "shell/common/node_includes.h"
#include "shell/common/node_util.h"
namespace {
const char* kEncryptionVersionPrefixV10 = "v10";
const char* kEncryptionVersionPrefixV11 = "v11";
} // 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) {}
SafeStorage::~SafeStorage() = default;
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)
.SetMethod("getSelectedStorageBackend",
&SafeStorage::GetSelectedLinuxBackend)
#endif
;
}
void SafeStorage::EnsureAsyncEncryptorRequested() {
DCHECK(electron::Browser::Get()->is_ready());
if (encryptor_requested_)
return;
encryptor_requested_ = true;
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;
// This callback may fire from a posted task without an active V8 scope.
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope handle_scope(isolate);
for (auto& pending : pending_availability_checks_) {
pending.Resolve(true);
}
pending_availability_checks_.clear();
for (auto& pending : pending_encrypts_) {
std::string ciphertext;
bool encrypted = encryptor_->EncryptString(pending.plaintext, &ciphertext);
if (encrypted) {
pending.promise.Resolve(
electron::Buffer::Copy(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) {
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_ &&
static_cast<BrowserProcessImpl*>(g_browser_process)
->linux_storage_backend() == "basic_text");
#else
return OSCrypt::IsEncryptionAvailable();
#endif
}
v8::Local<v8::Promise> SafeStorage::IsAsyncEncryptionAvailable(
v8::Isolate* isolate) {
gin_helper::Promise<bool> promise(isolate);
v8::Local<v8::Promise> handle = promise.GetHandle();
if (!electron::Browser::Get()->is_ready()) {
promise.Resolve(false);
return handle;
}
#if BUILDFLAG(IS_LINUX)
if (use_password_v10_ && static_cast<BrowserProcessImpl*>(g_browser_process)
->linux_storage_backend() == "basic_text") {
promise.Resolve(true);
return handle;
}
#endif
EnsureAsyncEncryptorRequested();
if (is_available_) {
promise.Resolve(true);
return handle;
}
pending_availability_checks_.push_back(std::move(promise));
return handle;
}
void SafeStorage::SetUsePasswordV10(bool use) {
use_password_v10_ = use;
}
#if BUILDFLAG(IS_LINUX)
std::string SafeStorage::GetSelectedLinuxBackend() {
if (!electron::Browser::Get()->is_ready())
return "unknown";
return static_cast<BrowserProcessImpl*>(g_browser_process)
->linux_storage_backend();
}
#endif
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(
"safeStorage cannot be used before app is ready");
return {};
}
gin_helper::ErrorThrower(isolate).ThrowError(
"Error while encrypting the text provided to "
"safeStorage.encryptString. "
"Encryption is not available.");
return {};
}
std::string ciphertext;
bool encrypted = OSCrypt::EncryptString(plaintext, &ciphertext);
if (!encrypted) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Error while encrypting the text provided to "
"safeStorage.encryptString.");
return {};
}
return electron::Buffer::Copy(isolate, ciphertext).ToLocalChecked();
}
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(
"safeStorage cannot be used before app is ready");
return "";
}
gin_helper::ErrorThrower(isolate).ThrowError(
"Error while decrypting the ciphertext provided to "
"safeStorage.decryptString. "
"Decryption is not available.");
return "";
}
if (!node::Buffer::HasInstance(buffer)) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Expected the first argument of decryptString() to be a buffer");
return "";
}
// ensures an error is thrown in Mac or Linux on
// decryption failure, rather than failing silently
const char* data = node::Buffer::Data(buffer);
auto size = node::Buffer::Length(buffer);
std::string ciphertext(data, size);
if (ciphertext.empty()) {
return "";
}
if (ciphertext.find(kEncryptionVersionPrefixV10) != 0 &&
ciphertext.find(kEncryptionVersionPrefixV11) != 0) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Error while decrypting the ciphertext provided to "
"safeStorage.decryptString. "
"Ciphertext does not appear to be encrypted.");
return "";
}
std::string plaintext;
bool decrypted = OSCrypt::DecryptString(ciphertext, &plaintext);
if (!decrypted) {
gin_helper::ErrorThrower(isolate).ThrowError(
"Error while decrypting the ciphertext provided to "
"safeStorage.decryptString.");
return "";
}
return plaintext;
}
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;
}
EnsureAsyncEncryptorRequested();
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;
}
EnsureAsyncEncryptorRequested();
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,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* const isolate = electron::JavascriptEnvironment::GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.Set("safeStorage", electron::api::SafeStorage::Create(isolate));
}
NODE_LINKED_BINDING_CONTEXT_AWARE(electron_browser_safe_storage, Initialize)