mirror of
https://github.com/electron/electron.git
synced 2026-02-19 03:14:51 -05:00
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:
@@ -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');
|
||||
|
||||
23
spec/fixtures/crash-cases/safe-storage/index.js
vendored
23
spec/fixtures/crash-cases/safe-storage/index.js
vendored
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user