Files
Nucleus/scripts/update-electron-builder-lock.sh
Elie Gambache e7407ca4cc fix(plugin): install electron-builder from a pinned lock file instead of npx
Packaging invoked `npx --yes electron-builder@26.15.5`, which pinned only
the top-level version: the ~275 transitive packages were re-resolved from
the registry on every build, with no lock state to check them against and
with install scripts enabled. That runs on the machine holding the
code-signing certificates and notarization credentials, so a compromise
anywhere in that tree executed with those.

The plugin now embeds a package.json / package-lock.json pair pinning 277
packages (all from registry.npmjs.org, each with a sha512 integrity hash),
stages it into a build-local directory, installs with
`npm ci --ignore-scripts`, and runs the CLI directly through
`node node_modules/electron-builder/cli.js`. A tarball whose bytes do not
match the recorded hash now fails the build (EINTEGRITY), and no
pre/postinstall hook from that tree runs at all.

node and npm still come from the build machine; only the dependency tree is
pinned. electron-builder's own run-time downloads (app-builder, 7-Zip, NSIS,
snap/AppImage templates) remain outside any npm lock file — they are
verified by its own checksums.

Also in this change:

  - NodeJsDetector.detectNpm(), plus a shared detectSibling() helper for the
    nodePath override; the task now requires both node and npm and says
    what each is for when one is missing.
  - MacDmgLzma.locateAppBuilder searched the npx cache, which no longer
    exists, and never looked in ELECTRON_BUILDER_CACHE where
    electron-builder 26 actually downloads app-builder. It now checks that
    first, so the differential-update blockmap is no longer silently
    dropped after the DMG is recompressed with LZMA.
  - scripts/update-electron-builder-lock.sh regenerates the lock file,
    resolving against the public registry explicitly and refusing to run
    when certificate verification has been switched off — a lock file
    resolved over an unverified connection would record hashes for bytes
    nothing authenticated.
  - ElectronBuilderToolchainLockTest guards the boundary offline: version
    pinned in step with the Kotlin constant, lockfileVersion 3, every
    package from the public registry, every one carrying a sha512 hash.

Verified end to end against a minimal consumer project (TargetFormat.Zip):
the nominal path provisions the toolchain and produces the signed .app zip
plus its blockmap, and tampering with a single integrity hash in the
embedded lock file fails the build as intended.
2026-08-18 12:43:17 +03:00

89 lines
4.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# Regenerates the pinned electron-builder toolchain lock file shipped inside the Gradle plugin.
#
# The plugin provisions electron-builder with `npm ci --ignore-scripts` against this lock file, so
# the lock file is the integrity boundary for ~275 npm packages pulled onto the machine that holds
# the code-signing certificates. Run this ONLY on a trusted machine with a clean npm config, and
# review the diff before committing.
#
# Usage:
# scripts/update-electron-builder-lock.sh # re-resolve the currently pinned version
# scripts/update-electron-builder-lock.sh 26.16.0 # bump to a new version
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
resource_dir="$repo_root/plugin-build/plugin/src/main/resources/nucleus/electron-builder"
tool_manager="$repo_root/plugin-build/plugin/src/main/kotlin/dev/nucleusframework/desktop/application/internal/electronbuilder/ElectronBuilderToolManager.kt"
pinned_version="$(sed -nE 's/.*ELECTRON_BUILDER_VERSION = "([^"]+)".*/\1/p' "$tool_manager")"
version="${1:-$pinned_version}"
if [[ -z "$version" ]]; then
echo "Could not read ELECTRON_BUILDER_VERSION from $tool_manager" >&2
exit 1
fi
# This refuses only when certificate verification has been *switched off* — not merely because a
# corporate proxy terminates TLS. A proxy with its CA properly installed leaves verification on
# (NODE_TLS_REJECT_UNAUTHORIZED unset) and is fine: the hashes below cover tarball bytes, which such
# a proxy does not rewrite. With verification off there is nothing left to tell a scanning proxy from
# an attacker, and these hashes are what every later `npm ci` trusts.
if [[ -n "${NODE_TLS_REJECT_UNAUTHORIZED:-}" && "${NODE_TLS_REJECT_UNAUTHORIZED}" != "1" ]]; then
echo "Refusing to resolve the tree with certificate verification disabled" >&2
echo "(NODE_TLS_REJECT_UNAUTHORIZED=${NODE_TLS_REJECT_UNAUTHORIZED}). The integrity hashes would" >&2
echo "record whatever the connection served, unverified. Unset it — installing your proxy's CA" >&2
echo "certificate (NODE_EXTRA_CA_CERTS) keeps the proxy working with verification on." >&2
exit 1
fi
# Resolve against the public registry regardless of local npm config: the lock file must record
# registry.npmjs.org tarballs, so an internal mirror must not leak into `resolved` URLs. A mirror
# stays usable at install time — npm rewrites the host and still checks the recorded hash.
registry="https://registry.npmjs.org/"
configured_registry="$(npm config get registry)"
if [[ "$configured_registry" != "$registry" ]]; then
echo "note: npm is configured for $configured_registry; resolving against $registry for this run."
fi
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
cat > "$workdir/package.json" <<JSON
{
"name": "nucleus-electron-builder-toolchain",
"version": "1.0.0",
"private": true,
"description": "Pinned electron-builder toolchain used by the Nucleus Gradle plugin.",
"license": "MIT",
"dependencies": {
"electron-builder": "$version"
}
}
JSON
echo "Resolving electron-builder $version against $registry ..."
(cd "$workdir" && npm install --package-lock-only --ignore-scripts --no-audit --no-fund --registry="$registry")
echo "Verifying the resolved tree installs and runs ..."
(cd "$workdir" && npm ci --ignore-scripts --no-audit --no-fund --no-progress --registry="$registry" >/dev/null)
installed="$(cd "$workdir" && node node_modules/electron-builder/cli.js --version)"
if [[ "$installed" != "$version" ]]; then
echo "Installed CLI reports $installed, expected $version" >&2
exit 1
fi
mkdir -p "$resource_dir"
cp "$workdir/package.json" "$workdir/package-lock.json" "$resource_dir/"
echo
echo "Updated $resource_dir"
echo " electron-builder: $version ($(grep -c '"integrity":' "$resource_dir/package-lock.json") locked packages)"
if [[ "$version" != "$pinned_version" ]]; then
echo
echo "Now set ELECTRON_BUILDER_VERSION = \"$version\" in:"
echo " $tool_manager"
echo "(ElectronBuilderToolchainLockTest fails until it matches.)"
fi
echo
echo "Review the diff before committing: git diff -- $resource_dir"