Skip to content

Commit

Permalink
fix: When application uses multiple ASARs, `EnableEmbeddedAsarIntegri…
Browse files Browse the repository at this point in the history
…tyValidation` fuse breaks the application due to not all ASARs having integrity generated for them. Fixes: #116
  • Loading branch information
mmaietta committed Feb 19, 2025
1 parent 7c0ad6c commit 3ba0f9b
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 29 deletions.
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
test:
name: Test
strategy:
fail-fast: false
matrix:
node-version:
- '20.5'
Expand Down
6 changes: 3 additions & 3 deletions jest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as fs from 'fs-extra';
import * as path from 'path';
import { appsDir, asarsDir, templateApp } from './test/util';
import { appsPath, asarsDir, templateApp } from './test/util';

export default async () => {
await fs.remove(appsDir);
await fs.mkdirp(appsDir);
await fs.remove(appsPath);
await fs.mkdirp(appsPath);
await templateApp('Arm64Asar.app', 'arm64', async (appPath) => {
await fs.copy(
path.resolve(asarsDir, 'app.asar'),
Expand Down
2 changes: 1 addition & 1 deletion src/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
throw e;
}
}
if (p.includes('app.asar')) {
if (p.endsWith('.asar')) {
fileType = AppFileType.APP_CODE;
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
fileType = AppFileType.MACHO;
Expand Down
18 changes: 4 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import * as plist from 'plist';
import * as dircompare from 'dir-compare';

import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
import { AsarMode, detectAsarMode, generateAsarIntegrity, mergeASARs } from './asar-utils';
import { AsarMode, detectAsarMode, mergeASARs } from './asar-utils';
import { sha } from './sha';
import { d } from './debug';
import { computeIntegrityData } from './integrity';

/**
* Options to pass into the {@link makeUniversalApp} function.
Expand Down Expand Up @@ -251,9 +252,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
}
}

const generatedIntegrity: Record<string, { algorithm: 'SHA256'; hash: string }> = {};
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
Expand All @@ -271,8 +269,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
outputAsarPath: output,
singleArchFiles: opts.singleArchFiles,
});

generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(output);
} else if (x64AsarMode === AsarMode.HAS_ASAR) {
d('checking if the x64 and arm64 asars are identical');
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
Expand All @@ -281,7 +277,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
);

if (x64AsarSha !== arm64AsarSha) {
didSplitAsar = true;
d('x64 and arm64 asars are different');
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar');
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
Expand Down Expand Up @@ -329,18 +324,13 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
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 generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'));

const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
for (const plistFile of plistFiles) {
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);
Expand Down
51 changes: 51 additions & 0 deletions src/integrity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as fs from 'fs-extra';
import path from 'path';
import { AppFileType, getAllAppFiles } from './file-utils';
import { sha } from './sha';
import { generateAsarIntegrity } from './asar-utils';

type IntegrityMap = {
[filepath: string]: string;
};

export interface HeaderHash {
algorithm: 'SHA256';
hash: string;
}

export interface AsarIntegrity {
[key: string]: HeaderHash;
}

export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
const root = await fs.realpath(contentsPath);

const resourcesRelativePath = 'Resources';
const resourcesPath = path.resolve(root, resourcesRelativePath);

const resources = await getAllAppFiles(resourcesPath);
const resourceAsars = resources
.filter((file) => file.type === AppFileType.APP_CODE)
.reduce<IntegrityMap>(
(prev, file) => ({
...prev,
[path.join(resourcesRelativePath, file.relativePath)]: path.join(
resourcesPath,
file.relativePath,
),
}),
{},
);

// sort to produce constant result
const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) =>
name1.localeCompare(name2),
);
const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from)));
const asarIntegrity: AsarIntegrity = {};
for (let i = 0; i < allAsars.length; i++) {
const [asar] = allAsars[i];
asarIntegrity[asar] = hashes[i];
}
return asarIntegrity;
}
138 changes: 137 additions & 1 deletion test/__snapshots__/index.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,137 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
}
`;

exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 1`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;

exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 2`] = `
{
"files": {
"index.js": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
],
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
},
"size": 66,
},
"package.json": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
],
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
},
"size": 41,
},
"private": {
"files": {
"var": {
"files": {
"app": {
"files": {
"file.txt": {
"link": "private/var/file.txt",
},
},
},
"file.txt": {
"integrity": {
"algorithm": "SHA256",
"blockSize": 4194304,
"blocks": [
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
],
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
},
"size": 11,
},
},
},
},
},
"var": {
"link": "private/var",
},
},
}
`;

exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 3`] = `
{
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
},
"Resources/webbapp.asar": {
"algorithm": "SHA256",
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
},
},
}
`;

exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = `
{
"files": {
Expand Down Expand Up @@ -405,6 +536,11 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 4`]

exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = `
{
"Contents/Info.plist": {},
"Contents/Info.plist": {
"Resources/app.asar": {
"algorithm": "SHA256",
"hash": "27433ee3e34b3b0dabb29d18d40646126e80c56dbce8c4bb2adef7278b5a46c0",
},
},
}
`;
50 changes: 46 additions & 4 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import * as fs from 'fs-extra';
import * as path from 'path';

import { makeUniversalApp } from '../dist/cjs/index';
import { createTestApp, templateApp, VERIFY_APP_TIMEOUT, verifyApp } from './util';
import {
appsPath,
appsOutPath,
createTestApp,
templateApp,
VERIFY_APP_TIMEOUT,
verifyApp,
} from './util';
import { createPackage } from '@electron/asar';

const appsPath = path.resolve(__dirname, 'fixtures', 'apps');
const appsOutPath = path.resolve(__dirname, 'fixtures', 'apps', 'out');

// See `jest.setup.ts` for app fixture setup process
describe('makeUniversalApp', () => {
afterEach(async () => {
Expand Down Expand Up @@ -156,6 +160,44 @@ describe('makeUniversalApp', () => {
},
VERIFY_APP_TIMEOUT,
);
it(
'should generate AsarIntegrity for all asars in the application',
async () => {
const { testPath } = await createTestApp('app-2');
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
await createPackage(testPath, testAsarPath);

const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
);
});
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
);
await fs.copyFile(
testAsarPath,
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
);
});
const outAppPath = path.resolve(appsOutPath, 'MultipleAsars.app');
await makeUniversalApp({
x64AppPath,
arm64AppPath,
outAppPath,
mergeASARs: true,
});
await verifyApp(outAppPath);
},
VERIFY_APP_TIMEOUT,
);
});

describe('no asar mode', () => {
Expand Down
Loading

0 comments on commit 3ba0f9b

Please sign in to comment.