Skip to content

Source Code Protection

NOTE

Source code protection feature is available since electron-vite 1.0.9.

We all know that Electron uses javascript to build desktop applications, which makes it very easy for hackers to unpack our applications, modify logic to break commercial restrictions, repackage, and redistribute cracked versions.

Solutions

To really solve the problem, in addition to putting all the commercial logic on the server side, we need to harden the code to avoid unpacking, tampering, repackaging, and redistributing.

The mainstream plan:

  1. Uglify / Obfuscator: Minimize the readability of JS code by uglifying and obfuscating it.
  2. Native encryption: Encrypt the bundle via XOR or AES, encapsulated into Node Addon, and decrypted by JS at runtime.
  3. ASAR encryption: Encrypt the Electron ASAR file, modify the Electron source code, decrypt the ASAR file before reading it, and then run it.
  4. V8 bytecode: The vm module in the Node standard library can generate its cache data from script objects (see). The cached data can be interpreted as v8 bytecode, which is distributed to achieve source code protection.

Scheme comparison:

-ObfuscatorNative encryptionASAR encryptionV8 bytecode
UnpackEasyHighHighHigh
TamperingEasyEasyMiddleHigh
ReadabilityEasyEasyEasyHigh
RepackagingEasyEasyEasyEasy
Access costLowHighHighMiddle
Overall protectionLowMiddleMiddleHigh

For now, the solution with v8 bytecode seems to be the best one.

Read more:

What is V8 Bytecode

As we can understand, V8 bytecode is a serialized form of JavaScript parsed and compiled by the V8 engine, and it is often used for performance optimization within the browser. So if we run the code through V8 bytecode, we can not only protect the code, but also improve performance.

electron-vite inspired by bytenode, the specific implementation:

  • Implement a plugin bytecodePlugin to parse the bundles, and determines whether to compile to bytecode.
  • Start the Electron process to compile the bundles into .jsc files and ensure that the generated bytecode can run in Electron's Node environment.
  • Generate a bytecode loader to enable Electron applications to load bytecode modules.
  • Support developers to freely decide which chunks to compile.

In addition, electron-vite also solves some problems that bytenode can't solve:

  • Fixed the issue where async arrow functions could crash Electron apps.
  • Protect strings.

Warning

The Function.prototype.toString is not supported, because the source code does not follow the bytecode distribution, so the source code for the function is not available.

Enable Bytecode to Protect Your Electron Source Code

Use the plugin bytecodePlugin to enable it:

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

export default defineConfig({
  main: {
    plugins: [bytecodePlugin()]
  },
  preload: {
    plugins: [bytecodePlugin()]
  },
  renderer: {
    // ...
  }
})
import { defineConfig, bytecodePlugin } from 'electron-vite'

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

NOTE

bytecodePlugin only works in production and supports main process and preload scripts.

It is important to note that the preload script needs to disable the sandbox to support the bytecode, because the bytecode is based on the Node's vm module. Since Electron 20, renderers will be sandboxed by default, so if you want to use bytecode to protect preload scripts you need to set sandbox: false.

bytecodePlugin Options

chunkAlias

  • Type: string | string[]

Set chunk alias to instruct the bytecode compiler to compile the associated bundles. Usually needs to be used with the option build.rollupOptions.output.manualChunks.

transformArrowFunctions

  • Type: boolean
  • default: true(enabled by default since electron-vite 1.0.10)

Set false to disable transforming arrow functions to normal functions.

removeBundleJS

  • Type: boolean
  • default: true

Set false to keep bundle files which compiled as bytecode files.

protectedStrings

Specify which strings(such as sensitive strings, encryption keys, passwords, etc) in source code need to be protected.

V8 bytecode does not protect strings, but electron-vite will transform these strings to character codes(using String.fromCharCode) so that these strings can be protected by V8 bytecode.

Customizing Protection

For example, only protect src/main/foo.ts:

txt
.
├──src
│  ├──main
│  │  ├──index.ts
│  │  ├──foo.ts
│  │  └──...
└──...
.
├──src
│  ├──main
│  │  ├──index.ts
│  │  ├──foo.ts
│  │  └──...
└──...

