Files
electron/shell/browser/api/electron_api_safe_storage.h
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

130 lines
3.9 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.
#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/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> {
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:
// Lazily request the async encryptor on first use. ESM named imports
// eagerly evaluate all electron module getters, so requesting in the
// constructor would touch the OS keychain even when safeStorage is unused.
void EnsureAsyncEncryptorRequested();
void OnOsCryptReady(os_crypt_async::Encryptor encryptor);
bool IsEncryptionAvailable();
v8::Local<v8::Promise> IsAsyncEncryptionAvailable(v8::Isolate* isolate);
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 encryptor_requested_ = 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_;
std::vector<gin_helper::Promise<bool>> pending_availability_checks_;
};
} // 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_