electron/spec-main/api-safe-storage-spec.ts
Darshan Sen 03e68e2efe
fix: fix a crash in safeStorage on Linux (#33913)
On Linux, `isEncryptionAvailable()` was crashing instead of returning a
boolean before the 'ready' event was emitted by the app. The reason of
the crash is that [`CreateKeyStorage()`](https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=74;drc=35be6215ec8f09e50176f36753c68f26c63d1885;bpv=1;bpt=0)
expects the config to be set but the function responsible for setting the
config, [`SetConfig()`](https://source.chromium.org/chromium/chromium/src/+/main:components/os_crypt/os_crypt_linux.cc;l=237;drc=35be6215ec8f09e50176f36753c68f26c63d1885;bpv=1;bpt=0),
is called only after the app is ready inside [`PostCreateMainMessageLoop()`](https://github.com/electron/electron/blob/main/shell/browser/electron_browser_main_parts.cc#L499).
So this changes `IsEncryptionAvailable()` to return `false` when the app
is not ready on Linux and uses that instead of the raw API in other
places like `EncryptString()` and `DecryptString()`.

Fixes: https://github.com/electron/electron/issues/32206
Signed-off-by: Darshan Sen <raisinten@gmail.com>
2022-05-09 09:38:53 -04:00

122 lines
4.5 KiB
TypeScript

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.
* Refs: https://github.com/electron/electron/issues/30424.
*/
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 emittedOnce(appProcess, 'exit'))[0] ?? 1;
if (code !== 0 && output) {
console.log(output);
}
expect(code).to.equal(0);
});
});
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;
}
});
});
});