You can modify your config file like this:

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

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ chunkAlias: 'foo' })],
    build: {
      rollupOptions: {
        output: {
          manualChunks(id): string | void {
            if (id.includes('foo')) {
              return 'foo'
            }
          }
        }
      }
    }
  },
  preload: {
    // ...
  },
  renderer: {
    // ...
  }
})
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ chunkAlias: 'foo' })],
    build: {
      rollupOptions: {
        output: {
          manualChunks(id): string | void {
            if (id.includes('foo')) {
              return 'foo'
            }
          }
        }
      }
    }
  },
  preload: {
    // ...
  },
  renderer: {
    // ...
  }
})

Examples

You can learn more by playing with the example.

Limitations of V8 Bytecode

V8 bytecode does not protect strings, so if we write some encryption keys or other sensitive strings in JS code, we can still see the string contents directly by reading V8 bytecode as a string.

However, electron-vite can transform these strings to character codes so that these strings can be protected by V8 bytecode. For example:

js
// string in source code
const encryptKey = 'ABC'

// electron-vite will transform string to character codes
const encryptKey = String.fromCharCode(65, 66, 67)
// string in source code
const encryptKey = 'ABC'

// electron-vite will transform string to character codes
const encryptKey = String.fromCharCode(65, 66, 67)

The strings to be protected in the source code can be specified via plugin protectedStrings option.

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

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ protectedStrings: ['ABC'] })]
  },
  // ...
})
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({ protectedStrings: ['ABC'] })]
  },
  // ...
})

Warning

You should not enumerate all strings in source code for protection, usually we only need to protect sensitive strings.

Warning

When minification (build.minify) is enabled, string protection has no effect. This is because string protection is based on character codes. However, modern minification tools (such as esbuild or terser) will restore the converted character codes, causing the protection to fail. electron-vite will throw a warning. In fact, minification has little effect on reducing bytecode size, so it is recommended not to enable minification when protecting strings.

Multi Platform Build

NOTE

Don’t expect that you can build app for all platforms on one platform.

By default compile bytecode based on current Electron Node.js version and current architecture (such as x86, x64, ARM, etc). In addition to ensuring the Node.js version of the released Electron app is the same as when it was compiled, the architecture is the constraint of the multi-platform build.

Multi Platform Build on One Architecture

It is possible for multi-platform build:

  • 64-bit Electron app for MacOS, Windows or Linux in 64-bit MacOS

Multi Architecture Build on One Platform

For example, building an 64-bit app for MacOS in arm64 MacOS, it will run with an error. Because the arm64-based bytecode built by default cannot run in an 64-bit app.

But we can specify another configuration file and set the environment variable ELECTRON_EXEC_PATH to the path of (64-bit) Electron app. The bytecode compiler will compile with the specified Electron app.

js
// specify `electron.x64.vite.config.ts` for building x64 Electron app
import { defineConfig } from 'electron-vite'

export default defineConfig(() => {
  process.env.ELECTRON_EXEC_PATH = '/path/to/electron-x64/electron.app'

  return {
    // electron-vite config
  }
})
// specify `electron.x64.vite.config.ts` for building x64 Electron app
import { defineConfig } from 'electron-vite'

export default defineConfig(() => {
  process.env.ELECTRON_EXEC_PATH = '/path/to/electron-x64/electron.app'

  return {
    // electron-vite config
  }
})

NOTE

You can use the --arch flag with npm install to install Electron for other architectures.

sh
npm install --arch=ia32 electron
npm install --arch=ia32 electron

Of course, this is also limited, because the Electron process needs to be run to compile the bytecode to ensure that the bytecode is generated according to the Electron Node.js version. But different architectures are not necessarily compatible with each other. For example, an 64-bit app can run in arm64 MacOS, but an arm64 app cannot run in 64-bit MacOS.

It is possible for multi architecture build:

  • 64-bit Electron app for MacOS in arm64 MacOS
  • 64-bit Electron app for Windows in arm64 Windows
  • 32-bit Electron app for Windows in 64-bit Windows

Warning

Bytecode are CPU-agnostic. However, you should run your tests before and after deployment, because V8 sanity checks include some checks related to CPU supported features, so this may cause errors in some rare cases.

FAQ

Impact on code organization and writing?

The only effect that bytecode schemes have found on code so far is Function.prototype.toString()Method does not work because the source code does not follow the bytecode distribution, so the source code for the function is not available.

Does it affect application performance?

There is no impact on code execution performance and a slight improvement.

Impact on program volume?

For bundles of only a few hundred Kilobytes, there is a significant increase in bytecode size, but for 2M+ bundles, there is no significant difference in bytecode size.

How strong is the code protection?

Currently, there are no tools available to decompile V8 bytecode, so this solution is reliable and secure.

Released under the MIT License