Skip to content

Commit 8434e10

Browse files
authored
feat: add integration for @electron/fuses (#8588)
1 parent c848430 commit 8434e10

28 files changed

+508
-5
lines changed

.changeset/two-rice-wash.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"app-builder-lib": minor
3+
---
4+
5+
feat: adding integration with @electron/fuses

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ nav:
123123
- Multi Platform Build: multi-platform-build.md
124124

125125
- Tutorials:
126+
- Configuring Electron Fuses: tutorials/adding-electron-fuses.md
126127
- Loading App Dependencies Manually: tutorials/loading-app-dependencies-manually.md
127128
- Two package.json Structure: tutorials/two-package-structure.md
128129
- macOS Kernel Extensions: tutorials/macos-kernel-extensions.md

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"docs:prebuild": "docker build -t mkdocs-dockerfile -f mkdocs-dockerfile . ",
3535
"docs:build": "pnpm compile && node scripts/renderer/out/typedoc2html.js",
3636
"docs:mkdocs": "docker run --rm -v ${PWD}:/docs -v ${PWD}/site:/site mkdocs-dockerfile build",
37+
"docs:preview": "open ./site/index.html",
3738
"docs:all": "pnpm docs:prebuild && pnpm docs:build && pnpm docs:mkdocs",
3839
"prepare": "husky install"
3940
},

