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 cannot decrypt ASAR at runtime when combined with v8 bytecode source code protecton #83

Open
kethinov opened this issue Aug 28, 2024 · 1 comment

Comments

@kethinov
Copy link

I'm trying to combine V8 bytecode source code protection with asarmor's ASAR encryption and I'm having some trouble. It looks like I am able to get it to build correctly, but the ASAR archive is not being decrypted by Electron at runtime.

Steps I followed:

I created an Electron app using npm create @quick-start/electron@latest

I modified the following files with the following code:

electron-builder.yml: default file except I added the afterPack and beforePack lines

appId: com.electron.app
productName: testapp
directories:
  buildResources: build
files:
  - '!**/.vscode/*'
  - '!src/*'
  - '!electron.vite.config.{js,ts,mjs,cjs}'
  - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
asarUnpack:
  - resources/**
afterPack: './afterPack.js'
beforePack: './beforePack.js'
win:
  executableName: testapp
nsis:
  artifactName: ${name}-${version}-setup.${ext}
  shortcutName: ${productName}
  uninstallDisplayName: ${productName}
  createDesktopShortcut: always
mac:
  entitlementsInherit: build/entitlements.mac.plist
  extendInfo:
    - NSCameraUsageDescription: Application requests access to the device's camera.
    - NSMicrophoneUsageDescription: Application requests access to the device's microphone.
    - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
    - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
  notarize: false
dmg:
  artifactName: ${name}-${version}.${ext}
linux:
  target:
    - AppImage
    - snap
    - deb
  maintainer: electronjs.org
  category: Utility
appImage:
  artifactName: ${name}-${version}.${ext}
npmRebuild: false
publish:
  provider: generic
  url: https://example.com/auto-updates

electron.vite.config.mjs: I modified it to enable V8 bytecode source code protection

import { defineConfig, externalizeDepsPlugin, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin(), bytecodePlugin()]
  },
  preload: {
    plugins: [externalizeDepsPlugin(), bytecodePlugin()]
  },
  renderer: {}
})

beforePack.js: this is mostly based on this repo's README, but I had to modify some things to get it to build

const { join } = require('path');
const { copyFile } = require('fs/promises');

exports.default = async (context) => {
  try {
    console.log('copying native dependencies');

    const release = join(__dirname, 'node_modules', 'asarmor', 'build', 'Release');

    // copy main.node from asarmor to our dist/build/release folder; this will become the entrypoint later on.
    await copyFile(
      join(release, 'main.node'),
      join(
        context.packager.info.projectDir,
        'out',
        'main',
        'main.node'
      )
    );

    // copy renderer.node to our dist/build/release folder; the render process will be bootstrapped from the main process later on.
    await copyFile(
      join(release, 'renderer.node'),
      join(
        context.packager.info.projectDir,
        'out',
        'renderer',
        'renderer.node'
      )
    );
  } catch (err) {
    console.error(err);
  }
};

afterPack.js: this is mostly based on this repo's README, but I had to modify some things to get it to build

const asarmor = require('asarmor')

exports.default = async ({ appOutDir, packager }) => {
  try {
    const asarPath = packager.getResourcesDir(appOutDir) + '/app.asar';

    console.log(
      `  \x1B[34m•\x1B[0m asarmor encrypting contents of ${asarPath}`
    );
    await asarmor.encrypt({
      src: asarPath,
      dst: asarPath,
    });

    // then patch the header
    console.log(`  \x1B[34m•\x1B[0m asarmor applying patches to ${asarPath}`);
    const archive = await asarmor.open(asarPath);
    archive.patch(); // apply default patches
    await archive.write(asarPath);
  } catch (err) {
    console.error(err);
  }
};

index.js: this is mostly the default file created by npm create @quick-start/electron@latest but I modified it using instructions from this repo's README, with comments calling out where asarmor tweaks have been made

import { app, shell, BrowserWindow, ipcMain } from 'electron'
import { join } from 'path'
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
import icon from '../../resources/icon.png?asset'
import { allowUnencrypted } from 'asarmor';
allowUnencrypted(['node_modules']); // enables resolution of non-encrypted dependencies from `node_modules.asar`

// REQUIRED FOR ASARMOR
module.exports = bootstrap

function bootstrap(k) {

  // sanity check
  if (!Array.isArray(k) || k.length === 0) {
    throw new Error('Failed to bootstrap application.');
  }

  // key should be valid at this point, but you can access it here to perform additional checks.
  console.log('decryption key: ' + k);

  async function createWindow() {
    // Create the browser window.
    const mainWindow = new BrowserWindow({
      width: 900,
      height: 670,
      show: false,
      autoHideMenuBar: true,
      ...(process.platform === 'linux' ? { icon } : {}),
      webPreferences: {
        preload: join(__dirname, '../preload/index.js'),
        sandbox: false,
        nodeIntegration: true,   // MUST BE ENABLED FOR ASARMOR
        contextIsolation: false, // MUST BE DISABLED FOR ASARMOR
      }
    })

    mainWindow.on('ready-to-show', () => {
      mainWindow.show()
    })

    mainWindow.webContents.setWindowOpenHandler((details) => {
      shell.openExternal(details.url)
      return { action: 'deny' }
    })

    // HMR for renderer base on electron-vite cli.
    // Load the remote URL for development or the local html file for production.
    if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
      mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
    } else {
      mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
      // REQUIRED FOR ASARMOR
      // Load encrypted renderer process
      await mainWindow.webContents.executeJavaScript(`!function () {
        require('../renderer/renderer.node');
        require('../renderer/renderer.js');
      }()`);
    }
  }

  // This method will be called when Electron has finished
  // initialization and is ready to create browser windows.
  // Some APIs can only be used after this event occurs.
  app.whenReady().then(() => {
    // Set app user model id for windows
    electronApp.setAppUserModelId('com.electron')

    // Default open or close DevTools by F12 in development
    // and ignore CommandOrControl + R in production.
    // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
    app.on('browser-window-created', (_, window) => {
      optimizer.watchWindowShortcuts(window)
    })

    // IPC test
    ipcMain.on('ping', () => console.log('pong'))

    createWindow()

    app.on('activate', function () {
      // On macOS it's common to re-create a window in the app when the
      // dock icon is clicked and there are no other windows open.
      if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
  })

  // Quit when all windows are closed, except on macOS. There, it's common
  // for applications and their menu bar to stay active until the user quits
  // explicitly with Cmd + Q.
  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
      app.quit()
    }
  })

  // In this file you can include the rest of your app's specific main process
  // code. You can also put them in separate files and require them here.
}

if (is.dev) {
  bootstrap([1])
}

To build:

  • npm i
  • npm run dev (works fine)
  • npm run build:mac (I tested on Mac; it appears to build fine)
  • Open the built app, receive this error:
Uncaught Exception:
/Users/kethinov/Desktop/TestApp/dist/mac/testapp.app/Contents/Resources/app.asar/out/main/index.js:1
EZG7CJr+zjEkZKlBAJlRbWE2epySSsRkhycG1A5HT2vTC9xhnBjCW4+2egLaWqUnX9kuGXf1prOTfxet71u14li8DCUS/sqixE6wEnRMf92wWi8t7YcanAhNG/TlIVgi
^^

SyntaxError: Invalid or unexpected token
at wrapSafe (node:internal/modules/cjs/loader:1288:20)
at Module._compile (node:internal/modules/cjs/loader:1328:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1432:10)
at Module.load (node:internal/modules/cjs/loader:1215:32)
at Module._load (node:internal/modules/cjs/loader:1031:12)
at c._load (node:electron/js2c/node_init:2:17025)
at node:electron/js2c/browser_init:2:126921
at node:electron/js2c/browser_init:2:127130
at node:electron/js2c/browser_init:2:127134
at BuiltinModule.compileForInternalLoader (node:internal/bootstrap/realm:398:7)

Any idea what steps are missing to get it to decrypt the ASAR file correctly at runtime?

@github-staff github-staff deleted a comment Aug 28, 2024
@richardsimkus
Copy link

Getting the same issue

@sleeyax sleeyax changed the title Electron cannot decrypt ASAR at runtime Electron cannot decrypt ASAR at runtime when combined with v8 bytecode source code protecton Mar 18, 2025
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

No branches or pull requests

6 participants
@kethinov @richardsimkus and others