Skip to content

Commit 6ada8d4

Browse files
Improve script error dialogs #304
- Include the script's directory path #304. - Exclude Windows-specific instructions on non-Windows OS. - Standardize language across dialogs for consistency. Other supporting changes: - Add script diagnostics data collection from main process. - Document script file storage and execution tamper protection in SECURITY.md. - Remove redundant comment in `NodeReadbackFileWriter`. - Centralize error display for uniformity and simplicity. - Simpify `WindowVariablesValidator` to omit checks when not on the renderer process. - Improve and centralize Electron environment detection. - Use more emphatic language (don't worry) in error messages.
1 parent f03fc24 commit 6ada8d4

34 files changed

Lines changed: 1167 additions & 435 deletions

‎SECURITY.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
4141
The desktop application operates without persistent administrative or `sudo` privileges, reinforcing its security posture. It requests
4242
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
4343
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
44+
- **Secure Script Execution/Storage:**
45+
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
46+
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
47+
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
4448

4549
### Update Security and Integrity
4650

‎docs/desktop-vs-web-features.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ This table highlights differences between the desktop and web versions of `priva
1010
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
1111
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
1212
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
13-
| [Native dialogs](#error-handling) | 🟢 Available | 🔴 Not available |
13+
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
14+
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
1415

1516
## Feature descriptions
1617

@@ -74,3 +75,21 @@ In contrast, the web version has more basic error handling due to browser limita
7475

7576
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
7677
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
78+
79+
### Secure script execution/storage
80+
81+
**Integrity checks:**
82+
83+
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
84+
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
85+
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
86+
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
87+
Due to browser constraints, this feature is absent in the web version.
88+
89+
**Error handling:**
90+
91+
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
92+
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
93+
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
94+
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
95+
offering a feature not achievable in the web version due to browser limitations.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { OperatingSystem } from '@/domain/OperatingSystem';
2+
3+
export interface ScriptDiagnosticsCollector {
4+
collectDiagnosticInformation(): Promise<ScriptDiagnosticData>;
5+
}
6+
7+
export interface ScriptDiagnosticData {
8+
readonly scriptsDirectoryAbsolutePath?: string;
9+
readonly currentOperatingSystem?: OperatingSystem;
10+
}

‎src/infrastructure/ReadbackFileWriter/NodeReadbackFileWriter.ts‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,6 @@ export class NodeReadbackFileWriter implements ReadbackFileWriter {
2727
const fileWritePipelineActions: ReadonlyArray<() => Promise<FileWriteOutcome>> = [
2828
() => this.createOrOverwriteFile(filePath, fileContents),
2929
() => this.verifyFileExistsWithoutReading(filePath),
30-
/*
31-
Reading the file contents back, we can detect if the file has been altered or
32-
removed post-creation. Removal of scripts when reading back is seen by some antivirus
33-
software when it falsely identifies a script as harmful.
34-
*/
3530
() => this.verifyFileContentsByReading(filePath, fileContents),
3631
];
3732
for (const action of fileWritePipelineActions) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ElectronEnvironmentDetector, ElectronProcessType } from './ElectronEnvironmentDetector';
2+
3+
export class ContextIsolatedElectronDetector implements ElectronEnvironmentDetector {
4+
constructor(
5+
private readonly nodeProcessAccessor: NodeProcessAccessor = () => globalThis?.process,
6+
private readonly userAgentAccessor: UserAgentAccessor = () => globalThis?.navigator?.userAgent,
7+
) { }
8+
9+
public isRunningInsideElectron(): boolean {
10+
return isNodeProcessElectronBased(this.nodeProcessAccessor)
11+
|| isUserAgentElectronBased(this.userAgentAccessor);
12+
}
13+
14+
public determineElectronProcessType(): ElectronProcessType {
15+
const isNodeAccessible = isNodeProcessElectronBased(this.nodeProcessAccessor);
16+
const isBrowserAccessible = isUserAgentElectronBased(this.userAgentAccessor);
17+
if (!isNodeAccessible && !isBrowserAccessible) {
18+
throw new Error('Unable to determine the Electron process type. Neither Node.js nor browser-based Electron contexts were detected.');
19+
}
20+
if (isNodeAccessible && isBrowserAccessible) {
21+
return 'preloader'; // Only preloader can access both Node.js and browser contexts in Electron with context isolation.
22+
}
23+
if (isNodeAccessible) {
24+
return 'main';
25+
}
26+
return 'renderer';
27+
}
28+
}
29+
30+
export type NodeProcessAccessor = () => NodeJS.Process | undefined;
31+
32+
function isNodeProcessElectronBased(nodeProcessAccessor: NodeProcessAccessor): boolean {
33+
const nodeProcess = nodeProcessAccessor();
34+
if (!nodeProcess) {
35+
return false;
36+
}
37+
if (nodeProcess.versions.electron) {
38+
// Electron populates `nodeProcess.versions.electron` with its version, see https://web.archive.org/web/20240113162837/https://www.electronjs.org/docs/latest/api/process#processversionselectron-readonly.
39+
return true;
40+
}
41+
return false;
42+
}
43+
44+
export type UserAgentAccessor = () => string | undefined;
45+
46+
function isUserAgentElectronBased(
47+
userAgentAccessor: UserAgentAccessor,
48+
): boolean {
49+
const userAgent = userAgentAccessor();
50+
if (userAgent?.includes('Electron')) {
51+
return true;
52+
}
53+
return false;
54+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface ElectronEnvironmentDetector {
2+
isRunningInsideElectron(): boolean;
3+
determineElectronProcessType(): ElectronProcessType;
4+
}
5+
6+
export type ElectronProcessType = 'main' | 'preloader' | 'renderer';
Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,32 @@
1+
import { ElectronEnvironmentDetector } from './Electron/ElectronEnvironmentDetector';
12
import { BrowserRuntimeEnvironment } from './Browser/BrowserRuntimeEnvironment';
23
import { NodeRuntimeEnvironment } from './Node/NodeRuntimeEnvironment';
34
import { RuntimeEnvironment } from './RuntimeEnvironment';
5+
import { ContextIsolatedElectronDetector } from './Electron/ContextIsolatedElectronDetector';
46

5-
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment({
6-
window: globalThis.window,
7-
process: globalThis.process,
8-
});
7+
export const CurrentEnvironment = determineAndCreateRuntimeEnvironment(globalThis.window);
98

109
export function determineAndCreateRuntimeEnvironment(
11-
globalAccessor: GlobalPropertiesAccessor,
10+
globalWindow: Window | undefined | null = globalThis.window,
11+
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
1212
browserEnvironmentFactory: BrowserRuntimeEnvironmentFactory = (
1313
window,
1414
) => new BrowserRuntimeEnvironment(window),
15-
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = (
16-
process: NodeJS.Process,
17-
) => new NodeRuntimeEnvironment(process),
15+
nodeEnvironmentFactory: NodeRuntimeEnvironmentFactory = () => new NodeRuntimeEnvironment(),
1816
): RuntimeEnvironment {
19-
if (isElectronMainProcess(globalAccessor.process)) {
20-
return nodeEnvironmentFactory(globalAccessor.process);
17+
if (
18+
electronDetector.isRunningInsideElectron()
19+
&& electronDetector.determineElectronProcessType() === 'main') {
20+
return nodeEnvironmentFactory();
2121
}
22-
const { window } = globalAccessor;
23-
if (!window) {
22+
if (!globalWindow) {
2423
throw new Error('Unsupported runtime environment: The current context is neither a recognized browser nor a desktop environment.');
2524
}
26-
return browserEnvironmentFactory(window);
27-
}
28-
29-
function isElectronMainProcess(
30-
nodeProcess: NodeJS.Process | undefined,
31-
): nodeProcess is NodeJS.Process {
32-
// Electron populates `nodeProcess.versions.electron` with its version, see https://web.archive.org/web/20240113162837/https://www.electronjs.org/docs/latest/api/process#processversionselectron-readonly.
33-
if (!nodeProcess) {
34-
return false;
35-
}
36-
if (nodeProcess.versions.electron) {
37-
return true;
38-
}
39-
return false;
25+
return browserEnvironmentFactory(globalWindow);
4026
}
4127

4228
export type BrowserRuntimeEnvironmentFactory = (window: Window) => RuntimeEnvironment;
4329

44-
export type NodeRuntimeEnvironmentFactory = (process: NodeJS.Process) => NodeRuntimeEnvironment;
30+
export type NodeRuntimeEnvironmentFactory = () => NodeRuntimeEnvironment;
4531

46-
export interface GlobalPropertiesAccessor {
47-
readonly window: Window | undefined;
48-
readonly process: NodeJS.Process | undefined;
49-
}
32+
export type GlobalWindowAccessor = Window | undefined;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
2+
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
3+
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
4+
import { PersistentDirectoryProvider } from '@/infrastructure/CodeRunner/Creation/Directory/PersistentDirectoryProvider';
5+
import { ScriptDirectoryProvider } from '../CodeRunner/Creation/Directory/ScriptDirectoryProvider';
6+
7+
export class ScriptEnvironmentDiagnosticsCollector implements ScriptDiagnosticsCollector {
8+
constructor(
9+
private readonly directoryProvider: ScriptDirectoryProvider = new PersistentDirectoryProvider(),
10+
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
11+
) { }
12+
13+
public async collectDiagnosticInformation(): Promise<ScriptDiagnosticData> {
14+
const { directoryAbsolutePath } = await this.directoryProvider.provideScriptDirectory();
15+
return {
16+
scriptsDirectoryAbsolutePath: directoryAbsolutePath,
17+
currentOperatingSystem: this.environment.os,
18+
};
19+
}
20+
}

‎src/infrastructure/WindowVariables/WindowVariables.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
22
import { Logger } from '@/application/Common/Log/Logger';
33
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
44
import { Dialog } from '@/presentation/common/Dialog';
5+
import { ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
56

67
/* Primary entry point for platform-specific injections */
78
export interface WindowVariables {
@@ -10,4 +11,5 @@ export interface WindowVariables {
1011
readonly os?: OperatingSystem;
1112
readonly log?: Logger;
1213
readonly dialog?: Dialog;
14+
readonly scriptDiagnosticsCollector?: ScriptDiagnosticsCollector;
1315
}

‎src/infrastructure/WindowVariables/WindowVariablesValidator.ts‎

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ContextIsolatedElectronDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ContextIsolatedElectronDetector';
2+
import { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
13
import { OperatingSystem } from '@/domain/OperatingSystem';
24
import {
35
PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
@@ -7,7 +9,14 @@ import { WindowVariables } from './WindowVariables';
79
/**
810
* Checks for consistency in runtime environment properties injected by Electron preloader.
911
*/
10-
export function validateWindowVariables(variables: Partial<WindowVariables>) {
12+
export function validateWindowVariables(
13+
variables: Partial<WindowVariables>,
14+
electronDetector: ElectronEnvironmentDetector = new ContextIsolatedElectronDetector(),
15+
) {
16+
if (!electronDetector.isRunningInsideElectron()
17+
|| electronDetector.determineElectronProcessType() !== 'renderer') {
18+
return;
19+
}
1120
if (!isPlainObject(variables)) {
1221
throw new Error('window is not an object');
1322
}
@@ -20,12 +29,11 @@ export function validateWindowVariables(variables: Partial<WindowVariables>) {
2029
function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<string> {
2130
const tests: Record<PropertyKeys<Required<WindowVariables>>, boolean> = {
2231
os: testOperatingSystem(variables.os),
23-
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(
24-
variables.isRunningAsDesktopApplication,
25-
),
32+
isRunningAsDesktopApplication: testIsRunningAsDesktopApplication(variables),
2633
codeRunner: testCodeRunner(variables),
2734
log: testLogger(variables),
2835
dialog: testDialog(variables),
36+
scriptDiagnosticsCollector: testScriptDiagnosticsCollector(variables),
2937
};
3038

3139
for (const [propertyName, testResult] of Object.entries(tests)) {
@@ -49,30 +57,30 @@ function testOperatingSystem(os: unknown): boolean {
4957
}
5058

5159
function testLogger(variables: Partial<WindowVariables>): boolean {
52-
if (!variables.isRunningAsDesktopApplication) {
53-
return true;
54-
}
55-
return isPlainObject(variables.log);
60+
return isPlainObject(variables.log)
61+
&& isFunction(variables.log.debug)
62+
&& isFunction(variables.log.info)
63+
&& isFunction(variables.log.error)
64+
&& isFunction(variables.log.warn);
5665
}
5766

5867
function testCodeRunner(variables: Partial<WindowVariables>): boolean {
59-
if (!variables.isRunningAsDesktopApplication) {
60-
return true;
61-
}
6268
return isPlainObject(variables.codeRunner)
6369
&& isFunction(variables.codeRunner.runCode);
6470
}
6571

66-
function testIsRunningAsDesktopApplication(isRunningAsDesktopApplication: unknown): boolean {
67-
if (isRunningAsDesktopApplication === undefined) {
68-
return true;
69-
}
70-
return isBoolean(isRunningAsDesktopApplication);
72+
function testIsRunningAsDesktopApplication(variables: Partial<WindowVariables>): boolean {
73+
return isBoolean(variables.isRunningAsDesktopApplication)
74+
&& variables.isRunningAsDesktopApplication === true;
7175
}
7276

7377
function testDialog(variables: Partial<WindowVariables>): boolean {
74-
if (!variables.isRunningAsDesktopApplication) {
75-
return true;
76-
}
77-
return isPlainObject(variables.dialog);
78+
return isPlainObject(variables.dialog)
79+
&& isFunction(variables.dialog.saveFile)
80+
&& isFunction(variables.dialog.showError);
81+
}
82+
83+
function testScriptDiagnosticsCollector(variables: Partial<WindowVariables>): boolean {
84+
return isPlainObject(variables.scriptDiagnosticsCollector)
85+
&& isFunction(variables.scriptDiagnosticsCollector.collectDiagnosticInformation);
7886
}

0 commit comments

Comments
 (0)