From 4fe944064fe8dcd2acf73f4e915519ff79338dd6 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 11 Oct 2024 10:35:15 -0700 Subject: [PATCH 1/7] feat: adding hardcoded interface for FuseV1Config so that electron-builder can integrate @electron/fuses while also allowing scheme.json to still generate correctly --- packages/app-builder-lib/package.json | 1 + packages/app-builder-lib/scheme.json | 44 +++++++++++++++++++ packages/app-builder-lib/src/configuration.ts | 18 ++++++++ .../app-builder-lib/src/platformPackager.ts | 38 ++++++++++++++++ pnpm-lock.yaml | 13 ++++++ test/src/linux/linuxPackagerTest.ts | 11 +++++ test/src/mac/macPackagerTest.ts | 11 +++++ test/src/windows/winPackagerTest.ts | 11 +++++ 8 files changed, 147 insertions(+) diff --git a/packages/app-builder-lib/package.json b/packages/app-builder-lib/package.json index 130a60dbd3e..58751156fca 100644 --- a/packages/app-builder-lib/package.json +++ b/packages/app-builder-lib/package.json @@ -47,6 +47,7 @@ "homepage": "https://github.com/electron-userland/electron-builder", "dependencies": { "@develar/schema-utils": "~2.6.5", + "@electron/fuses": "^1.8.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.1", "@electron/rebuild": "3.7.0", diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index dbb2c643a41..9e7963b4ea5 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -1284,6 +1284,39 @@ }, "type": "object" }, + "FuseOptionsV1": { + "additionalProperties": false, + "properties": { + "enableCookieEncryption": { + "type": "boolean" + }, + "enableEmbeddedAsarIntegrityValidation": { + "type": "boolean" + }, + "enableNodeCliInspectArguments": { + "type": "boolean" + }, + "enableNodeOptionsEnvironmentVariable": { + "type": "boolean" + }, + "grantFileProtocolExtraPrivileges": { + "type": "boolean" + }, + "loadBrowserProcessSpecificV8Snapshot": { + "type": "boolean" + }, + "onlyLoadAppFromAsar": { + "type": "boolean" + }, + "resetAdHocDarwinSignature": { + "type": "boolean" + }, + "runAsNode": { + "type": "boolean" + } + }, + "type": "object" + }, "GenericServerOptions": { "additionalProperties": false, "description": "Generic (any HTTP(S) server) options.\nIn all publish options [File Macros](./file-patterns.md#file-macros) are supported.", @@ -7023,6 +7056,17 @@ "$ref": "#/definitions/ElectronDownloadOptions", "description": "The [electron-download](https://github.com/electron-userland/electron-download#usage) options." }, + "electronFuses": { + "anyOf": [ + { + "$ref": "#/definitions/FuseOptionsV1" + }, + { + "type": "null" + } + ], + "description": "Options to pass to `@electron/fuses`\nRef: https://github.com/electron/fuses" + }, "electronLanguages": { "anyOf": [ { diff --git a/packages/app-builder-lib/src/configuration.ts b/packages/app-builder-lib/src/configuration.ts index 2ca16a8c4ac..edcd142961f 100644 --- a/packages/app-builder-lib/src/configuration.ts +++ b/packages/app-builder-lib/src/configuration.ts @@ -181,6 +181,12 @@ export interface CommonConfiguration { * @default true */ readonly removePackageKeywords?: boolean + + /** + * Options to pass to `@electron/fuses` + * Ref: https://github.com/electron/fuses + */ + readonly electronFuses?: FuseOptionsV1 | null } export interface Configuration extends CommonConfiguration, PlatformSpecificBuildOptions, Hooks { /** @@ -375,3 +381,15 @@ export interface MetadataDirectories { */ readonly app?: string | null } + +export interface FuseOptionsV1 { + runAsNode?: boolean + enableCookieEncryption?: boolean + enableNodeOptionsEnvironmentVariable?: boolean + enableNodeCliInspectArguments?: boolean + enableEmbeddedAsarIntegrityValidation?: boolean + onlyLoadAppFromAsar?: boolean + loadBrowserProcessSpecificV8Snapshot?: boolean + grantFileProtocolExtraPrivileges?: boolean + resetAdHocDarwinSignature?: boolean +} diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 6f02633eabd..19dda162fd1 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -30,6 +30,8 @@ import { executeAppBuilderAsJson } from "./util/appBuilder" import { computeFileSets, computeNodeModuleFileSets, copyAppFiles, ELECTRON_COMPILE_SHIM_FILENAME, transformFiles } from "./util/appFileCopier" import { expandMacro as doExpandMacro } from "./util/macroExpander" import { resolveFunction } from "./util/resolve" +import { flipFuses, FuseV1Config, FuseV1Options, FuseVersion } from "@electron/fuses" +import { FuseOptionsV1 as ConfigurationFusesV1Options } from "./configuration" export abstract class PlatformPackager { get packagerOptions(): PackagerOptions { @@ -321,6 +323,10 @@ export abstract class PlatformPackager await this.info.afterPack(packContext) + if (this.config.electronFuses != null) { + await this.addElectronFuses(packContext, this.config.electronFuses) + } + if (framework.afterPack != null) { await framework.afterPack(packContext) } @@ -332,6 +338,38 @@ export abstract class PlatformPackager } } + private async addElectronFuses(context: AfterPackContext, fuses: ConfigurationFusesV1Options) { + const { + appOutDir, + packager: { appInfo }, + electronPlatformName, + arch, + } = context + + const ext = { + darwin: ".app", + win32: ".exe", + linux: [""], + }[electronPlatformName] + + const electronBinaryPath = path.join(appOutDir, `${appInfo.productFilename}${ext}`) + log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses") + + const fuseOptions: FuseV1Config = { + version: FuseVersion.V1, + resetAdHocDarwinSignature: fuses.resetAdHocDarwinSignature ?? (electronPlatformName === "darwin" && arch === Arch.universal), + [FuseV1Options.RunAsNode]: fuses.runAsNode, + [FuseV1Options.EnableCookieEncryption]: fuses.enableCookieEncryption, + [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: fuses.enableNodeOptionsEnvironmentVariable, + [FuseV1Options.EnableNodeCliInspectArguments]: fuses.enableNodeCliInspectArguments, + [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: fuses.enableEmbeddedAsarIntegrityValidation, + [FuseV1Options.OnlyLoadAppFromAsar]: fuses.onlyLoadAppFromAsar, + [FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: fuses.loadBrowserProcessSpecificV8Snapshot, + [FuseV1Options.GrantFileProtocolExtraPrivileges]: fuses.grantFileProtocolExtraPrivileges, + } + return flipFuses(electronBinaryPath, fuseOptions) + } + protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array) { const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions) const isAsar = asarOptions != null diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70c7b973520..b3e1078914a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: '@develar/schema-utils': specifier: ~2.6.5 version: 2.6.5 + '@electron/fuses': + specifier: ^1.8.0 + version: 1.8.0 '@electron/notarize': specifier: 2.5.0 version: 2.5.0 @@ -1597,6 +1600,10 @@ packages: engines: {node: '>=10.12.0'} hasBin: true + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + '@electron/get@2.0.3': resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} engines: {node: '>=12'} @@ -7434,6 +7441,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + '@electron/get@2.0.3': dependencies: debug: 4.3.7 diff --git a/test/src/linux/linuxPackagerTest.ts b/test/src/linux/linuxPackagerTest.ts index 42ad80bbec7..9e8e87a7a12 100644 --- a/test/src/linux/linuxPackagerTest.ts +++ b/test/src/linux/linuxPackagerTest.ts @@ -26,6 +26,17 @@ test.ifNotWindows( }, downloadAlternateFFmpeg: true, publish: testPublishConfig, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + // grantFileProtocolExtraPrivileges: true, + resetAdHocDarwinSignature: true, + } }, }) ) diff --git a/test/src/mac/macPackagerTest.ts b/test/src/mac/macPackagerTest.ts index 37871333d0e..790ebfb72fc 100644 --- a/test/src/mac/macPackagerTest.ts +++ b/test/src/mac/macPackagerTest.ts @@ -24,6 +24,17 @@ test.ifMac.ifAll("two-package", () => }, //tslint:disable-next-line:no-invalid-template-strings artifactName: "${name}-${version}-${os}-${arch}.${ext}", + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + // grantFileProtocolExtraPrivileges: true, + resetAdHocDarwinSignature: true, + } }, }, { diff --git a/test/src/windows/winPackagerTest.ts b/test/src/windows/winPackagerTest.ts index 0242ab03b6c..6af8fc0d3b5 100644 --- a/test/src/windows/winPackagerTest.ts +++ b/test/src/windows/winPackagerTest.ts @@ -22,6 +22,17 @@ test.ifNotCiMac( targets: Platform.WINDOWS.createTarget(["zip"], Arch.x64), config: { downloadAlternateFFmpeg: true, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + // grantFileProtocolExtraPrivileges: true, + resetAdHocDarwinSignature: true, + } }, }) ) From 12d8d4d93a76e8dfc4a0c5ea0dfe3ed5257973ef Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 11 Oct 2024 10:57:59 -0700 Subject: [PATCH 2/7] update logic to resolve electron/fuses error of "too many fuses" for older versions of electron --- .changeset/two-rice-wash.md | 5 ++ .../app-builder-lib/src/platformPackager.ts | 46 ++++++++++++++----- test/src/linux/linuxPackagerTest.ts | 3 +- test/src/mac/macPackagerTest.ts | 3 +- test/src/windows/winPackagerTest.ts | 3 +- 5 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 .changeset/two-rice-wash.md diff --git a/.changeset/two-rice-wash.md b/.changeset/two-rice-wash.md new file mode 100644 index 00000000000..8a5ad6e48ad --- /dev/null +++ b/.changeset/two-rice-wash.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": minor +--- + +feat: adding integration with @electron/fuses diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 19dda162fd1..e156d2b8da4 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -352,22 +352,44 @@ export abstract class PlatformPackager linux: [""], }[electronPlatformName] - const electronBinaryPath = path.join(appOutDir, `${appInfo.productFilename}${ext}`) + const executableName = electronPlatformName === "linux" ? appInfo.sanitizedName.toLowerCase() : appInfo.productFilename + const electronBinaryPath = path.join(appOutDir, `${executableName}${ext}`) + log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses") + return flipFuses(electronBinaryPath, this.generateFuseConfig(electronPlatformName, arch, fuses)) + } - const fuseOptions: FuseV1Config = { + private generateFuseConfig(electronPlatformName: string, arch: Arch, fuses: ConfigurationFusesV1Options): FuseV1Config { + const config: FuseV1Config = { version: FuseVersion.V1, resetAdHocDarwinSignature: fuses.resetAdHocDarwinSignature ?? (electronPlatformName === "darwin" && arch === Arch.universal), - [FuseV1Options.RunAsNode]: fuses.runAsNode, - [FuseV1Options.EnableCookieEncryption]: fuses.enableCookieEncryption, - [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: fuses.enableNodeOptionsEnvironmentVariable, - [FuseV1Options.EnableNodeCliInspectArguments]: fuses.enableNodeCliInspectArguments, - [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: fuses.enableEmbeddedAsarIntegrityValidation, - [FuseV1Options.OnlyLoadAppFromAsar]: fuses.onlyLoadAppFromAsar, - [FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: fuses.loadBrowserProcessSpecificV8Snapshot, - [FuseV1Options.GrantFileProtocolExtraPrivileges]: fuses.grantFileProtocolExtraPrivileges, - } - return flipFuses(electronBinaryPath, fuseOptions) + } + // this is annoying, but we must filter out undefined entries because some older electron versions will receive `the fuse wire in this version of Electron is not long enough` even if entry is set undefined + if (fuses.runAsNode != null) { + config[FuseV1Options.RunAsNode] = fuses.runAsNode + } + if (fuses.enableCookieEncryption != null) { + config[FuseV1Options.EnableCookieEncryption] = fuses.enableCookieEncryption + } + if (fuses.enableNodeOptionsEnvironmentVariable != null) { + config[FuseV1Options.EnableNodeOptionsEnvironmentVariable] = fuses.enableNodeOptionsEnvironmentVariable + } + if (fuses.enableNodeCliInspectArguments != null) { + config[FuseV1Options.EnableNodeCliInspectArguments] = fuses.enableNodeCliInspectArguments + } + if (fuses.enableEmbeddedAsarIntegrityValidation != null) { + config[FuseV1Options.EnableEmbeddedAsarIntegrityValidation] = fuses.enableEmbeddedAsarIntegrityValidation + } + if (fuses.onlyLoadAppFromAsar != null) { + config[FuseV1Options.OnlyLoadAppFromAsar] = fuses.onlyLoadAppFromAsar + } + if (fuses.loadBrowserProcessSpecificV8Snapshot != null) { + config[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot] = fuses.loadBrowserProcessSpecificV8Snapshot + } + if (fuses.grantFileProtocolExtraPrivileges != null) { + config[FuseV1Options.GrantFileProtocolExtraPrivileges] = fuses.grantFileProtocolExtraPrivileges + } + return config } protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array) { diff --git a/test/src/linux/linuxPackagerTest.ts b/test/src/linux/linuxPackagerTest.ts index 9e8e87a7a12..04c3c02eefc 100644 --- a/test/src/linux/linuxPackagerTest.ts +++ b/test/src/linux/linuxPackagerTest.ts @@ -34,8 +34,7 @@ test.ifNotWindows( enableEmbeddedAsarIntegrityValidation: true, onlyLoadAppFromAsar: true, loadBrowserProcessSpecificV8Snapshot: true, - // grantFileProtocolExtraPrivileges: true, - resetAdHocDarwinSignature: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests } }, }) diff --git a/test/src/mac/macPackagerTest.ts b/test/src/mac/macPackagerTest.ts index 790ebfb72fc..570080d8202 100644 --- a/test/src/mac/macPackagerTest.ts +++ b/test/src/mac/macPackagerTest.ts @@ -32,8 +32,7 @@ test.ifMac.ifAll("two-package", () => enableEmbeddedAsarIntegrityValidation: true, onlyLoadAppFromAsar: true, loadBrowserProcessSpecificV8Snapshot: true, - // grantFileProtocolExtraPrivileges: true, - resetAdHocDarwinSignature: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests } }, }, diff --git a/test/src/windows/winPackagerTest.ts b/test/src/windows/winPackagerTest.ts index 6af8fc0d3b5..8f10609a2eb 100644 --- a/test/src/windows/winPackagerTest.ts +++ b/test/src/windows/winPackagerTest.ts @@ -30,8 +30,7 @@ test.ifNotCiMac( enableEmbeddedAsarIntegrityValidation: true, onlyLoadAppFromAsar: true, loadBrowserProcessSpecificV8Snapshot: true, - // grantFileProtocolExtraPrivileges: true, - resetAdHocDarwinSignature: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests } }, }) From 5e296d6cbb33ddebcd8b83eef8d46c5ed642edbb Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 11 Oct 2024 13:09:19 -0700 Subject: [PATCH 3/7] updating docs and moving electron fuses directly before signing step --- packages/app-builder-lib/scheme.json | 10 +++ packages/app-builder-lib/src/configuration.ts | 32 +++++++++ .../app-builder-lib/src/platformPackager.ts | 68 +++++++++---------- test/src/linux/debTest.ts | 10 +++ test/src/linux/flatpakTest.ts | 12 ++++ test/src/linux/fpmTest.ts | 38 ++++++++++- test/src/linux/linuxArchiveTest.ts | 19 +++++- test/src/linux/linuxPackagerTest.ts | 12 +++- test/src/linux/snapHeavyTest.ts | 10 +++ test/src/linux/snapTest.ts | 10 +++ test/src/mac/macPackagerTest.ts | 2 +- test/src/windows/appxTest.ts | 12 ++++ test/src/windows/assistedInstallerTest.ts | 10 +++ test/src/windows/msiTest.ts | 10 +++ test/src/windows/msiWrappedTest.ts | 10 +++ test/src/windows/oneClickInstallerTest.ts | 20 ++++++ test/src/windows/portableTest.ts | 10 +++ test/src/windows/squirrelWindowsTest.ts | 11 +++ test/src/windows/webInstallerTest.ts | 10 +++ test/src/windows/winPackagerTest.ts | 2 +- 20 files changed, 276 insertions(+), 42 deletions(-) diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index 9e7963b4ea5..3d9f1a51167 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -1286,32 +1286,42 @@ }, "FuseOptionsV1": { "additionalProperties": false, + "description": "All options come from [@electron/fuses](https://github.com/electron/fuses)", "properties": { "enableCookieEncryption": { + "description": "Enables cookie encryption", "type": "boolean" }, "enableEmbeddedAsarIntegrityValidation": { + "description": "Enables validation of the app.asar archive on macOS", "type": "boolean" }, "enableNodeCliInspectArguments": { + "description": "Disables the --inspect and --inspect-brk family of CLI options", "type": "boolean" }, "enableNodeOptionsEnvironmentVariable": { + "description": "Disables the NODE_OPTIONS environment variable", "type": "boolean" }, "grantFileProtocolExtraPrivileges": { + "description": "Grants the file protocol extra privileges", "type": "boolean" }, "loadBrowserProcessSpecificV8Snapshot": { + "description": "Loads V8 Snapshot from `browser_v8_context_snapshot.bin` for the browser process", "type": "boolean" }, "onlyLoadAppFromAsar": { + "description": "Enforces that Electron will only load your app from \"app.asar\" instead of its normal search paths", "type": "boolean" }, "resetAdHocDarwinSignature": { + "description": "Resets the app signature, specifically used for macOS.\nNote: This should be unneeded since electron-builder signs the app directly after flipping the fuses.\nRef: https://github.com/electron/fuses?tab=readme-ov-file#apple-silicon", "type": "boolean" }, "runAsNode": { + "description": "Disables ELECTRON_RUN_AS_NODE", "type": "boolean" } }, diff --git a/packages/app-builder-lib/src/configuration.ts b/packages/app-builder-lib/src/configuration.ts index edcd142961f..0d3ed79cafd 100644 --- a/packages/app-builder-lib/src/configuration.ts +++ b/packages/app-builder-lib/src/configuration.ts @@ -382,14 +382,46 @@ export interface MetadataDirectories { readonly app?: string | null } +/** + * All options come from [@electron/fuses](https://github.com/electron/fuses) + */ export interface FuseOptionsV1 { + /** + * Disables ELECTRON_RUN_AS_NODE + */ runAsNode?: boolean + /** + * Enables cookie encryption + */ enableCookieEncryption?: boolean + /** + * Disables the NODE_OPTIONS environment variable + */ enableNodeOptionsEnvironmentVariable?: boolean + /** + * Disables the --inspect and --inspect-brk family of CLI options + */ enableNodeCliInspectArguments?: boolean + /** + * Enables validation of the app.asar archive on macOS + */ enableEmbeddedAsarIntegrityValidation?: boolean + /** + * Enforces that Electron will only load your app from "app.asar" instead of its normal search paths + */ onlyLoadAppFromAsar?: boolean + /** + * Loads V8 Snapshot from `browser_v8_context_snapshot.bin` for the browser process + */ loadBrowserProcessSpecificV8Snapshot?: boolean + /** + * Grants the file protocol extra privileges + */ grantFileProtocolExtraPrivileges?: boolean + /** + * Resets the app signature, specifically used for macOS. + * Note: This should be unneeded since electron-builder signs the app directly after flipping the fuses. + * Ref: https://github.com/electron/fuses?tab=readme-ov-file#apple-silicon + */ resetAdHocDarwinSignature?: boolean } diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index e156d2b8da4..a1629a3e619 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -19,6 +19,7 @@ import { Configuration, ElectronPlatformName, FileAssociation, + LinuxPackager, Packager, PackagerOptions, Platform, @@ -323,10 +324,6 @@ export abstract class PlatformPackager await this.info.afterPack(packContext) - if (this.config.electronFuses != null) { - await this.addElectronFuses(packContext, this.config.electronFuses) - } - if (framework.afterPack != null) { await framework.afterPack(packContext) } @@ -338,31 +335,52 @@ export abstract class PlatformPackager } } - private async addElectronFuses(context: AfterPackContext, fuses: ConfigurationFusesV1Options) { - const { + protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array) { + const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions) + const isAsar = asarOptions != null + const packContext = { appOutDir, - packager: { appInfo }, - electronPlatformName, + outDir, arch, - } = context + targets, + packager: this, + electronPlatformName: platformName, + } + // the fuses MUST be flipped right before signing + if (this.config.electronFuses != null) { + await this.addElectronFuses(packContext, this.config.electronFuses) + } + const didSign = await this.signApp(packContext, isAsar) + const afterSign = await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign") + if (afterSign != null) { + if (didSign) { + await Promise.resolve(afterSign(packContext)) + } else { + log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) + } + } + } + + public async addElectronFuses(context: AfterPackContext, fuses: ConfigurationFusesV1Options) { + const { appOutDir, electronPlatformName, packager } = context const ext = { darwin: ".app", win32: ".exe", - linux: [""], + linux: "", }[electronPlatformName] - const executableName = electronPlatformName === "linux" ? appInfo.sanitizedName.toLowerCase() : appInfo.productFilename + const executableName = packager instanceof LinuxPackager ? packager.executableName : packager.appInfo.productFilename const electronBinaryPath = path.join(appOutDir, `${executableName}${ext}`) log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses") - return flipFuses(electronBinaryPath, this.generateFuseConfig(electronPlatformName, arch, fuses)) + return flipFuses(electronBinaryPath, this.generateFuseConfig(fuses)) } - private generateFuseConfig(electronPlatformName: string, arch: Arch, fuses: ConfigurationFusesV1Options): FuseV1Config { + private generateFuseConfig(fuses: ConfigurationFusesV1Options): FuseV1Config { const config: FuseV1Config = { version: FuseVersion.V1, - resetAdHocDarwinSignature: fuses.resetAdHocDarwinSignature ?? (electronPlatformName === "darwin" && arch === Arch.universal), + resetAdHocDarwinSignature: fuses.resetAdHocDarwinSignature, } // this is annoying, but we must filter out undefined entries because some older electron versions will receive `the fuse wire in this version of Electron is not long enough` even if entry is set undefined if (fuses.runAsNode != null) { @@ -392,28 +410,6 @@ export abstract class PlatformPackager return config } - protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array) { - const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions) - const isAsar = asarOptions != null - const packContext = { - appOutDir, - outDir, - arch, - targets, - packager: this, - electronPlatformName: platformName, - } - const didSign = await this.signApp(packContext, isAsar) - const afterSign = await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign") - if (afterSign != null) { - if (didSign) { - await Promise.resolve(afterSign(packContext)) - } else { - log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) - } - } - } - // eslint-disable-next-line protected createTransformerForExtraFiles(packContext: AfterPackContext): FileTransformer | null { return null diff --git a/test/src/linux/debTest.ts b/test/src/linux/debTest.ts index 09fd05701a6..b315a0fbc33 100644 --- a/test/src/linux/debTest.ts +++ b/test/src/linux/debTest.ts @@ -22,6 +22,16 @@ test.ifNotWindows( deb: { depends: ["foo"], }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }) ) diff --git a/test/src/linux/flatpakTest.ts b/test/src/linux/flatpakTest.ts index d694c91c508..fae959be3e2 100644 --- a/test/src/linux/flatpakTest.ts +++ b/test/src/linux/flatpakTest.ts @@ -15,6 +15,18 @@ test.ifAll.ifDevOrLinuxCi( "flatpak", app({ targets: Platform.LINUX.createTarget("flatpak"), + config: { + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, + }, }) ) diff --git a/test/src/linux/fpmTest.ts b/test/src/linux/fpmTest.ts index 88ad4812038..9d5b8a32eb8 100644 --- a/test/src/linux/fpmTest.ts +++ b/test/src/linux/fpmTest.ts @@ -9,8 +9,42 @@ if (process.platform === "win32") { } // "apk" is very slow, don't test for now -test.ifAll.ifDevOrLinuxCi("targets", app({ targets: Platform.LINUX.createTarget(["sh", "freebsd", "pacman", "zip", "7z"]) })) +test.ifAll.ifDevOrLinuxCi( + "targets", + app({ + targets: Platform.LINUX.createTarget(["sh", "freebsd", "pacman", "zip", "7z"]), + config: { + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, + }, + }) +) // https://github.com/electron-userland/electron-builder/issues/460 // for some reasons in parallel to fmp we cannot use tar -test.ifAll.ifDevOrLinuxCi("rpm and tar.gz", app({ targets: Platform.LINUX.createTarget(["rpm", "tar.gz"]) })) +test.ifAll.ifDevOrLinuxCi( + "rpm and tar.gz", + app({ + targets: Platform.LINUX.createTarget(["rpm", "tar.gz"]), + config: { + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, + }, + }) +) diff --git a/test/src/linux/linuxArchiveTest.ts b/test/src/linux/linuxArchiveTest.ts index 295a0f02d16..4bdd433968b 100644 --- a/test/src/linux/linuxArchiveTest.ts +++ b/test/src/linux/linuxArchiveTest.ts @@ -1,4 +1,21 @@ import { Platform } from "electron-builder" import { app } from "../helpers/packTester" -test.ifAll.ifNotWindows.ifDevOrLinuxCi("tar", app({ targets: Platform.LINUX.createTarget(["tar.xz", "tar.lz", "tar.bz2"]) })) +test.ifAll.ifNotWindows.ifDevOrLinuxCi( + "tar", + app({ + targets: Platform.LINUX.createTarget(["tar.xz", "tar.lz", "tar.bz2"]), + config: { + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, + }, + }) +) diff --git a/test/src/linux/linuxPackagerTest.ts b/test/src/linux/linuxPackagerTest.ts index 04c3c02eefc..284fa3a7e77 100644 --- a/test/src/linux/linuxPackagerTest.ts +++ b/test/src/linux/linuxPackagerTest.ts @@ -35,7 +35,7 @@ test.ifNotWindows( onlyLoadAppFromAsar: true, loadBrowserProcessSpecificV8Snapshot: true, grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests - } + }, }, }) ) @@ -147,6 +147,16 @@ test.ifNotWindows.ifNotCiMac( // tslint:disable-next-line:no-invalid-template-strings artifactName: "boo-${productName}", }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, effectiveOptionComputed: async it => { const content: string = it.desktop diff --git a/test/src/linux/snapHeavyTest.ts b/test/src/linux/snapHeavyTest.ts index 23300f697e0..c7760d21a5e 100644 --- a/test/src/linux/snapHeavyTest.ts +++ b/test/src/linux/snapHeavyTest.ts @@ -21,6 +21,16 @@ test.ifAll( snap: { useTemplateApp: false, }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }) ) diff --git a/test/src/linux/snapTest.ts b/test/src/linux/snapTest.ts index e8919824e2f..2448aa40a7a 100644 --- a/test/src/linux/snapTest.ts +++ b/test/src/linux/snapTest.ts @@ -20,6 +20,16 @@ test.ifAll.ifDevOrLinuxCi( name: "sep", }, productName: "Sep", + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }) ) diff --git a/test/src/mac/macPackagerTest.ts b/test/src/mac/macPackagerTest.ts index 570080d8202..ff2ff9f165b 100644 --- a/test/src/mac/macPackagerTest.ts +++ b/test/src/mac/macPackagerTest.ts @@ -33,7 +33,7 @@ test.ifMac.ifAll("two-package", () => onlyLoadAppFromAsar: true, loadBrowserProcessSpecificV8Snapshot: true, grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests - } + }, }, }, { diff --git a/test/src/windows/appxTest.ts b/test/src/windows/appxTest.ts index 9638eba0a5e..591224af0d2 100644 --- a/test/src/windows/appxTest.ts +++ b/test/src/windows/appxTest.ts @@ -15,6 +15,18 @@ it.ifDevOrWinCi( app( { targets: Platform.WINDOWS.createTarget(["appx"], Arch.x64), + config: { + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, + }, }, { projectDirCreated: async projectDir => { diff --git a/test/src/windows/assistedInstallerTest.ts b/test/src/windows/assistedInstallerTest.ts index 16798257c65..d34addcf0e3 100644 --- a/test/src/windows/assistedInstallerTest.ts +++ b/test/src/windows/assistedInstallerTest.ts @@ -19,6 +19,16 @@ test.ifNotCiMac( win: { legalTrademarks: "My Trademark", }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }, { diff --git a/test/src/windows/msiTest.ts b/test/src/windows/msiTest.ts index 0a9d76a87d8..a1ea2deaac7 100644 --- a/test/src/windows/msiTest.ts +++ b/test/src/windows/msiTest.ts @@ -13,6 +13,16 @@ test.ifAll.ifDevOrWinCi( // version: "1.0.0", }, productName: "Test MSI", + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }, { diff --git a/test/src/windows/msiWrappedTest.ts b/test/src/windows/msiWrappedTest.ts index 9f285a37886..939b1a00620 100644 --- a/test/src/windows/msiWrappedTest.ts +++ b/test/src/windows/msiWrappedTest.ts @@ -23,6 +23,16 @@ test.ifAll.ifDevOrWinCi( win: { target: ["msiWrapped"], }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }, {}, diff --git a/test/src/windows/oneClickInstallerTest.ts b/test/src/windows/oneClickInstallerTest.ts index c55ca3b49c6..b53c5eb4b19 100644 --- a/test/src/windows/oneClickInstallerTest.ts +++ b/test/src/windows/oneClickInstallerTest.ts @@ -43,6 +43,16 @@ test( deleteAppDataOnUninstall: true, packElevateHelper: false, }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }, { @@ -332,6 +342,16 @@ test.skip.ifWindows( win: { executableName: "Boo", }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, effectiveOptionComputed: async it => { expect(pickSnapshotDefines(it[0])).toMatchSnapshot() diff --git a/test/src/windows/portableTest.ts b/test/src/windows/portableTest.ts index e3d201df510..83e0c9c4e16 100644 --- a/test/src/windows/portableTest.ts +++ b/test/src/windows/portableTest.ts @@ -12,6 +12,16 @@ test.ifAll.ifNotCiMac( nsis: { differentialPackage: false, }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }) ) diff --git a/test/src/windows/squirrelWindowsTest.ts b/test/src/windows/squirrelWindowsTest.ts index 3bbcd5c35d9..78b622461ea 100644 --- a/test/src/windows/squirrelWindowsTest.ts +++ b/test/src/windows/squirrelWindowsTest.ts @@ -12,6 +12,17 @@ test.ifAll.ifNotCiMac( win: { compression: "normal", }, + executableName: " test with spaces", + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }, { signedWin: true } diff --git a/test/src/windows/webInstallerTest.ts b/test/src/windows/webInstallerTest.ts index 29d621e84fa..5e11f605e62 100644 --- a/test/src/windows/webInstallerTest.ts +++ b/test/src/windows/webInstallerTest.ts @@ -13,6 +13,16 @@ test.ifNotCiMac( bucket: "develar", path: "test", }, + electronFuses: { + runAsNode: true, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: true, + enableNodeCliInspectArguments: true, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: true, + grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests + }, }, }) ) diff --git a/test/src/windows/winPackagerTest.ts b/test/src/windows/winPackagerTest.ts index 8f10609a2eb..79a47da5c1e 100644 --- a/test/src/windows/winPackagerTest.ts +++ b/test/src/windows/winPackagerTest.ts @@ -31,7 +31,7 @@ test.ifNotCiMac( onlyLoadAppFromAsar: true, loadBrowserProcessSpecificV8Snapshot: true, grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests - } + }, }, }) ) From f5cc9ed4352a9c42343d4e0fe4dde6067d33d952 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 11 Oct 2024 13:13:57 -0700 Subject: [PATCH 4/7] extract logic to allow public access to `addElectronFuses` --- .../app-builder-lib/src/platformPackager.ts | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index a1629a3e619..3792e287c12 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -31,8 +31,8 @@ import { executeAppBuilderAsJson } from "./util/appBuilder" import { computeFileSets, computeNodeModuleFileSets, copyAppFiles, ELECTRON_COMPILE_SHIM_FILENAME, transformFiles } from "./util/appFileCopier" import { expandMacro as doExpandMacro } from "./util/macroExpander" import { resolveFunction } from "./util/resolve" -import { flipFuses, FuseV1Config, FuseV1Options, FuseVersion } from "@electron/fuses" -import { FuseOptionsV1 as ConfigurationFusesV1Options } from "./configuration" +import { flipFuses, FuseConfig, FuseV1Config, FuseV1Options, FuseVersion } from "@electron/fuses" +import { FuseOptionsV1 } from "./configuration" export abstract class PlatformPackager { get packagerOptions(): PackagerOptions { @@ -348,7 +348,8 @@ export abstract class PlatformPackager } // the fuses MUST be flipped right before signing if (this.config.electronFuses != null) { - await this.addElectronFuses(packContext, this.config.electronFuses) + const fuseConfig = this.generateFuseConfig(this.config.electronFuses) + await this.addElectronFuses(packContext, fuseConfig) } const didSign = await this.signApp(packContext, isAsar) const afterSign = await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign") @@ -361,23 +362,7 @@ export abstract class PlatformPackager } } - public async addElectronFuses(context: AfterPackContext, fuses: ConfigurationFusesV1Options) { - const { appOutDir, electronPlatformName, packager } = context - - const ext = { - darwin: ".app", - win32: ".exe", - linux: "", - }[electronPlatformName] - - const executableName = packager instanceof LinuxPackager ? packager.executableName : packager.appInfo.productFilename - const electronBinaryPath = path.join(appOutDir, `${executableName}${ext}`) - - log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses") - return flipFuses(electronBinaryPath, this.generateFuseConfig(fuses)) - } - - private generateFuseConfig(fuses: ConfigurationFusesV1Options): FuseV1Config { + private generateFuseConfig(fuses: FuseOptionsV1): FuseV1Config { const config: FuseV1Config = { version: FuseVersion.V1, resetAdHocDarwinSignature: fuses.resetAdHocDarwinSignature, @@ -410,6 +395,22 @@ export abstract class PlatformPackager return config } + public async addElectronFuses(context: AfterPackContext, fuses: FuseConfig) { + const { appOutDir, electronPlatformName, packager } = context + + const ext = { + darwin: ".app", + win32: ".exe", + linux: "", + }[electronPlatformName] + + const executableName = packager instanceof LinuxPackager ? packager.executableName : packager.appInfo.productFilename + const electronBinaryPath = path.join(appOutDir, `${executableName}${ext}`) + + log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses") + return flipFuses(electronBinaryPath, fuses) + } + // eslint-disable-next-line protected createTransformerForExtraFiles(packContext: AfterPackContext): FileTransformer | null { return null From 25d4131f149a9e6272417b35d42a8e69abab8d22 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 11 Oct 2024 17:31:16 -0700 Subject: [PATCH 5/7] moving fuses logic outside of signing function --- .../app-builder-lib/src/platformPackager.ts | 61 +++++++++++-------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 3792e287c12..3e87a06637f 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -330,35 +330,14 @@ export abstract class PlatformPackager const isAsar = asarOptions != null await this.sanityCheckPackage(appOutDir, isAsar, framework, !!this.config.disableSanityCheckAsar) - if (sign) { - await this.doSignAfterPack(outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets) - } - } - protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array) { - const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions) - const isAsar = asarOptions != null - const packContext = { - appOutDir, - outDir, - arch, - targets, - packager: this, - electronPlatformName: platformName, - } // the fuses MUST be flipped right before signing if (this.config.electronFuses != null) { const fuseConfig = this.generateFuseConfig(this.config.electronFuses) await this.addElectronFuses(packContext, fuseConfig) } - const didSign = await this.signApp(packContext, isAsar) - const afterSign = await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign") - if (afterSign != null) { - if (didSign) { - await Promise.resolve(afterSign(packContext)) - } else { - log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) - } + if (sign) { + await this.doSignAfterPack(outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets) } } @@ -395,8 +374,18 @@ export abstract class PlatformPackager return config } + /** + * Use `AfterPackContext` here to keep available for public API + * @param {AfterPackContext} context + * @param {FuseConfig} fuses + * + * Can be used in `afterPack` hook for custom fuse logic like below. It's an alternative approach if one wants to override electron-builder's @electron/fuses version + * ``` + * await context.packager.addElectronFuses(context, { ... }) + * ``` + */ public async addElectronFuses(context: AfterPackContext, fuses: FuseConfig) { - const { appOutDir, electronPlatformName, packager } = context + const { appOutDir, electronPlatformName } = context const ext = { darwin: ".app", @@ -404,13 +393,35 @@ export abstract class PlatformPackager linux: "", }[electronPlatformName] - const executableName = packager instanceof LinuxPackager ? packager.executableName : packager.appInfo.productFilename + const executableName = this instanceof LinuxPackager ? this.executableName : this.appInfo.productFilename const electronBinaryPath = path.join(appOutDir, `${executableName}${ext}`) log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses") return flipFuses(electronBinaryPath, fuses) } + protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array) { + const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions) + const isAsar = asarOptions != null + const packContext = { + appOutDir, + outDir, + arch, + targets, + packager: this, + electronPlatformName: platformName, + } + const didSign = await this.signApp(packContext, isAsar) + const afterSign = await resolveFunction(this.appInfo.type, this.config.afterSign, "afterSign") + if (afterSign != null) { + if (didSign) { + await Promise.resolve(afterSign(packContext)) + } else { + log.warn(null, `skipping "afterSign" hook as no signing occurred, perhaps you intended "afterPack"?`) + } + } + } + // eslint-disable-next-line protected createTransformerForExtraFiles(packContext: AfterPackContext): FileTransformer | null { return null From 957e02ab2315c6d3a7497749383f11c7af3fd6fb Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 12 Oct 2024 08:39:03 -0700 Subject: [PATCH 6/7] add more docs and add tutorial page to documentation site --- mkdocs.yml | 1 + packages/app-builder-lib/scheme.json | 18 +++--- packages/app-builder-lib/src/configuration.ts | 25 +++++--- packages/app-builder-lib/src/index.ts | 13 ++++- pages/tutorials/adding-electron-fuses.md | 58 +++++++++++++++++++ .../code-signing-windows-apps-on-unix.md | 4 +- 6 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 pages/tutorials/adding-electron-fuses.md diff --git a/mkdocs.yml b/mkdocs.yml index b189066156a..6682d083bce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -123,6 +123,7 @@ nav: - Multi Platform Build: multi-platform-build.md - Tutorials: + - Configuring Electron Fuses: tutorials/adding-electron-fuses.md - Loading App Dependencies Manually: tutorials/loading-app-dependencies-manually.md - Two package.json Structure: tutorials/two-package-structure.md - macOS Kernel Extensions: tutorials/macos-kernel-extensions.md diff --git a/packages/app-builder-lib/scheme.json b/packages/app-builder-lib/scheme.json index 3d9f1a51167..065f9a0a28b 100644 --- a/packages/app-builder-lib/scheme.json +++ b/packages/app-builder-lib/scheme.json @@ -1286,34 +1286,34 @@ }, "FuseOptionsV1": { "additionalProperties": false, - "description": "All options come from [@electron/fuses](https://github.com/electron/fuses)", + "description": "All options come from [@electron/fuses](https://github.com/electron/fuses)\nRef: https://raw.githubusercontent.com/electron/electron/refs/heads/main/docs/tutorial/fuses.md", "properties": { "enableCookieEncryption": { - "description": "Enables cookie encryption", + "description": "The cookieEncryption fuse toggles whether the cookie store on disk is encrypted using OS level cryptography keys. By default the sqlite database that Chromium uses to store cookies stores the values in plaintext. If you wish to ensure your apps cookies are encrypted in the same way Chrome does then you should enable this fuse. Please note it is a one-way transition, if you enable this fuse existing unencrypted cookies will be encrypted-on-write but if you then disable the fuse again your cookie store will effectively be corrupt and useless. Most apps can safely enable this fuse.", "type": "boolean" }, "enableEmbeddedAsarIntegrityValidation": { - "description": "Enables validation of the app.asar archive on macOS", + "description": "The embeddedAsarIntegrityValidation fuse toggles an experimental feature on macOS that validates the content of the `app.asar` file when it is loaded. This feature is designed to have a minimal performance impact but may marginally slow down file reads from inside the `app.asar` archive.\nCurrently, ASAR integrity checking is supported on:\n- macOS as of electron>=16.0.0\n- Windows as of electron>=30.0.0\nFor more information on how to use asar integrity validation please read the [Asar Integrity](https://github.com/electron/electron/blob/main/docs/tutorial/asar-integrity.md) documentation.", "type": "boolean" }, "enableNodeCliInspectArguments": { - "description": "Disables the --inspect and --inspect-brk family of CLI options", + "description": "The nodeCliInspect fuse toggles whether the `--inspect`, `--inspect-brk`, etc. flags are respected or not. When disabled it also ensures that `SIGUSR1` signal does not initialize the main process inspector. Most apps can safely disable this fuse.", "type": "boolean" }, "enableNodeOptionsEnvironmentVariable": { - "description": "Disables the NODE_OPTIONS environment variable", + "description": "The nodeOptions fuse toggles whether the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#node_optionsoptions) and [`NODE_EXTRA_CA_CERTS`](https://github.com/nodejs/node/blob/main/doc/api/cli.md#node_extra_ca_certsfile) environment variables are respected. The `NODE_OPTIONS` environment variable can be used to pass all kinds of custom options to the Node.js runtime and isn't typically used by apps in production. Most apps can safely disable this fuse.", "type": "boolean" }, "grantFileProtocolExtraPrivileges": { - "description": "Grants the file protocol extra privileges", + "description": "The grantFileProtocolExtraPrivileges fuse changes whether pages loaded from the `file://` protocol are given privileges beyond what they would receive in a traditional web browser. This behavior was core to Electron apps in original versions of Electron but is no longer required as apps should be [serving local files from custom protocols](https://github.com/electron/electron/blob/main/docs/tutorial/security.md#18-avoid-usage-of-the-file-protocol-and-prefer-usage-of-custom-protocols) now instead. If you aren't serving pages from `file://` you should disable this fuse.\nThe extra privileges granted to the `file://` protocol by this fuse are incompletely documented below:\n- `file://` protocol pages can use `fetch` to load other assets over `file://`\n- `file://` protocol pages can use service workers\n- `file://` protocol pages have universal access granted to child frames also running on `file://` protocols regardless of sandbox settings", "type": "boolean" }, "loadBrowserProcessSpecificV8Snapshot": { - "description": "Loads V8 Snapshot from `browser_v8_context_snapshot.bin` for the browser process", + "description": "The loadBrowserProcessSpecificV8Snapshot fuse changes which V8 snapshot file is used for the browser process. By default Electron's processes will all use the same V8 snapshot file. When this fuse is enabled the browser process uses the file called `browser_v8_context_snapshot.bin` for its V8 snapshot. The other processes will use the V8 snapshot file that they normally do.", "type": "boolean" }, "onlyLoadAppFromAsar": { - "description": "Enforces that Electron will only load your app from \"app.asar\" instead of its normal search paths", + "description": "The onlyLoadAppFromAsar fuse changes the search system that Electron uses to locate your app code. By default Electron will search in the following order `app.asar` -> `app` -> `default_app.asar`. When this fuse is enabled the search order becomes a single entry `app.asar` thus ensuring that when combined with the `embeddedAsarIntegrityValidation` fuse it is impossible to load non-validated code.", "type": "boolean" }, "resetAdHocDarwinSignature": { @@ -1321,7 +1321,7 @@ "type": "boolean" }, "runAsNode": { - "description": "Disables ELECTRON_RUN_AS_NODE", + "description": "The runAsNode fuse toggles whether the `ELECTRON_RUN_AS_NODE` environment variable is respected or not. Please note that if this fuse is disabled then `process.fork` in the main process will not function as expected as it depends on this environment variable to function. Instead, we recommend that you use [Utility Processes](https://github.com/electron/electron/blob/main/docs/api/utility-process.md), which work for many use cases where you need a standalone Node.js process (like a Sqlite server process or similar scenarios).", "type": "boolean" } }, diff --git a/packages/app-builder-lib/src/configuration.ts b/packages/app-builder-lib/src/configuration.ts index 0d3ed79cafd..b0a37aac546 100644 --- a/packages/app-builder-lib/src/configuration.ts +++ b/packages/app-builder-lib/src/configuration.ts @@ -384,38 +384,47 @@ export interface MetadataDirectories { /** * All options come from [@electron/fuses](https://github.com/electron/fuses) + * Ref: https://raw.githubusercontent.com/electron/electron/refs/heads/main/docs/tutorial/fuses.md */ export interface FuseOptionsV1 { /** - * Disables ELECTRON_RUN_AS_NODE + *The runAsNode fuse toggles whether the `ELECTRON_RUN_AS_NODE` environment variable is respected or not. Please note that if this fuse is disabled then `process.fork` in the main process will not function as expected as it depends on this environment variable to function. Instead, we recommend that you use [Utility Processes](https://github.com/electron/electron/blob/main/docs/api/utility-process.md), which work for many use cases where you need a standalone Node.js process (like a Sqlite server process or similar scenarios). */ runAsNode?: boolean /** - * Enables cookie encryption + * The cookieEncryption fuse toggles whether the cookie store on disk is encrypted using OS level cryptography keys. By default the sqlite database that Chromium uses to store cookies stores the values in plaintext. If you wish to ensure your apps cookies are encrypted in the same way Chrome does then you should enable this fuse. Please note it is a one-way transition, if you enable this fuse existing unencrypted cookies will be encrypted-on-write but if you then disable the fuse again your cookie store will effectively be corrupt and useless. Most apps can safely enable this fuse. */ enableCookieEncryption?: boolean /** - * Disables the NODE_OPTIONS environment variable + * The nodeOptions fuse toggles whether the [`NODE_OPTIONS`](https://nodejs.org/api/cli.html#node_optionsoptions) and [`NODE_EXTRA_CA_CERTS`](https://github.com/nodejs/node/blob/main/doc/api/cli.md#node_extra_ca_certsfile) environment variables are respected. The `NODE_OPTIONS` environment variable can be used to pass all kinds of custom options to the Node.js runtime and isn't typically used by apps in production. Most apps can safely disable this fuse. */ enableNodeOptionsEnvironmentVariable?: boolean /** - * Disables the --inspect and --inspect-brk family of CLI options + * The nodeCliInspect fuse toggles whether the `--inspect`, `--inspect-brk`, etc. flags are respected or not. When disabled it also ensures that `SIGUSR1` signal does not initialize the main process inspector. Most apps can safely disable this fuse. */ enableNodeCliInspectArguments?: boolean /** - * Enables validation of the app.asar archive on macOS + * The embeddedAsarIntegrityValidation fuse toggles an experimental feature on macOS that validates the content of the `app.asar` file when it is loaded. This feature is designed to have a minimal performance impact but may marginally slow down file reads from inside the `app.asar` archive. + * Currently, ASAR integrity checking is supported on: + * - macOS as of electron>=16.0.0 + * - Windows as of electron>=30.0.0 + * For more information on how to use asar integrity validation please read the [Asar Integrity](https://github.com/electron/electron/blob/main/docs/tutorial/asar-integrity.md) documentation. */ enableEmbeddedAsarIntegrityValidation?: boolean /** - * Enforces that Electron will only load your app from "app.asar" instead of its normal search paths + * The onlyLoadAppFromAsar fuse changes the search system that Electron uses to locate your app code. By default Electron will search in the following order `app.asar` -> `app` -> `default_app.asar`. When this fuse is enabled the search order becomes a single entry `app.asar` thus ensuring that when combined with the `embeddedAsarIntegrityValidation` fuse it is impossible to load non-validated code. */ onlyLoadAppFromAsar?: boolean /** - * Loads V8 Snapshot from `browser_v8_context_snapshot.bin` for the browser process + * The loadBrowserProcessSpecificV8Snapshot fuse changes which V8 snapshot file is used for the browser process. By default Electron's processes will all use the same V8 snapshot file. When this fuse is enabled the browser process uses the file called `browser_v8_context_snapshot.bin` for its V8 snapshot. The other processes will use the V8 snapshot file that they normally do. */ loadBrowserProcessSpecificV8Snapshot?: boolean /** - * Grants the file protocol extra privileges + * The grantFileProtocolExtraPrivileges fuse changes whether pages loaded from the `file://` protocol are given privileges beyond what they would receive in a traditional web browser. This behavior was core to Electron apps in original versions of Electron but is no longer required as apps should be [serving local files from custom protocols](https://github.com/electron/electron/blob/main/docs/tutorial/security.md#18-avoid-usage-of-the-file-protocol-and-prefer-usage-of-custom-protocols) now instead. If you aren't serving pages from `file://` you should disable this fuse. + * The extra privileges granted to the `file://` protocol by this fuse are incompletely documented below: + * - `file://` protocol pages can use `fetch` to load other assets over `file://` + * - `file://` protocol pages can use service workers + * - `file://` protocol pages have universal access granted to child frames also running on `file://` protocols regardless of sandbox settings */ grantFileProtocolExtraPrivileges?: boolean /** diff --git a/packages/app-builder-lib/src/index.ts b/packages/app-builder-lib/src/index.ts index cf5c3188817..73bc8794b9f 100644 --- a/packages/app-builder-lib/src/index.ts +++ b/packages/app-builder-lib/src/index.ts @@ -21,7 +21,18 @@ export { CompressionLevel, } from "./core" export { getArchSuffix, Arch, archFromString } from "builder-util" -export { CommonConfiguration, Configuration, AfterPackContext, MetadataDirectories, BeforePackContext, AfterExtractContext, Hooks, Hook, PackContext } from "./configuration" +export { + CommonConfiguration, + Configuration, + AfterPackContext, + MetadataDirectories, + BeforePackContext, + AfterExtractContext, + Hooks, + Hook, + PackContext, + FuseOptionsV1, +} from "./configuration" export { ElectronBrandingOptions, ElectronDownloadOptions, ElectronPlatformName } from "./electron/ElectronFramework" export { PlatformSpecificBuildOptions, AsarOptions, FileSet, Protocol, ReleaseInfo, FilesBuildOptions } from "./options/PlatformSpecificBuildOptions" export { FileAssociation } from "./options/FileAssociation" diff --git a/pages/tutorials/adding-electron-fuses.md b/pages/tutorials/adding-electron-fuses.md new file mode 100644 index 00000000000..5f79c382d29 --- /dev/null +++ b/pages/tutorials/adding-electron-fuses.md @@ -0,0 +1,58 @@ +!!! note + Information below has been partially copied from integration with [@electron/fuses](https://github.com/electron/fuses) and [electron tutorial](https://raw.githubusercontent.com/electron/electron/refs/heads/main/docs/tutorial/fuses.md) for easier reading/access. + +## What are fuses? + +For a subset of Electron functionality it makes sense to disable certain features for an entire application. For example, 99% of apps don't make use of `ELECTRON_RUN_AS_NODE`, these applications want to be able to ship a binary that is incapable of using that feature. We also don't want Electron consumers building Electron from source as that is both a massive technical challenge and has a high cost of both time and money. + +Fuses are the solution to this problem, at a high level they are "magic bits" in the Electron binary that can be flipped when packaging your Electron app to enable / disable certain features / restrictions. Because they are flipped at package time before you code sign your app the OS becomes responsible for ensuring those bits aren't flipped back via OS level code signing validation (Gatekeeper / App Locker). + +## How do I flip the fuses? + +Under-the-hood, electron-builder leverages the official [`@electron/fuses`](https://npmjs.com/package/@electron/fuses) module to make flipping these fuses easy. Previously, electron fuses could only be flipped within the `afterPack` hook (this is still a supported method). Now, you can instead set electron-builder configuration property `electronFuses: FuseOptionsV1` to activate electron-builder's integration. + +### Example +!!! note + The true/false below are just an example, customize your configuration to your own requirements + +```js +electronFuses: { + runAsNode: false, + enableCookieEncryption: true, + enableNodeOptionsEnvironmentVariable: false, + enableNodeCliInspectArguments: false, + enableEmbeddedAsarIntegrityValidation: true, + onlyLoadAppFromAsar: true, + loadBrowserProcessSpecificV8Snapshot: false, + grantFileProtocolExtraPrivileges: false +} +``` + +It also is still possible to continue to keep your current logic flow in `afterPack` hook, so a convience method has been exposed in the `PlatformPackager` for easy customization of the flags on your own. It directly accepts an `AfterPackContext` and a `FuseConfig` object of [type](https://github.com/electron/fuses/blob/main/src/config.ts). +This convience method was opened so that custom FuseConfig's could be provided, allow usage of `strictlyRequireAllFuses` to monitor your fuses and stay up-to-date with fuses as they're released, and/or force override the version of @electron/fuses in electron-builder if there's an update you'd like to leverage. + +!!! example "afterPack.ts" + +```typescript + const { FuseConfig, FuseVersion } = require("@electron/fuses") + + exports.default = function (context: AfterPackContext) { + const fuses: FuseConfig = { + version: FuseVersion.V1, + strictlyRequireAllFuses: true, + [FuseV1Options.RunAsNode]: false, + ... // all other flags must be specified since `strictlyRequireAllFuses = true` + } + await context.packager.addElectronFuses(context, fuses) + } +``` + +## Validating Fuses + +You can validate the fuses have been flipped or check the fuse status of an arbitrary Electron app using the fuses CLI. + +```bash +npx @electron/fuses read --app /Applications/Foo.app +``` + +{!./app-builder-lib.Interface.FuseOptionsV1.md!} \ No newline at end of file diff --git a/pages/tutorials/code-signing-windows-apps-on-unix.md b/pages/tutorials/code-signing-windows-apps-on-unix.md index 22aa1d28117..e0e2b85a8b7 100644 --- a/pages/tutorials/code-signing-windows-apps-on-unix.md +++ b/pages/tutorials/code-signing-windows-apps-on-unix.md @@ -109,7 +109,9 @@ package.json ... "win": { "target": "nsis", - "sign": "./sign.js" + "signtoolOptions": { + "sign": "./sign.js" + } }, ... ``` From 523fe4cf983ec93cf2a1667329ffdbee7480dde5 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Sat, 12 Oct 2024 09:21:26 -0700 Subject: [PATCH 7/7] update docs and add additional `pnpm docs:preview` for easy local previewing of generated docs site --- package.json | 1 + packages/app-builder-lib/src/configuration.ts | 13 +++++---- pages/tutorials/adding-electron-fuses.md | 27 ++++++++++--------- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 56dee7cf669..15c32f791bc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "docs:prebuild": "docker build -t mkdocs-dockerfile -f mkdocs-dockerfile . ", "docs:build": "pnpm compile && node scripts/renderer/out/typedoc2html.js", "docs:mkdocs": "docker run --rm -v ${PWD}:/docs -v ${PWD}/site:/site mkdocs-dockerfile build", + "docs:preview": "open ./site/index.html", "docs:all": "pnpm docs:prebuild && pnpm docs:build && pnpm docs:mkdocs", "prepare": "husky install" }, diff --git a/packages/app-builder-lib/src/configuration.ts b/packages/app-builder-lib/src/configuration.ts index b0a37aac546..edf5556cd9e 100644 --- a/packages/app-builder-lib/src/configuration.ts +++ b/packages/app-builder-lib/src/configuration.ts @@ -406,8 +406,10 @@ export interface FuseOptionsV1 { /** * The embeddedAsarIntegrityValidation fuse toggles an experimental feature on macOS that validates the content of the `app.asar` file when it is loaded. This feature is designed to have a minimal performance impact but may marginally slow down file reads from inside the `app.asar` archive. * Currently, ASAR integrity checking is supported on: - * - macOS as of electron>=16.0.0 - * - Windows as of electron>=30.0.0 + * + * - macOS as of electron>=16.0.0 + * - Windows as of electron>=30.0.0 + * * For more information on how to use asar integrity validation please read the [Asar Integrity](https://github.com/electron/electron/blob/main/docs/tutorial/asar-integrity.md) documentation. */ enableEmbeddedAsarIntegrityValidation?: boolean @@ -422,9 +424,10 @@ export interface FuseOptionsV1 { /** * The grantFileProtocolExtraPrivileges fuse changes whether pages loaded from the `file://` protocol are given privileges beyond what they would receive in a traditional web browser. This behavior was core to Electron apps in original versions of Electron but is no longer required as apps should be [serving local files from custom protocols](https://github.com/electron/electron/blob/main/docs/tutorial/security.md#18-avoid-usage-of-the-file-protocol-and-prefer-usage-of-custom-protocols) now instead. If you aren't serving pages from `file://` you should disable this fuse. * The extra privileges granted to the `file://` protocol by this fuse are incompletely documented below: - * - `file://` protocol pages can use `fetch` to load other assets over `file://` - * - `file://` protocol pages can use service workers - * - `file://` protocol pages have universal access granted to child frames also running on `file://` protocols regardless of sandbox settings + * + * - `file://` protocol pages can use `fetch` to load other assets over `file://` + * - `file://` protocol pages can use service workers + * - `file://` protocol pages have universal access granted to child frames also running on `file://` protocols regardless of sandbox settings */ grantFileProtocolExtraPrivileges?: boolean /** diff --git a/pages/tutorials/adding-electron-fuses.md b/pages/tutorials/adding-electron-fuses.md index 5f79c382d29..6c5b9415deb 100644 --- a/pages/tutorials/adding-electron-fuses.md +++ b/pages/tutorials/adding-electron-fuses.md @@ -13,9 +13,9 @@ Under-the-hood, electron-builder leverages the official [`@electron/fuses`](http ### Example !!! note - The true/false below are just an example, customize your configuration to your own requirements + The true/false below are just an example, customize your configuration to your own requirements -```js +```typescript electronFuses: { runAsNode: false, enableCookieEncryption: true, @@ -34,17 +34,17 @@ This convience method was opened so that custom FuseConfig's could be provided, !!! example "afterPack.ts" ```typescript - const { FuseConfig, FuseVersion } = require("@electron/fuses") - - exports.default = function (context: AfterPackContext) { - const fuses: FuseConfig = { - version: FuseVersion.V1, - strictlyRequireAllFuses: true, - [FuseV1Options.RunAsNode]: false, - ... // all other flags must be specified since `strictlyRequireAllFuses = true` - } - await context.packager.addElectronFuses(context, fuses) - } +const { FuseConfig, FuseVersion, FuseV1Options } = require("@electron/fuses") + +exports.default = function (context: AfterPackContext) { + const fuses: FuseConfig = { + version: FuseVersion.V1, + strictlyRequireAllFuses: true, + [FuseV1Options.RunAsNode]: false, + ... // all other flags must be specified since `strictlyRequireAllFuses = true` + } + await context.packager.addElectronFuses(context, fuses) +} ``` ## Validating Fuses @@ -55,4 +55,5 @@ You can validate the fuses have been flipped or check the fuse status of an arbi npx @electron/fuses read --app /Applications/Foo.app ``` +## Typedoc {!./app-builder-lib.Interface.FuseOptionsV1.md!} \ No newline at end of file