feat: add electron.safeStorage
encryption API (#30020)
* feat: add SafeStorage api; first commit * chore: rename files to fit semantically * chore: add linkedBindings * chore: fix function signatures * chore: refactor eisCookieEncryptionEnabled() fuse * chore: create test file * chore: add tests and documentation * chore: add copyright and lint * chore: add additional tests * chore: fix constructor * chore: commit for pair programming * wip: commit for keeley pairing * chore: docs change and code cleanup * chore: add linux import * chore: add description to documentation * chore: fixing tests * chore: modify behaviour to not allow unencrypted strings as decyption input * fix add patch for enabling default v11 encryption on Linux * chore: remove file after each test * chore: fix patch * chore: remove chromium patch * chore: add linux specific tests * chore: fix path * chore: add checker for linuux file deletion * chore: add dcheck back * chore: remove reference to headless mode * chore: remove tests for linux * chore: edit commit message * chore: refactor safeStorage to not be a class * chore: remove static variable from header * chore: spec file remove settimeout Co-authored-by: VerteDinde <keeleymhammond@gmail.com>
This commit is contained in:
parent
ec6cd0053e
commit
bc508c6113
17 changed files with 393 additions and 46 deletions
40
docs/api/safe-storage.md
Normal file
40
docs/api/safe-storage.md
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
# safeStorage
|
||||||
|
|
||||||
|
> Allows access to simple encryption and decryption of strings for storage on the local machine.
|
||||||
|
|
||||||
|
Process: [Main](../glossary.md#main-process)
|
||||||
|
|
||||||
|
This module protects data stored on disk from being accessed by other applications or users with full disk access.
|
||||||
|
|
||||||
|
Note that on Mac, 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.
|
||||||
|
|
||||||
|
## Methods
|
||||||
|
|
||||||
|
The `safeStorage` module has the following methods:
|
||||||
|
|
||||||
|
### `safeStorage.isEncryptionAvailable()`
|
||||||
|
|
||||||
|
Returns `Boolean` - Whether encryption is available.
|
||||||
|
|
||||||
|
On Linux, returns true if the secret key is
|
||||||
|
available. On MacOS, returns true if Keychain is available.
|
||||||
|
On Windows, returns true with no other preconditions.
|
||||||
|
|
||||||
|
### `safeStorage.encryptString(plainText)`
|
||||||
|
|
||||||
|
* `plainText` String
|
||||||
|
|
||||||
|
Returns `Buffer` - An array of bytes representing the encrypted string.
|
||||||
|
|
||||||
|
This function will throw an error if encryption fails.
|
||||||
|
|
||||||
|
### `safeStorage.decryptString(encrypted)`
|
||||||
|
|
||||||
|
* `encrypted` Buffer
|
||||||
|
|
||||||
|
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.
|
|
@ -42,6 +42,7 @@ auto_filenames = {
|
||||||
"docs/api/power-save-blocker.md",
|
"docs/api/power-save-blocker.md",
|
||||||
"docs/api/process.md",
|
"docs/api/process.md",
|
||||||
"docs/api/protocol.md",
|
"docs/api/protocol.md",
|
||||||
|
"docs/api/safe-storage.md",
|
||||||
"docs/api/screen.md",
|
"docs/api/screen.md",
|
||||||
"docs/api/service-workers.md",
|
"docs/api/service-workers.md",
|
||||||
"docs/api/session.md",
|
"docs/api/session.md",
|
||||||
|
@ -213,6 +214,7 @@ auto_filenames = {
|
||||||
"lib/browser/api/power-monitor.ts",
|
"lib/browser/api/power-monitor.ts",
|
||||||
"lib/browser/api/power-save-blocker.ts",
|
"lib/browser/api/power-save-blocker.ts",
|
||||||
"lib/browser/api/protocol.ts",
|
"lib/browser/api/protocol.ts",
|
||||||
|
"lib/browser/api/safe-storage.ts",
|
||||||
"lib/browser/api/screen.ts",
|
"lib/browser/api/screen.ts",
|
||||||
"lib/browser/api/session.ts",
|
"lib/browser/api/session.ts",
|
||||||
"lib/browser/api/share-menu.ts",
|
"lib/browser/api/share-menu.ts",
|
||||||
|
|
|
@ -288,6 +288,8 @@ filenames = {
|
||||||
"shell/browser/api/electron_api_printing.cc",
|
"shell/browser/api/electron_api_printing.cc",
|
||||||
"shell/browser/api/electron_api_protocol.cc",
|
"shell/browser/api/electron_api_protocol.cc",
|
||||||
"shell/browser/api/electron_api_protocol.h",
|
"shell/browser/api/electron_api_protocol.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.cc",
|
||||||
"shell/browser/api/electron_api_screen.h",
|
"shell/browser/api/electron_api_screen.h",
|
||||||
"shell/browser/api/electron_api_service_worker_context.cc",
|
"shell/browser/api/electron_api_service_worker_context.cc",
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
|
||||||
{ name: 'powerMonitor', loader: () => require('./power-monitor') },
|
{ name: 'powerMonitor', loader: () => require('./power-monitor') },
|
||||||
{ name: 'powerSaveBlocker', loader: () => require('./power-save-blocker') },
|
{ name: 'powerSaveBlocker', loader: () => require('./power-save-blocker') },
|
||||||
{ name: 'protocol', loader: () => require('./protocol') },
|
{ name: 'protocol', loader: () => require('./protocol') },
|
||||||
|
{ name: 'safeStorage', loader: () => require('./safe-storage') },
|
||||||
{ name: 'screen', loader: () => require('./screen') },
|
{ name: 'screen', loader: () => require('./screen') },
|
||||||
{ name: 'session', loader: () => require('./session') },
|
{ name: 'session', loader: () => require('./session') },
|
||||||
{ name: 'ShareMenu', loader: () => require('./share-menu') },
|
{ name: 'ShareMenu', loader: () => require('./share-menu') },
|
||||||
|
|
3
lib/browser/api/safe-storage.ts
Normal file
3
lib/browser/api/safe-storage.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
const safeStorage = process._linkedBinding('electron_browser_safe_storage');
|
||||||
|
|
||||||
|
module.exports = safeStorage;
|
121
shell/browser/api/electron_api_safe_storage.cc
Normal file
121
shell/browser/api/electron_api_safe_storage.cc
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// 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 "shell/browser/api/electron_api_safe_storage.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "components/os_crypt/os_crypt.h"
|
||||||
|
#include "shell/browser/browser.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/node_includes.h"
|
||||||
|
#include "shell/common/platform_util.h"
|
||||||
|
|
||||||
|
namespace electron {
|
||||||
|
|
||||||
|
namespace safestorage {
|
||||||
|
|
||||||
|
static const char* kEncryptionVersionPrefixV10 = "v10";
|
||||||
|
static const char* kEncryptionVersionPrefixV11 = "v11";
|
||||||
|
|
||||||
|
#if DCHECK_IS_ON()
|
||||||
|
static bool electron_crypto_ready = false;
|
||||||
|
|
||||||
|
void SetElectronCryptoReady(bool ready) {
|
||||||
|
electron_crypto_ready = ready;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool IsEncryptionAvailable() {
|
||||||
|
return OSCrypt::IsEncryptionAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
v8::Local<v8::Value> EncryptString(v8::Isolate* isolate,
|
||||||
|
const std::string& plaintext) {
|
||||||
|
if (!OSCrypt::IsEncryptionAvailable()) {
|
||||||
|
gin_helper::ErrorThrower(isolate).ThrowError(
|
||||||
|
"Error while decrypting the ciphertext provided to "
|
||||||
|
"safeStorage.decryptString. "
|
||||||
|
"Encryption is not available.");
|
||||||
|
return v8::Local<v8::Value>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 v8::Local<v8::Value>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return node::Buffer::Copy(isolate, ciphertext.c_str(), ciphertext.size())
|
||||||
|
.ToLocalChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DecryptString(v8::Isolate* isolate, v8::Local<v8::Value> buffer) {
|
||||||
|
if (!OSCrypt::IsEncryptionAvailable()) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace safestorage
|
||||||
|
|
||||||
|
} // namespace electron
|
||||||
|
|
||||||
|
void Initialize(v8::Local<v8::Object> exports,
|
||||||
|
v8::Local<v8::Value> unused,
|
||||||
|
v8::Local<v8::Context> context,
|
||||||
|
void* priv) {
|
||||||
|
v8::Isolate* isolate = context->GetIsolate();
|
||||||
|
gin_helper::Dictionary dict(isolate, exports);
|
||||||
|
dict.SetMethod("isEncryptionAvailable",
|
||||||
|
&electron::safestorage::IsEncryptionAvailable);
|
||||||
|
dict.SetMethod("encryptString", &electron::safestorage::EncryptString);
|
||||||
|
dict.SetMethod("decryptString", &electron::safestorage::DecryptString);
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_LINKED_MODULE_CONTEXT_AWARE(electron_browser_safe_storage, Initialize)
|
25
shell/browser/api/electron_api_safe_storage.h
Normal file
25
shell/browser/api/electron_api_safe_storage.h
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// 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 SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_
|
||||||
|
#define SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_
|
||||||
|
|
||||||
|
#include "base/dcheck_is_on.h"
|
||||||
|
|
||||||
|
namespace electron {
|
||||||
|
|
||||||
|
namespace safestorage {
|
||||||
|
|
||||||
|
// Used in a DCHECK to validate that our assumption that the network context
|
||||||
|
// manager has initialized before app ready holds true. Only used in the
|
||||||
|
// testing build
|
||||||
|
#if DCHECK_IS_ON()
|
||||||
|
void SetElectronCryptoReady(bool ready);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // namespace safestorage
|
||||||
|
|
||||||
|
} // namespace electron
|
||||||
|
|
||||||
|
#endif // SHELL_BROWSER_API_ELECTRON_API_SAFE_STORAGE_H_
|
|
@ -103,20 +103,14 @@ void BrowserProcessImpl::PostEarlyInitialization() {
|
||||||
|
|
||||||
// Only use a persistent prefs store when cookie encryption is enabled as that
|
// Only use a persistent prefs store when cookie encryption is enabled as that
|
||||||
// is the only key that needs it
|
// is the only key that needs it
|
||||||
if (electron::fuses::IsCookieEncryptionEnabled()) {
|
base::FilePath prefs_path;
|
||||||
base::FilePath prefs_path;
|
CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &prefs_path));
|
||||||
CHECK(base::PathService::Get(chrome::DIR_USER_DATA, &prefs_path));
|
prefs_path = prefs_path.Append(FILE_PATH_LITERAL("Local State"));
|
||||||
prefs_path = prefs_path.Append(FILE_PATH_LITERAL("Local State"));
|
base::ThreadRestrictions::ScopedAllowIO allow_io;
|
||||||
base::ThreadRestrictions::ScopedAllowIO allow_io;
|
scoped_refptr<JsonPrefStore> user_pref_store =
|
||||||
scoped_refptr<JsonPrefStore> user_pref_store =
|
base::MakeRefCounted<JsonPrefStore>(prefs_path);
|
||||||
base::MakeRefCounted<JsonPrefStore>(prefs_path);
|
user_pref_store->ReadPrefs();
|
||||||
user_pref_store->ReadPrefs();
|
prefs_factory.set_user_prefs(user_pref_store);
|
||||||
prefs_factory.set_user_prefs(user_pref_store);
|
|
||||||
} else {
|
|
||||||
auto user_pref_store =
|
|
||||||
base::MakeRefCounted<OverlayUserPrefStore>(new InMemoryPrefStore);
|
|
||||||
prefs_factory.set_user_prefs(user_pref_store);
|
|
||||||
}
|
|
||||||
local_state_ = prefs_factory.Create(std::move(pref_registry));
|
local_state_ = prefs_factory.Create(std::move(pref_registry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -536,13 +536,11 @@ void ElectronBrowserMainParts::PreCreateMainMessageLoopCommon() {
|
||||||
media::SetLocalizedStringProvider(MediaStringProvider);
|
media::SetLocalizedStringProvider(MediaStringProvider);
|
||||||
|
|
||||||
#if defined(OS_WIN)
|
#if defined(OS_WIN)
|
||||||
if (electron::fuses::IsCookieEncryptionEnabled()) {
|
auto* local_state = g_browser_process->local_state();
|
||||||
auto* local_state = g_browser_process->local_state();
|
DCHECK(local_state);
|
||||||
DCHECK(local_state);
|
|
||||||
|
|
||||||
bool os_crypt_init = OSCrypt::Init(local_state);
|
bool os_crypt_init = OSCrypt::Init(local_state);
|
||||||
DCHECK(os_crypt_init);
|
DCHECK(os_crypt_init);
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
#include "services/network/public/cpp/features.h"
|
#include "services/network/public/cpp/features.h"
|
||||||
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
#include "services/network/public/cpp/shared_url_loader_factory.h"
|
||||||
#include "services/network/public/mojom/network_context.mojom.h"
|
#include "services/network/public/mojom/network_context.mojom.h"
|
||||||
|
#include "shell/browser/api/electron_api_safe_storage.h"
|
||||||
#include "shell/browser/browser.h"
|
#include "shell/browser/browser.h"
|
||||||
#include "shell/browser/electron_browser_client.h"
|
#include "shell/browser/electron_browser_client.h"
|
||||||
#include "shell/common/application_info.h"
|
#include "shell/common/application_info.h"
|
||||||
|
@ -39,6 +40,10 @@
|
||||||
#include "components/os_crypt/keychain_password_mac.h"
|
#include "components/os_crypt/keychain_password_mac.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if defined(OS_LINUX)
|
||||||
|
#include "components/os_crypt/key_storage_config_linux.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The global instance of the SystemNetworkContextmanager.
|
// The global instance of the SystemNetworkContextmanager.
|
||||||
|
@ -233,38 +238,56 @@ void SystemNetworkContextManager::OnNetworkServiceCreated(
|
||||||
network_context_.BindNewPipeAndPassReceiver(),
|
network_context_.BindNewPipeAndPassReceiver(),
|
||||||
CreateNetworkContextParams());
|
CreateNetworkContextParams());
|
||||||
|
|
||||||
if (electron::fuses::IsCookieEncryptionEnabled()) {
|
std::string app_name = electron::Browser::Get()->GetName();
|
||||||
std::string app_name = electron::Browser::Get()->GetName();
|
|
||||||
#if defined(OS_MAC)
|
#if defined(OS_MAC)
|
||||||
*KeychainPassword::service_name = app_name + " Safe Storage";
|
*KeychainPassword::service_name = app_name + " Safe Storage";
|
||||||
*KeychainPassword::account_name = app_name;
|
*KeychainPassword::account_name = app_name;
|
||||||
#endif
|
#endif
|
||||||
// The OSCrypt keys are process bound, so if network service is out of
|
|
||||||
// process, send it the required key.
|
|
||||||
if (content::IsOutOfProcessNetworkService()) {
|
|
||||||
#if defined(OS_LINUX)
|
#if defined(OS_LINUX)
|
||||||
// c.f.
|
// c.f.
|
||||||
// https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/net/system_network_context_manager.cc;l=515;drc=9d82515060b9b75fa941986f5db7390299669ef1;bpv=1;bpt=1
|
// https://source.chromium.org/chromium/chromium/src/+/master:chrome/browser/net/system_network_context_manager.cc;l=515;drc=9d82515060b9b75fa941986f5db7390299669ef1;bpv=1;bpt=1
|
||||||
const base::CommandLine& command_line =
|
const base::CommandLine& command_line =
|
||||||
*base::CommandLine::ForCurrentProcess();
|
*base::CommandLine::ForCurrentProcess();
|
||||||
|
|
||||||
network::mojom::CryptConfigPtr config =
|
auto config = std::make_unique<os_crypt::Config>();
|
||||||
network::mojom::CryptConfig::New();
|
config->store = command_line.GetSwitchValueASCII(::switches::kPasswordStore);
|
||||||
config->application_name = app_name;
|
config->product_name = app_name;
|
||||||
config->product_name = app_name;
|
config->application_name = app_name;
|
||||||
// c.f.
|
config->main_thread_runner = base::ThreadTaskRunnerHandle::Get();
|
||||||
// https://source.chromium.org/chromium/chromium/src/+/master:chrome/common/chrome_switches.cc;l=689;drc=9d82515060b9b75fa941986f5db7390299669ef1
|
// c.f.
|
||||||
config->store =
|
// https://source.chromium.org/chromium/chromium/src/+/master:chrome/common/chrome_switches.cc;l=689;drc=9d82515060b9b75fa941986f5db7390299669ef1
|
||||||
command_line.GetSwitchValueASCII(::switches::kPasswordStore);
|
config->should_use_preference =
|
||||||
config->should_use_preference =
|
command_line.HasSwitch(::switches::kEnableEncryptionSelection);
|
||||||
command_line.HasSwitch(::switches::kEnableEncryptionSelection);
|
base::PathService::Get(chrome::DIR_USER_DATA, &config->user_data_path);
|
||||||
base::PathService::Get(chrome::DIR_USER_DATA, &config->user_data_path);
|
#endif
|
||||||
network_service->SetCryptConfig(std::move(config));
|
|
||||||
#else
|
// The OSCrypt keys are process bound, so if network service is out of
|
||||||
network_service->SetEncryptionKey(OSCrypt::GetRawEncryptionKey());
|
// process, send it the required key.
|
||||||
|
if (content::IsOutOfProcessNetworkService() &&
|
||||||
|
electron::fuses::IsCookieEncryptionEnabled()) {
|
||||||
|
#if defined(OS_LINUX)
|
||||||
|
network::mojom::CryptConfigPtr network_crypt_config =
|
||||||
|
network::mojom::CryptConfig::New();
|
||||||
|
network_crypt_config->application_name = config->application_name;
|
||||||
|
network_crypt_config->product_name = config->product_name;
|
||||||
|
network_crypt_config->store = config->store;
|
||||||
|
network_crypt_config->should_use_preference = config->should_use_preference;
|
||||||
|
network_crypt_config->user_data_path = config->user_data_path;
|
||||||
|
|
||||||
|
network_service->SetCryptConfig(std::move(network_crypt_config));
|
||||||
|
|
||||||
|
#else
|
||||||
|
network_service->SetEncryptionKey(OSCrypt::GetRawEncryptionKey());
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if defined(OS_LINUX)
|
||||||
|
OSCrypt::SetConfig(std::move(config));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if DCHECK_IS_ON()
|
||||||
|
electron::safestorage::SetElectronCryptoReady(true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
network::mojom::NetworkContextParamsPtr
|
network::mojom::NetworkContextParamsPtr
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
V(electron_browser_power_save_blocker) \
|
V(electron_browser_power_save_blocker) \
|
||||||
V(electron_browser_protocol) \
|
V(electron_browser_protocol) \
|
||||||
V(electron_browser_printing) \
|
V(electron_browser_printing) \
|
||||||
|
V(electron_browser_safe_storage) \
|
||||||
V(electron_browser_session) \
|
V(electron_browser_session) \
|
||||||
V(electron_browser_system_preferences) \
|
V(electron_browser_system_preferences) \
|
||||||
V(electron_browser_base_window) \
|
V(electron_browser_base_window) \
|
||||||
|
|
103
spec-main/api-safe-storage-spec.ts
Normal file
103
spec-main/api-safe-storage-spec.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import * as cp from 'child_process';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { safeStorage } from 'electron/main';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { emittedOnce } from './events-helpers';
|
||||||
|
import { ifdescribe } from './spec-helpers';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
|
||||||
|
/* isEncryptionAvailable returns false in Linux when running CI due to a mocked dbus. This stops
|
||||||
|
* Chrome from reaching the system's keyring or libsecret. When running the tests with config.store
|
||||||
|
* set to basic-text, a nullptr is returned from chromium, defaulting the available encryption to false.
|
||||||
|
*
|
||||||
|
* Because all encryption methods are gated by isEncryptionAvailable, the methods will never return the correct values
|
||||||
|
* when run on CI and linux.
|
||||||
|
*/
|
||||||
|
|
||||||
|
ifdescribe(process.platform !== 'linux')('safeStorage module', () => {
|
||||||
|
after(async () => {
|
||||||
|
const pathToEncryptedString = path.resolve(__dirname, 'fixtures', 'api', 'safe-storage', 'encrypted.txt');
|
||||||
|
if (fs.existsSync(pathToEncryptedString)) {
|
||||||
|
await fs.unlinkSync(pathToEncryptedString);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SafeStorage.isEncryptionAvailable()', () => {
|
||||||
|
it('should return true when encryption key is available (macOS, Windows)', () => {
|
||||||
|
expect(safeStorage.isEncryptionAvailable()).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SafeStorage.encryptString()', () => {
|
||||||
|
it('valid input should correctly encrypt string', () => {
|
||||||
|
const plaintext = 'plaintext';
|
||||||
|
const encrypted = safeStorage.encryptString(plaintext);
|
||||||
|
expect(Buffer.isBuffer(encrypted)).to.equal(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UTF-16 characters can be encrypted', () => {
|
||||||
|
const plaintext = '€ - utf symbol';
|
||||||
|
const encrypted = safeStorage.encryptString(plaintext);
|
||||||
|
expect(Buffer.isBuffer(encrypted)).to.equal(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SafeStorage.decryptString()', () => {
|
||||||
|
it('valid input should correctly decrypt string', () => {
|
||||||
|
const encrypted = safeStorage.encryptString('plaintext');
|
||||||
|
expect(safeStorage.decryptString(encrypted)).to.equal('plaintext');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('UTF-16 characters can be decrypted', () => {
|
||||||
|
const plaintext = '€ - utf symbol';
|
||||||
|
const encrypted = safeStorage.encryptString(plaintext);
|
||||||
|
expect(safeStorage.decryptString(encrypted)).to.equal(plaintext);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unencrypted input should throw', () => {
|
||||||
|
const plaintextBuffer = Buffer.from('I am unencoded!', 'utf-8');
|
||||||
|
expect(() => {
|
||||||
|
safeStorage.decryptString(plaintextBuffer);
|
||||||
|
}).to.throw(Error);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('non-buffer input should throw', () => {
|
||||||
|
const notABuffer = {} as any;
|
||||||
|
expect(() => {
|
||||||
|
safeStorage.decryptString(notABuffer);
|
||||||
|
}).to.throw(Error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('safeStorage persists encryption key across app relaunch', () => {
|
||||||
|
it('can decrypt after closing and reopening app', async () => {
|
||||||
|
const fixturesPath = path.resolve(__dirname, 'fixtures');
|
||||||
|
|
||||||
|
const encryptAppPath = path.join(fixturesPath, 'api', 'safe-storage', 'encrypt-app');
|
||||||
|
const encryptAppProcess = cp.spawn(process.execPath, [encryptAppPath]);
|
||||||
|
let stdout: string = '';
|
||||||
|
encryptAppProcess.stderr.on('data', data => { stdout += data; });
|
||||||
|
encryptAppProcess.stderr.on('data', data => { stdout += data; });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await emittedOnce(encryptAppProcess, 'exit');
|
||||||
|
|
||||||
|
const appPath = path.join(fixturesPath, 'api', 'safe-storage', 'decrypt-app');
|
||||||
|
const relaunchedAppProcess = cp.spawn(process.execPath, [appPath]);
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
relaunchedAppProcess.stdout.on('data', data => { output += data; });
|
||||||
|
relaunchedAppProcess.stderr.on('data', data => { output += data; });
|
||||||
|
|
||||||
|
const [code] = await emittedOnce(relaunchedAppProcess, 'exit');
|
||||||
|
|
||||||
|
if (!output.includes('plaintext')) {
|
||||||
|
console.log(code, output);
|
||||||
|
}
|
||||||
|
expect(output).to.include('plaintext');
|
||||||
|
} catch (e) {
|
||||||
|
console.log(stdout);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
13
spec-main/fixtures/api/safe-storage/decrypt-app/main.js
Normal file
13
spec-main/fixtures/api/safe-storage/decrypt-app/main.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
const { app, safeStorage, ipcMain } = require('electron');
|
||||||
|
const { promises: fs } = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const pathToEncryptedString = path.resolve(__dirname, '..', 'encrypted.txt');
|
||||||
|
const readFile = fs.readFile;
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
const encryptedString = await readFile(pathToEncryptedString);
|
||||||
|
const decrypted = safeStorage.decryptString(encryptedString);
|
||||||
|
console.log(decrypted);
|
||||||
|
app.quit();
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "electron-safe-storage",
|
||||||
|
"main": "main.js"
|
||||||
|
}
|
12
spec-main/fixtures/api/safe-storage/encrypt-app/main.js
Normal file
12
spec-main/fixtures/api/safe-storage/encrypt-app/main.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const { app, safeStorage, ipcMain } = require('electron');
|
||||||
|
const { promises: fs } = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const pathToEncryptedString = path.resolve(__dirname, '..', 'encrypted.txt');
|
||||||
|
const writeFile = fs.writeFile;
|
||||||
|
|
||||||
|
app.whenReady().then(async () => {
|
||||||
|
const encrypted = safeStorage.encryptString('plaintext');
|
||||||
|
const encryptedString = await writeFile(pathToEncryptedString, encrypted);
|
||||||
|
app.quit();
|
||||||
|
});
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"name": "electron-safe-storage",
|
||||||
|
"main": "main.js"
|
||||||
|
}
|
1
typings/internal-ambient.d.ts
vendored
1
typings/internal-ambient.d.ts
vendored
|
@ -233,6 +233,7 @@ declare namespace NodeJS {
|
||||||
};
|
};
|
||||||
_linkedBinding(name: 'electron_browser_power_monitor'): PowerMonitorBinding;
|
_linkedBinding(name: 'electron_browser_power_monitor'): PowerMonitorBinding;
|
||||||
_linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };
|
_linkedBinding(name: 'electron_browser_power_save_blocker'): { powerSaveBlocker: Electron.PowerSaveBlocker };
|
||||||
|
_linkedBinding(name: 'electron_browser_safe_storage'): { safeStorage: Electron.SafeStorage };
|
||||||
_linkedBinding(name: 'electron_browser_session'): typeof Electron.Session;
|
_linkedBinding(name: 'electron_browser_session'): typeof Electron.Session;
|
||||||
_linkedBinding(name: 'electron_browser_system_preferences'): { systemPreferences: Electron.SystemPreferences };
|
_linkedBinding(name: 'electron_browser_system_preferences'): { systemPreferences: Electron.SystemPreferences };
|
||||||
_linkedBinding(name: 'electron_browser_tray'): { Tray: Electron.Tray };
|
_linkedBinding(name: 'electron_browser_tray'): { Tray: Electron.Tray };
|
||||||
|
|
Loading…
Reference in a new issue