packages/app-builder-lib/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"homepage": "https://github.com/electron-userland/electron-builder",
4848
"dependencies": {
4949
"@develar/schema-utils": "~2.6.5",
50+
"@electron/fuses": "^1.8.0",
5051
"@electron/asar": "^3.2.13",
5152
"@electron/notarize": "2.5.0",
5253
"@electron/osx-sign": "1.3.1",

packages/app-builder-lib/scheme.json

+54
Original file line numberDiff line numberDiff line change
@@ -1284,6 +1284,49 @@
12841284
},
12851285
"type": "object"
12861286
},
1287+
"FuseOptionsV1": {
1288+
"additionalProperties": false,
1289+
"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",
1290+
"properties": {
1291+
"enableCookieEncryption": {
1292+
"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.",
1293+
"type": "boolean"
1294+
},
1295+
"enableEmbeddedAsarIntegrityValidation": {
1296+
"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.",
1297+
"type": "boolean"
1298+
},
1299+
"enableNodeCliInspectArguments": {
1300+
"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.",
1301+
"type": "boolean"
1302+
},
1303+
"enableNodeOptionsEnvironmentVariable": {
1304+
"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.",
1305+
"type": "boolean"
1306+
},
1307+
"grantFileProtocolExtraPrivileges": {
1308+
"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",
1309+
"type": "boolean"
1310+
},
1311+
"loadBrowserProcessSpecificV8Snapshot": {
1312+
"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.",
1313+
"type": "boolean"
1314+
},
1315+
"onlyLoadAppFromAsar": {
1316+
"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.",
1317+
"type": "boolean"
1318+
},
1319+
"resetAdHocDarwinSignature": {
1320+
"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",
1321+
"type": "boolean"
1322+
},
1323+
"runAsNode": {
1324+
"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).",
1325+
"type": "boolean"
1326+
}
1327+
},
1328+
"type": "object"
1329+
},
12871330
"GenericServerOptions": {
12881331
"additionalProperties": false,
12891332
"description": "Generic (any HTTP(S) server) options.\nIn all publish options [File Macros](./file-patterns.md#file-macros) are supported.",
@@ -7023,6 +7066,17 @@
70237066
"$ref": "#/definitions/ElectronDownloadOptions",
70247067
"description": "The [electron-download](https://github.com/electron-userland/electron-download#usage) options."
70257068
},
7069+
"electronFuses": {
7070+
"anyOf": [
7071+
{
7072+
"$ref": "#/definitions/FuseOptionsV1"
7073+
},
7074+
{
7075+
"type": "null"
7076+
}
7077+
],
7078+
"description": "Options to pass to `@electron/fuses`\nRef: https://github.com/electron/fuses"
7079+
},
70267080
"electronLanguages": {
70277081
"anyOf": [
70287082
{

packages/app-builder-lib/src/configuration.ts

+62
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ export interface CommonConfiguration {
181181
* @default true
182182
*/
183183
readonly removePackageKeywords?: boolean
184+
185+
/**
186+
* Options to pass to `@electron/fuses`
187+
* Ref: https://github.com/electron/fuses
188+
*/
189+
readonly electronFuses?: FuseOptionsV1 | null
184190
}
185191
export interface Configuration extends CommonConfiguration, PlatformSpecificBuildOptions, Hooks {
186192
/**
@@ -375,3 +381,59 @@ export interface MetadataDirectories {
375381
*/
376382
readonly app?: string | null
377383
}
384+
385+
/**
386+
* All options come from [@electron/fuses](https://github.com/electron/fuses)
387+
* Ref: https://raw.githubusercontent.com/electron/electron/refs/heads/main/docs/tutorial/fuses.md
388+
*/
389+
export interface FuseOptionsV1 {
390+
/**
391+
*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).
392+
*/
393+
runAsNode?: boolean
394+
/**
395+
* 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.
396+
*/
397+
enableCookieEncryption?: boolean
398+
/**
399+
* 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.
400+
*/
401+
enableNodeOptionsEnvironmentVariable?: boolean
402+
/**
403+
* 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.
404+
*/
405+
enableNodeCliInspectArguments?: boolean
406+
/**
407+
* 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.
408+
* Currently, ASAR integrity checking is supported on:
409+
*
410+
* - macOS as of electron>=16.0.0
411+
* - Windows as of electron>=30.0.0
412+
*
413+
* 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.
414+
*/
415+
enableEmbeddedAsarIntegrityValidation?: boolean
416+
/**
417+
* 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.
418+
*/
419+
onlyLoadAppFromAsar?: boolean
420+
/**
421+
* 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.
422+
*/
423+
loadBrowserProcessSpecificV8Snapshot?: boolean
424+
/**
425+
* 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.
426+
* The extra privileges granted to the `file://` protocol by this fuse are incompletely documented below:
427+
*
428+
* - `file://` protocol pages can use `fetch` to load other assets over `file://`
429+
* - `file://` protocol pages can use service workers
430+
* - `file://` protocol pages have universal access granted to child frames also running on `file://` protocols regardless of sandbox settings
431+
*/
432+
grantFileProtocolExtraPrivileges?: boolean
433+
/**
434+
* Resets the app signature, specifically used for macOS.
435+
* Note: This should be unneeded since electron-builder signs the app directly after flipping the fuses.
436+
* Ref: https://github.com/electron/fuses?tab=readme-ov-file#apple-silicon
437+
*/
438+
resetAdHocDarwinSignature?: boolean
439+
}

packages/app-builder-lib/src/index.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,18 @@ export {
2121
CompressionLevel,
2222
} from "./core"
2323
export { getArchSuffix, Arch, archFromString } from "builder-util"
24-
export { CommonConfiguration, Configuration, AfterPackContext, MetadataDirectories, BeforePackContext, AfterExtractContext, Hooks, Hook, PackContext } from "./configuration"
24+
export {
25+
CommonConfiguration,
26+
Configuration,
27+
AfterPackContext,
28+
MetadataDirectories,
29+
BeforePackContext,
30+
AfterExtractContext,
31+
Hooks,
32+
Hook,
33+
PackContext,
34+
FuseOptionsV1,
35+
} from "./configuration"
2536
export { ElectronBrandingOptions, ElectronDownloadOptions, ElectronPlatformName } from "./electron/ElectronFramework"
2637
export { PlatformSpecificBuildOptions, AsarOptions, FileSet, Protocol, ReleaseInfo, FilesBuildOptions } from "./options/PlatformSpecificBuildOptions"
2738
export { FileAssociation } from "./options/FileAssociation"

packages/app-builder-lib/src/platformPackager.ts

+68
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
Configuration,
2020
ElectronPlatformName,
2121
FileAssociation,
22+
LinuxPackager,
2223
Packager,
2324
PackagerOptions,
2425
Platform,
@@ -30,6 +31,8 @@ import { executeAppBuilderAsJson } from "./util/appBuilder"
3031
import { computeFileSets, computeNodeModuleFileSets, copyAppFiles, ELECTRON_COMPILE_SHIM_FILENAME, transformFiles } from "./util/appFileCopier"
3132
import { expandMacro as doExpandMacro } from "./util/macroExpander"
3233
import { resolveFunction } from "./util/resolve"
34+
import { flipFuses, FuseConfig, FuseV1Config, FuseV1Options, FuseVersion } from "@electron/fuses"
35+
import { FuseOptionsV1 } from "./configuration"
3336

3437
export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions> {
3538
get packagerOptions(): PackagerOptions {
@@ -327,11 +330,76 @@ export abstract class PlatformPackager<DC extends PlatformSpecificBuildOptions>
327330

328331
const isAsar = asarOptions != null
329332
await this.sanityCheckPackage(appOutDir, isAsar, framework, !!this.config.disableSanityCheckAsar)
333+
334+
// the fuses MUST be flipped right before signing
335+
if (this.config.electronFuses != null) {
336+
const fuseConfig = this.generateFuseConfig(this.config.electronFuses)
337+
await this.addElectronFuses(packContext, fuseConfig)
338+
}
330339
if (sign) {
331340
await this.doSignAfterPack(outDir, appOutDir, platformName, arch, platformSpecificBuildOptions, targets)
332341
}
333342
}
334343

344+
private generateFuseConfig(fuses: FuseOptionsV1): FuseV1Config {
345+
const config: FuseV1Config = {
346+
version: FuseVersion.V1,
347+
resetAdHocDarwinSignature: fuses.resetAdHocDarwinSignature,
348+
}
349+
// 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
350+
if (fuses.runAsNode != null) {
351+
config[FuseV1Options.RunAsNode] = fuses.runAsNode
352+
}
353+
if (fuses.enableCookieEncryption != null) {
354+
config[FuseV1Options.EnableCookieEncryption] = fuses.enableCookieEncryption
355+
}
356+
if (fuses.enableNodeOptionsEnvironmentVariable != null) {
357+
config[FuseV1Options.EnableNodeOptionsEnvironmentVariable] = fuses.enableNodeOptionsEnvironmentVariable
358+
}
359+
if (fuses.enableNodeCliInspectArguments != null) {
360+
config[FuseV1Options.EnableNodeCliInspectArguments] = fuses.enableNodeCliInspectArguments
361+
}
362+
if (fuses.enableEmbeddedAsarIntegrityValidation != null) {
363+
config[FuseV1Options.EnableEmbeddedAsarIntegrityValidation] = fuses.enableEmbeddedAsarIntegrityValidation
364+
}
365+
if (fuses.onlyLoadAppFromAsar != null) {
366+
config[FuseV1Options.OnlyLoadAppFromAsar] = fuses.onlyLoadAppFromAsar
367+
}
368+
if (fuses.loadBrowserProcessSpecificV8Snapshot != null) {
369+
config[FuseV1Options.LoadBrowserProcessSpecificV8Snapshot] = fuses.loadBrowserProcessSpecificV8Snapshot
370+
}
371+
if (fuses.grantFileProtocolExtraPrivileges != null) {
372+
config[FuseV1Options.GrantFileProtocolExtraPrivileges] = fuses.grantFileProtocolExtraPrivileges
373+
}
374+
return config
375+
}
376+
377+
/**
378+
* Use `AfterPackContext` here to keep available for public API
379+
* @param {AfterPackContext} context
380+
* @param {FuseConfig} fuses
381+
*
382+
* 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
383+
* ```
384+
* await context.packager.addElectronFuses(context, { ... })
385+
* ```
386+
*/
387+
public async addElectronFuses(context: AfterPackContext, fuses: FuseConfig) {
388+
const { appOutDir, electronPlatformName } = context
389+
390+
const ext = {
391+
darwin: ".app",
392+
win32: ".exe",
393+
linux: "",
394+
}[electronPlatformName]
395+
396+
const executableName = this instanceof LinuxPackager ? this.executableName : this.appInfo.productFilename
397+
const electronBinaryPath = path.join(appOutDir, `${executableName}${ext}`)
398+
399+
log.info({ electronPath: log.filePath(electronBinaryPath) }, "executing @electron/fuses")
400+
return flipFuses(electronBinaryPath, fuses)
401+
}
402+
335403
protected async doSignAfterPack(outDir: string, appOutDir: string, platformName: ElectronPlatformName, arch: Arch, platformSpecificBuildOptions: DC, targets: Array<Target>) {
336404
const asarOptions = await this.computeAsarOptions(platformSpecificBuildOptions)
337405
const isAsar = asarOptions != null

0 commit comments

Comments
 (0)