Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Electron Fuses support #6365

Closed
kaatt opened this issue Oct 22, 2021 · 14 comments · Fixed by #8588
Closed

Electron Fuses support #6365

kaatt opened this issue Oct 22, 2021 · 14 comments · Fixed by #8588

Comments

@kaatt
Copy link

kaatt commented Oct 22, 2021

I'm flipping fuses in my afterPack script. Is there a better way to get the path to the final packaged Electron executable?

const path = require('path')
const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses')

module.exports = async function afterPack(context) {
  const ext = {
    darwin: '.app',
    win32: '.exe',
  }[context.electronPlatformName]

  const electronBinaryPath = path.join(context.appOutDir, context.packager.appInfo.productFilename + ext) // is there a better way?
  await flipFuses(
    electronBinaryPath,
    {
      version: FuseVersion.V1,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
    },
  )
}
@liode1s
Copy link

liode1s commented Dec 28, 2021

Hi Brother, I also encountered this problem. Electron-builder is an installer, and you cannot run Fuses directly. I obtained the generated folder and packaged it again with other packaging software. I think this function is a very important function. option configure Fuses! This is great!

@stale
Copy link

stale bot commented Apr 16, 2022

Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward?

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.

@stale stale bot added the backlog label Apr 16, 2022
@stale stale bot closed this as completed Apr 30, 2022
@kaatt
Copy link
Author

kaatt commented May 29, 2022

Relevant, please reopen.

@mmaietta
Copy link
Collaborator

mmaietta commented Jul 4, 2022

This is how I pull the executable in my work project:

import * as builder from "electron-builder";

const appExecutableDestinationPath = (context: builder.AfterPackContext) => {
    const { packager, appOutDir, electronPlatformName } = context;
    const resourcesDir = packager.getResourcesDir(appOutDir);
    return path.join(resourcesDir, "../", electronPlatformName === "darwin" ? "MacOS" : "");
}

Basically, the Resources folder is always nested one level down, so resolving to the higher directory pulls the folder I need to access the executable. Maybe an approach like that could work for your purposes?

I think in your case it would be for .app or .exe

path.join(resourcesDir, "../", electronPlatformName === "darwin" ? "../" : "");

@Nantris
Copy link

Nantris commented Jul 16, 2022

I don't think #6930 should be closed unless this issue explicitly includes support for EnableEmbeddedAsarIntegrityValidation, which as I understand it will require more substantive changes to electron-builder than the others, which can simply be added with a quick post-build script.

@Nantris
Copy link

Nantris commented Jul 16, 2022

This adapted code from @kaatt's shared code should cover most use cases with reasonable defaults and includes basic Linux workarounds.

