diff --git a/package.json b/package.json index 1a7145a..d9f23f6 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "@types/debug": "^4.1.5", "@types/fs-extra": "^9.0.4", "@types/node": "^14.14.7", + "@types/plist": "^3.0.2", "husky": "^4.3.0", "lint-staged": "^10.5.1", "prettier": "^2.1.2", @@ -38,10 +39,11 @@ }, "dependencies": { "@malept/cross-spawn-promise": "^1.1.0", - "asar": "^3.0.3", + "asar": "^3.1.0", "debug": "^4.3.1", "dir-compare": "^2.4.0", - "fs-extra": "^9.0.1" + "fs-extra": "^9.0.1", + "plist": "^3.0.4" }, "husky": { "hooks": { diff --git a/src/asar-utils.ts b/src/asar-utils.ts index aa002ce..90f4cbc 100644 --- a/src/asar-utils.ts +++ b/src/asar-utils.ts @@ -1,3 +1,5 @@ +import * as asar from 'asar'; +import * as crypto from 'crypto'; import * as fs from 'fs-extra'; import * as path from 'path'; import { d } from './debug'; @@ -19,3 +21,13 @@ export const detectAsarMode = async (appPath: string) => { d('determined has asar'); return AsarMode.HAS_ASAR; }; + +export const generateAsarIntegrity = (asarPath: string) => { + return { + algorithm: 'SHA256' as const, + hash: crypto + .createHash('SHA256') + .update(asar.getRawHeader(asarPath).headerString) + .digest('hex'), + }; +}; diff --git a/src/file-utils.ts b/src/file-utils.ts index 12376e2..7a603a8 100644 --- a/src/file-utils.ts +++ b/src/file-utils.ts @@ -7,6 +7,7 @@ const MACHO_PREFIX = 'Mach-O '; export enum AppFileType { MACHO, PLAIN, + INFO_PLIST, SNAPSHOT, APP_CODE, } @@ -50,6 +51,8 @@ export const getAllAppFiles = async (appPath: string): Promise => { fileType = AppFileType.MACHO; } else if (p.endsWith('.bin')) { fileType = AppFileType.SNAPSHOT; + } else if (path.basename(p) === 'Info.plist') { + fileType = AppFileType.INFO_PLIST; } files.push({ diff --git a/src/index.ts b/src/index.ts index 91f733b..dd0839c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,14 @@ import { spawn } from '@malept/cross-spawn-promise'; import * as asar from 'asar'; +import * as crypto from 'crypto'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; +import * as plist from 'plist'; import * as dircompare from 'dir-compare'; + import { AppFile, AppFileType, getAllAppFiles } from './file-utils'; -import { AsarMode, detectAsarMode } from './asar-utils'; +import { AsarMode, detectAsarMode, generateAsarIntegrity } from './asar-utils'; import { sha } from './sha'; import { d } from './debug'; @@ -172,6 +175,9 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = } } + const generatedIntegrity: Record = {}; + let didSplitAsar = false; + /** * If we have an ASAR we just need to check if the two "app.asar" files have the same hash, * if they are, same as above, we can leave one there and call it a day. If they're different @@ -188,11 +194,10 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = ); if (x64AsarSha !== arm64AsarSha) { + didSplitAsar = true; d('x64 and arm64 asars are different'); - await fs.move( - path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), - path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'), - ); + const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar'); + await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath); const x64Unpacked = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar.unpacked'); if (await fs.pathExists(x64Unpacked)) { await fs.move( @@ -201,9 +206,10 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = ); } + const arm64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'); await fs.copy( path.resolve(opts.arm64AppPath, 'Contents', 'Resources', 'app.asar'), - path.resolve(tmpApp, 'Contents', 'Resources', 'app-arm64.asar'), + arm64AsarPath, ); const arm64Unpacked = path.resolve( opts.arm64AppPath, @@ -234,15 +240,42 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise = ); pj.main = 'index.js'; await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj); - await asar.createPackage( - entryAsar, - path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), - ); + const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'); + await asar.createPackage(entryAsar, asarPath); + + generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(asarPath); + generatedIntegrity['Resources/app-x64.asar'] = generateAsarIntegrity(x64AsarPath); + generatedIntegrity['Resources/app-arm64.asar'] = generateAsarIntegrity(arm64AsarPath); } else { d('x64 and arm64 asars are the same'); + generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity( + path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), + ); } } + const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST); + for (const plistFile of plistFiles) { + const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath); + const arm64PlistPath = path.resolve(opts.arm64AppPath, plistFile.relativePath); + + const { ElectronAsarIntegrity: x64Integrity, ...x64Plist } = plist.parse( + await fs.readFile(x64PlistPath, 'utf8'), + ) as any; + const { ElectronAsarIntegrity: arm64Integrity, ...arm64Plist } = plist.parse( + await fs.readFile(arm64PlistPath, 'utf8'), + ) as any; + if (JSON.stringify(x64Plist) !== JSON.stringify(arm64Plist)) { + throw new Error( + `Expected all Info.plist files to be identical when ignoring integrity when creating a universal build but "${plistFile.relativePath}" was not`, + ); + } + + const mergedPlist = { ...x64Plist, ElectronAsarIntegrity: generatedIntegrity }; + + await fs.writeFile(path.resolve(tmpApp, plistFile.relativePath), plist.build(mergedPlist)); + } + for (const snapshotsFile of arm64Files.filter((f) => f.type === AppFileType.SNAPSHOT)) { d('copying snapshot file', snapshotsFile.relativePath, 'to target application'); await fs.copy( diff --git a/yarn.lock b/yarn.lock index b2dd505..fb2a50c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -314,6 +314,14 @@ resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== +"@types/plist@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" + integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== + dependencies: + "@types/node" "*" + xmlbuilder ">=11.0.1" + "@types/retry@^0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -497,10 +505,10 @@ asap@^2.0.0: resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= -asar@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" - integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== +asar@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/asar/-/asar-3.1.0.tgz#70b0509449fe3daccc63beb4d3c7d2e24d3c6473" + integrity sha512-vyxPxP5arcAqN4F/ebHd/HhwnAiZtwhglvdmc7BR2f0ywbVNTOpSeyhLDbGXtE/y58hv1oC75TaNIXutnsOZsQ== dependencies: chromium-pickle-js "^0.2.0" commander "^5.0.0" @@ -559,6 +567,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" @@ -3711,6 +3724,14 @@ please-upgrade-node@^3.2.0: dependencies: semver-compare "^1.0.0" +plist@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.4.tgz#a62df837e3aed2bb3b735899d510c4f186019cbe" + integrity sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg== + dependencies: + base64-js "^1.5.1" + xmlbuilder "^9.0.7" + prepend-http@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" @@ -5037,6 +5058,16 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xmlbuilder@>=11.0.1: + version "15.1.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz#9dcdce49eea66d8d10b42cae94a79c3c8d0c2ec5" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +xmlbuilder@^9.0.7: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"