From c6d6b6e57be2c042dc586ae13f1af38a8a19af41 Mon Sep 17 00:00:00 2001 From: Mike Maietta Date: Fri, 24 Jan 2025 20:12:53 -0800 Subject: [PATCH] fix: ASAR files in `extraResources` are not included in integrity calculations (#8805) --- .changeset/nine-cats-eat.md | 5 + .../app-builder-lib/src/asar/integrity.ts | 60 ++++++++++-- .../app-builder-lib/src/platformPackager.ts | 2 +- .../test-app-one/build/extraAsar.asar | Bin 0 -> 272 bytes test/snapshots/mac/macPackagerTest.js.snap | 88 ++++++++++++++++++ .../snapshots/windows/winPackagerTest.js.snap | 26 ++++++ test/src/helpers/packTester.ts | 1 - test/src/mac/macPackagerTest.ts | 28 +++++- test/src/windows/winPackagerTest.ts | 30 ++++-- 9 files changed, 220 insertions(+), 20 deletions(-) create mode 100644 .changeset/nine-cats-eat.md create mode 100644 test/fixtures/test-app-one/build/extraAsar.asar diff --git a/.changeset/nine-cats-eat.md b/.changeset/nine-cats-eat.md new file mode 100644 index 00000000000..cd7f563560b --- /dev/null +++ b/.changeset/nine-cats-eat.md @@ -0,0 +1,5 @@ +--- +"app-builder-lib": patch +--- + +fix: ASAR files in extraResources are not included in integrity calculations diff --git a/packages/app-builder-lib/src/asar/integrity.ts b/packages/app-builder-lib/src/asar/integrity.ts index 67e78b4d137..c9ba38636a3 100644 --- a/packages/app-builder-lib/src/asar/integrity.ts +++ b/packages/app-builder-lib/src/asar/integrity.ts @@ -4,10 +4,14 @@ import { createReadStream } from "fs" import { readdir } from "fs/promises" import * as path from "path" import { readAsarHeader, NodeIntegrity } from "./asar" +import { FileMatcher } from "../fileMatcher" +import { statOrNull, walk, FilterStats, log } from "builder-util" export interface AsarIntegrityOptions { readonly resourcesPath: string readonly resourcesRelativePath: string + readonly resourcesDestinationPath: string + readonly extraResourceMatchers: Array | null } export interface HeaderHash { @@ -19,16 +23,54 @@ export interface AsarIntegrity { [key: string]: HeaderHash } -export async function computeData({ resourcesPath, resourcesRelativePath }: AsarIntegrityOptions): Promise { - // sort to produce constant result - const names = (await readdir(resourcesPath)).filter(it => it.endsWith(".asar")).sort() - const checksums = await BluebirdPromise.map(names, it => hashHeader(path.join(resourcesPath, it))) - - const result: AsarIntegrity = {} - for (let i = 0; i < names.length; i++) { - result[path.join(resourcesRelativePath, names[i])] = checksums[i] +export async function computeData({ resourcesPath, resourcesRelativePath, resourcesDestinationPath, extraResourceMatchers }: AsarIntegrityOptions): Promise { + type Match = Pick + type IntegrityMap = { + [filepath: string]: string } - return result + const isAsar = (filepath: string) => filepath.endsWith(".asar") + + const resources = await readdir(resourcesPath) + const resourceAsars = resources.filter(isAsar).reduce( + (prev, filename) => ({ + ...prev, + [path.join(resourcesRelativePath, filename)]: path.join(resourcesPath, filename), + }), + {} + ) + + const extraResources = await BluebirdPromise.map(extraResourceMatchers ?? [], async (matcher: FileMatcher): Promise => { + const { from, to } = matcher + const stat = await statOrNull(from) + if (stat == null) { + log.warn({ from }, `file source doesn't exist`) + return [] + } + if (stat.isFile()) { + return [{ from, to }] + } + + if (matcher.isEmpty() || matcher.containsOnlyIgnore()) { + matcher.prependPattern("**/*") + } + const matcherFilter = matcher.createFilter() + const extraResourceMatches = await walk(matcher.from, (file: string, stats: FilterStats) => matcherFilter(file, stats) || stats.isDirectory()) + return extraResourceMatches.map(from => ({ from, to: matcher.to })) + }) + const extraResourceAsars = extraResources + .flat(1) + .filter(match => isAsar(match.from)) + .reduce((prev, { to, from }) => { + const prefix = path.relative(resourcesDestinationPath, to) + return { + ...prev, + [path.join(resourcesRelativePath, prefix, path.basename(from))]: from, + } + }, {}) + + // sort to produce constant result + const allAsars = [...Object.entries(resourceAsars), ...Object.entries(extraResourceAsars)].sort(([name1], [name2]) => name1.localeCompare(name2)) + return BluebirdPromise.reduce(allAsars, async (prev, [relativePathKey, from]) => ({ ...prev, [relativePathKey]: await hashHeader(from) }), {} as AsarIntegrity) } async function hashHeader(file: string): Promise { diff --git a/packages/app-builder-lib/src/platformPackager.ts b/packages/app-builder-lib/src/platformPackager.ts index 220e9a64e0c..3d1f9fc9d8c 100644 --- a/packages/app-builder-lib/src/platformPackager.ts +++ b/packages/app-builder-lib/src/platformPackager.ts @@ -318,7 +318,7 @@ export abstract class PlatformPackager let asarIntegrity: AsarIntegrity | null = null if (!(asarOptions == null || options?.disableAsarIntegrity)) { - asarIntegrity = await computeData({ resourcesPath, resourcesRelativePath }) + asarIntegrity = await computeData({ resourcesPath, resourcesRelativePath, resourcesDestinationPath: this.getResourcesDir(appOutDir), extraResourceMatchers }) } await framework.beforeCopyExtraFiles({ diff --git a/test/fixtures/test-app-one/build/extraAsar.asar b/test/fixtures/test-app-one/build/extraAsar.asar new file mode 100644 index 0000000000000000000000000000000000000000..b71d50895816389e30b7ff63150f347c0419ae1e GIT binary patch literal 272 zcmbtOF%H5o3=9$j@5o%CBz4-j8ykB^#E?WuThUTPG5{5S$3^@AODEl(PxfsYW4jGw zTLL32JPu<>yjggjZY$sCM_y$bUV%5F;B$;Aki`Wr_;AgkyAS!fM(o(l#QTYs?eU;= zMTy>~o^&Xk@Bj);^wLW}8(jo%5Uc1=fvRl)8K44mSc12J8d191XyuI7bjgjgzqH>P SAQ4IdKac93{(o?O(|iE+u}ZiA literal 0 HcmV?d00001 diff --git a/test/snapshots/mac/macPackagerTest.js.snap b/test/snapshots/mac/macPackagerTest.js.snap index 442355a10f2..2adb4420c60 100644 --- a/test/snapshots/mac/macPackagerTest.js.snap +++ b/test/snapshots/mac/macPackagerTest.js.snap @@ -2,6 +2,94 @@ exports[`electronDist 1`] = `"corrupted Electron dist"`; +exports[`multiple asar resources 1`] = ` +{ + "mac": [ + { + "arch": "x64", + "file": "Test App ßW-1.1.0-mac.zip", + "safeArtifactName": "TestApp-1.1.0-mac.zip", + "updateInfo": { + "sha512": "@sha512", + "size": "@size", + }, + }, + { + "file": "Test App ßW-1.1.0-mac.zip.blockmap", + "safeArtifactName": "Test App ßW-1.1.0-mac.zip.blockmap", + "updateInfo": { + "sha512": "@sha512", + "size": "@size", + }, + }, + ], +} +`; + +exports[`multiple asar resources 2`] = ` +{ + "CFBundleDisplayName": "Test App ßW", + "CFBundleExecutable": "Test App ßW", + "CFBundleIconFile": "icon.icns", + "CFBundleIdentifier": "org.electron-builder.testApp", + "CFBundleInfoDictionaryVersion": "6.0", + "CFBundleName": "Test App ßW", + "CFBundlePackageType": "APPL", + "CFBundleShortVersionString": "1.1.0", + "ElectronAsarIntegrity": { + "Resources/app.asar": { + "algorithm": "SHA256", + "hash": "hash", + }, + "Resources/extraAsar.asar": { + "algorithm": "SHA256", + "hash": "hash", + }, + "Resources/subdir/extraAsar2.asar": { + "algorithm": "SHA256", + "hash": "hash", + }, + }, + "LSApplicationCategoryType": "your.app.category.type", + "LSEnvironment": { + "MallocNanoZone": "0", + }, + "NSAppTransportSecurity": { + "NSAllowsLocalNetworking": true, + "NSExceptionDomains": { + "127.0.0.1": { + "NSIncludesSubdomains": false, + "NSTemporaryExceptionAllowsInsecureHTTPLoads": true, + "NSTemporaryExceptionAllowsInsecureHTTPSLoads": false, + "NSTemporaryExceptionMinimumTLSVersion": "1.0", + "NSTemporaryExceptionRequiresForwardSecrecy": false, + }, + "localhost": { + "NSIncludesSubdomains": false, + "NSTemporaryExceptionAllowsInsecureHTTPLoads": true, + "NSTemporaryExceptionAllowsInsecureHTTPSLoads": false, + "NSTemporaryExceptionMinimumTLSVersion": "1.0", + "NSTemporaryExceptionRequiresForwardSecrecy": false, + }, + }, + }, + "NSBluetoothAlwaysUsageDescription": "This app needs access to Bluetooth", + "NSBluetoothPeripheralUsageDescription": "This app needs access to Bluetooth", + "NSHighResolutionCapable": true, + "NSPrincipalClass": "AtomApplication", + "NSSupportsAutomaticGraphicsSwitching": true, +} +`; + +exports[`multiple asar resources 3`] = ` +[ + "app.asar", + "extraAsar.asar", + "icon.icns", + "subdir/extraAsar2.asar", +] +`; + exports[`one-package 1`] = ` { "mac": [ diff --git a/test/snapshots/windows/winPackagerTest.js.snap b/test/snapshots/windows/winPackagerTest.js.snap index 9ab4794c9d1..31e6dcd346e 100644 --- a/test/snapshots/windows/winPackagerTest.js.snap +++ b/test/snapshots/windows/winPackagerTest.js.snap @@ -75,6 +75,9 @@ exports[`win zip 2`] = ` "locales/", "resources/", "resources/app.asar", + "resources/extraAsar.asar", + "resources/subdir/", + "resources/subdir/extraAsar2.asar", "resources.pak", "snapshot_blob.bin", "Test App ßW.exe", @@ -92,6 +95,16 @@ exports[`win zip 3`] = ` "file": "resources\\app.asar", "value": "hash", }, + { + "alg": "SHA256", + "file": "resources\\extraAsar.asar", + "value": "hash", + }, + { + "alg": "SHA256", + "file": "resources\\subdir\\extraAsar2.asar", + "value": "hash", + }, ] `; @@ -108,6 +121,9 @@ exports[`win zip 4`] = ` "locales/", "resources/", "resources/app.asar", + "resources/extraAsar.asar", + "resources/subdir/", + "resources/subdir/extraAsar2.asar", "resources.pak", "snapshot_blob.bin", "Test App ßW.exe", @@ -125,6 +141,16 @@ exports[`win zip 5`] = ` "file": "resources\\app.asar", "value": "hash", }, + { + "alg": "SHA256", + "file": "resources\\extraAsar.asar", + "value": "hash", + }, + { + "alg": "SHA256", + "file": "resources\\subdir\\extraAsar2.asar", + "value": "hash", + }, ] `; diff --git a/test/src/helpers/packTester.ts b/test/src/helpers/packTester.ts index 9f81acb84a5..55146e7c368 100644 --- a/test/src/helpers/packTester.ts +++ b/test/src/helpers/packTester.ts @@ -432,7 +432,6 @@ async function checkWindowsResult(packager: Packager, checkOptions: AssertPackOp } else if (hasTarget("zip") && !(checkOptions.signed || checkOptions.signedWin)) { return checkZipResult() } - } const checkResult = async (artifacts: Array, extension: string) => { diff --git a/test/src/mac/macPackagerTest.ts b/test/src/mac/macPackagerTest.ts index ff2ff9f165b..66c44ee5649 100644 --- a/test/src/mac/macPackagerTest.ts +++ b/test/src/mac/macPackagerTest.ts @@ -3,7 +3,7 @@ import { Arch, createTargets, DIR_TARGET, Platform } from "electron-builder" import * as fs from "fs/promises" import * as path from "path" import { assertThat } from "../helpers/fileAssert" -import { app, appThrows, assertPack, platform } from "../helpers/packTester" +import { app, appThrows, assertPack, checkDirContents, platform } from "../helpers/packTester" import { verifySmartUnpack } from "../helpers/verifySmartUnpack" test.ifMac.ifAll("two-package", () => @@ -141,3 +141,29 @@ test.ifMac.ifAll( ) test.ifWinCi("Build macOS on Windows is not supported", appThrows(platform(Platform.MAC))) + +test.ifAll( + "multiple asar resources", + app( + { + targets: Platform.MAC.createTarget("zip", Arch.x64), + config: { + extraResources: [ + { from: "build", to: "./", filter: "*.asar" }, + { from: "build/subdir", to: "./subdir", filter: "*.asar" }, + ], + electronLanguages: "en", + }, + }, + { + signed: true, + projectDirCreated: async projectDir => { + await fs.mkdir(path.join(projectDir, "build", "subdir")) + await fs.copyFile(path.join(projectDir, "build", "extraAsar.asar"), path.join(projectDir, "build", "subdir", "extraAsar2.asar")) + }, + checkMacApp: async (appDir, info) => { + await checkDirContents(path.join(appDir, "Contents", "Resources")) + }, + } + ) +) diff --git a/test/src/windows/winPackagerTest.ts b/test/src/windows/winPackagerTest.ts index 91fdaaa743a..d44370070f7 100644 --- a/test/src/windows/winPackagerTest.ts +++ b/test/src/windows/winPackagerTest.ts @@ -30,6 +30,10 @@ test.ifAll( { targets: Platform.WINDOWS.createTarget(["zip"], Arch.x64, Arch.arm64), config: { + extraResources: [ + { from: "build", to: "./", filter: "*.asar" }, + { from: "build/subdir", to: "./subdir", filter: "*.asar" }, + ], electronLanguages: "en", downloadAlternateFFmpeg: true, electronFuses: { @@ -43,21 +47,31 @@ test.ifAll( grantFileProtocolExtraPrivileges: undefined, // unsupported on current electron version in our tests }, }, + }, + { + signed: false, + projectDirCreated: async projectDir => { + await fs.mkdir(path.join(projectDir, "build", "subdir")) + await fs.copyFile(path.join(projectDir, "build", "extraAsar.asar"), path.join(projectDir, "build", "subdir", "extraAsar2.asar")) + }, } ) ) test.ifAll( "zip artifactName", - app({ - targets: Platform.WINDOWS.createTarget(["zip"], Arch.x64), - config: { - //tslint:disable-next-line:no-invalid-template-strings - artifactName: "${productName}-${version}-${os}-${arch}.${ext}", + app( + { + targets: Platform.WINDOWS.createTarget(["zip"], Arch.x64), + config: { + //tslint:disable-next-line:no-invalid-template-strings + artifactName: "${productName}-${version}-${os}-${arch}.${ext}", + }, }, - }, { - signed: true - }) + { + signed: true, + } + ) ) test.ifAll(