// Adapted from https://github.com/electron-userland/electron-builder/issues/6365#issue-1033809141
const afterPack = async context => {
  const ext = {
    darwin: '.app',
    win32: '.exe',
    linux: [''],
  }[context.electronPlatformName];

  const IS_LINUX = context.electronPlatformName === 'linux';
  const executableName = IS_LINUX
    ? context.packager.appInfo.productFilename.toLowerCase().replace('-dev', '')
    : context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds

  const electronBinaryPath = path.join(
    context.appOutDir,
    `${executableName}${ext}`
  );

  await flipFuses(electronBinaryPath, {
    version: FuseVersion.V1,
    [FuseV1Options.EnableCookieEncryption]: true,
    [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
    [FuseV1Options.EnableNodeCliInspectArguments]: false,
    // [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true, // Only affects macOS builds, but breaks them -- https://github.com/electron/fuses/issues/7
    [FuseV1Options.OnlyLoadAppFromAsar]: true,
  });
};

@mmaietta
Copy link
Collaborator

mmaietta commented Jul 21, 2022

Adding on to ☝️ , when building for MacOS Universal distributable and want to flip fuses, you'll need to run flipFuses only on the afterPack of the universal callback stage (added in https://github.com/electron-userland/electron-builder/releases/tag/v23.1.0) as afterPack is called for x64, arm64, and universal stages
You'll need to wrap the code above with:
(note: I use electron-builder.js as a config file to achieve typesafety. More info: https://www.electron.build/api/programmatic-usage)

import * as builder from "electron-builder";
....

afterPack: async (context: builder.AfterPackContext) => {
    if (context.electronPlatformName !== 'darwin' || context.arch === builder.Arch.universal) {
        await addElectronFuses(context)
    }
},

...

// Adapted from https://github.com/electron-userland/electron-builder/issues/6365#issue-1033809141
async function addElectronFuses(context: builder.AfterPackContext) {
    const { appOutDir, packager: { appInfo }, electronPlatformName, arch } = context
    const ext = {
      darwin: '.app',
      win32: '.exe',
      linux: [''],
    }[electronPlatformName];
  
    const electronBinaryPath = path.join(appOutDir, `${appInfo.productFilename}${ext}`);
    console.log('Flipping fuses for: ', electronBinaryPath)

    await flipFuses(electronBinaryPath, {
      version: FuseVersion.V1,
      resetAdHocDarwinSignature: electronPlatformName === 'darwin' && arch === builder.Arch.arm64, // necessary for building on Apple Silicon
      [FuseV1Options.RunAsNode]: false,
      [FuseV1Options.EnableCookieEncryption]: true,
      [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
      [FuseV1Options.EnableNodeCliInspectArguments]: false,
      [FuseV1Options.OnlyLoadAppFromAsar]: true,
    });
  };

@mmaietta mmaietta pinned this issue Jul 21, 2022
@mmaietta
Copy link
Collaborator

mmaietta commented Jul 27, 2022

Hey guys, I investigated further to see if I could integrate Fuses package directly into electron-builder. Got the logic flow but the schema validator fails due to having to use FuseConfig from @electron/fuses https://github.com/electron/fuses/blob/d69eb9cc92b13e77b08bdf3b594635fc0a2c83b8/src/config.ts#L17-L22

This theoretically would allow version overrides since electron-builder schema wouldn't need to have a breaking change when a FuseV2Config eventually comes out. Instead, it'd just be a simple package update and the schema would automatically accept the new version.

Due to the way the package's FuseOptions was written, the schema comes out all weird because it can't handle enums. This is what is generated in the schema

{
          "additionalProperties": false,
          "properties": {
            "0": {
              "type": "boolean"
            },
            "1": {
              "type": "boolean"
            },
            "2": {
              "type": "boolean"
            },
            "3": {
              "type": "boolean"
            },
            "4": {
              "type": "boolean"
            },
            "5": {
              "type": "boolean"
            }
          },
          "type": "object"
        },

Which translates to:

  {
    version: FuseVersion.V1,
    [FuseV1Options.RunAsNode]: false,
    [FuseV1Options.EnableCookieEncryption]: true,
    [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
    [FuseV1Options.EnableNodeCliInspectArguments]: false,
    [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true
    [FuseV1Options.OnlyLoadAppFromAsar]: true
  }

version and EnableCookieEncryption overlap values and aren't rendered in the schema validator.

This was the configuration property

  /**
   * *electron frameworks only* Electron fuses configuration. 
   */
  readonly electronFuses?: FuseConfig

The documentation site won't even generate either. 😢

I don't see a way for this to be able to be published. Thoughts are welcome. For now, I'm closing this ticket and suggesting this approach #6365 (comment). Sorry folks

@Nantris
Copy link

Nantris commented Jul 27, 2022

@mmaietta that makes sense but please consider re-opening #6930 which is quite distinct from this issue.

@theogravity
Copy link

theogravity commented Apr 27, 2023

In the example @mmaietta provided, resetAdHocDarwinSignature will always return false since addElectronFuses will only run when the platform is apple and the arch is universal.

These are the settings that worked for me for universal:

// https://github.com/electron-userland/electron-builder/issues/6365
const path = require('path');
const { flipFuses, FuseVersion, FuseV1Options } = require('@electron/fuses');
const builder = require('electron-builder');

// Adapted from https://github.com/electron-userland/electron-builder/issues/6365#issue-1033809141
async function addElectronFuses(context) {
  const { electronPlatformName, arch } = context;

  const ext = {
    darwin: '.app',
    win32: '.exe',
    linux: [''],
  }[electronPlatformName];

  const IS_LINUX = context.electronPlatformName === 'linux';
  const executableName = IS_LINUX
    ? context.packager.appInfo.productFilename.toLowerCase().replace('-dev', '').replace(' ', '-')
    : context.packager.appInfo.productFilename; // .toLowerCase() to accomodate Linux file named `name` but productFileName is `Name` -- Replaces '-dev' because on Linux the executable name is `name` even for the DEV builds

  const electronBinaryPath = path.join(context.appOutDir, `${executableName}${ext}`);

  await flipFuses(electronBinaryPath, {
    version: FuseVersion.V1,
    [FuseV1Options.EnableCookieEncryption]: true,
    resetAdHocDarwinSignature: electronPlatformName === 'darwin' && arch === builder.Arch.universal,
    [FuseV1Options.RunAsNode]: false,
    [FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
    [FuseV1Options.EnableNodeCliInspectArguments]: false,
    [FuseV1Options.OnlyLoadAppFromAsar]: true,
    // Mac app crashes when enabled for us on arm, might be fine for you
    [FuseV1Options.LoadBrowserProcessSpecificV8Snapshot]: false,
    // https://github.com/electron/fuses/issues/7
    [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: false,
  });
}

module.exports = async (context) => {
  if (context.electronPlatformName !== 'darwin' || context.arch === builder.Arch.universal) {
    await addElectronFuses(context);
  }
};

@mmaietta
Copy link
Collaborator

I'm taking another stab at this and thinking about hardcoding a configuration object as a copy of their configuration. When they release an additional V1 fuse, we just add the additional fuse to our configuration interface. I'm thinking of doing something like this in configuration.ts

interface FuseV1Options {
  version: FuseVersion.V1
  runAsNode?: boolean
  enableCookieEncryption?: boolean
  enableNodeOptionsEnvironmentVariable?: boolean
  enableNodeCliInspectArguments?: boolean
  enableEmbeddedAsarIntegrityValidation?: boolean
  onlyLoadAppFromAsar?: boolean
  loadBrowserProcessSpecificV8Snapshot?: boolean
  grantFileProtocolExtraPrivileges?: boolean
  /**
   * Ensures that all fuses in the fuse wire being set have been defined to a set value
   * by the provided config. Set this to true to ensure you don't accidentally miss a
   * fuse being added in future Electron upgrades.
   *
   * This option may default to "true" in a future version of @electron/fuses but currently
   * defaults to "false"
   */
  strictlyRequireAllFuses?: boolean
  resetAdHocDarwinSignature?: boolean
}

Whenever they upgrade to a V2 options, I'll just have to add a new interface for it like fuseOptions?: FuseV1Options | FuseV2Options

It's a minor snag, but overall, this approach allows schema.json to be generated correctly. Any changes to electron/fuses dependency would only result in a minor semver bump for electron-builder

Anyone have thoughts on this approach?

@mmaietta
Copy link
Collaborator

mmaietta commented Oct 11, 2024

Alrighty, got the PR ☝️ up and running in the CI and passing. There will be a new configuration object electronFuses: FuseOptionsV1 and it executes @electron/fuses directly before signing as is recommended on the repo instructions: https://github.com/electron/fuses?tab=readme-ov-file#apple-silicon

I've also opened it up as a convenience method should you want to keep custom logic in your afterPack method called via

await context.packager.addElectronFuses(context, { ... })

public async addElectronFuses(context: AfterPackContext, fuses: FuseConfig) {

It allows you to pass a full FuseConfig into the method as opposed to having electron-builder parsing electronFuses config property.
Main notable change in the convenience method versus approaches mentioned above is:

const executableName = packager instanceof LinuxPackager ? packager.executableName : packager.appInfo.productFilename

Taking this ☝️ approach would also allow you to set strictlyRequireAllFuses: true should you so desire. (I couldn't pass any var of this in-code due to https://github.com/electron/fuses/blob/0cf2a177e70dd81cc74c4449847287cd65ea140b/src/config.ts#L31-L34)

Quick additional note, the fuse FuseV1Options.EnableEmbeddedAsarIntegrityValidation = true should actually be supported after the electron/asar migration PR goes in #8570 (the combined changes worked for me in my local testing)

@mmaietta
Copy link
Collaborator

Released in v26.0.0-alpha.2. Please give it a shot!

Verified it locally (on Mac build machine) that it launches with the EnableEmbeddedAsarIntegrityValidation fuse set:

npx @electron/fuses read --app dist/mac-universal/electron-quick-start-typescript.app
Analyzing app: electron-quick-start-typescript.app
Fuse Version: v1
  RunAsNode is Enabled
  EnableCookieEncryption is Disabled
  EnableNodeOptionsEnvironmentVariable is Enabled
  EnableNodeCliInspectArguments is Enabled
  EnableEmbeddedAsarIntegrityValidation is Enabled
  OnlyLoadAppFromAsar is Disabled
  LoadBrowserProcessSpecificV8Snapshot is Disabled

@Lemonexe
Copy link
Contributor

Hi @mmaietta, I tried Windows build on v26.0.0-alpha.4 and it works great 👌

Specifically, I tested fuses EnableEmbeddedAsarIntegrityValidation & OnlyLoadAppFromAsar, so also testing #6930.
What I did:

  • built the app, verified it builds & runs successfully ✅
  • changed JS bundle source code, built again
  • copied the "tampered" asar file to the previous build, which stopped working as expected ✅

Thank you very much for your work! Just today I researched how can I check asar integrity with electron-builder, only to learn that you have adopted electron/asar and added the support just very recently ❤️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants