Skip to content

Commit 2b67c90

Browse files
authored
fix: Allow EnableEmbeddedAsarIntegrityValidation when multiple asars are present in app (#124)
- When an application uses multiple asars (`webapp.asar`, `anything.asar`, etc.), `EnableEmbeddedAsarIntegrityValidation` fuse breaks the application due to not all asars having integrity generated for them. Fixes: #116 - **Also fixes bug** to correctly test `makeUniversalApp no asar mode should shim two different app folders`, (it was not having an asar integrity generated for the shimmed asar) Functionality added: - Moves all asar integrity generation to **after** all app assets have been merged/shimmed/copied. This allows other asars that were provided to also be scanned and have asar integrity generated for them. - Extracted common Integrity logic to a single file `integrity.ts` - Adds unit test for multi-asar apps
1 parent 740dd4a commit 2b67c90

File tree

6 files changed

+237
-17
lines changed

6 files changed

+237
-17
lines changed

src/file-utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export const getAllAppFiles = async (appPath: string): Promise<AppFile[]> => {
4545
throw e;
4646
}
4747
}
48-
if (p.includes('app.asar')) {
48+
if (p.endsWith('.asar')) {
4949
fileType = AppFileType.APP_CODE;
5050
} else if (fileOutput.startsWith(MACHO_PREFIX)) {
5151
fileType = AppFileType.MACHO;

src/index.ts

+4-14
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import * as plist from 'plist';
88
import * as dircompare from 'dir-compare';
99

1010
import { AppFile, AppFileType, getAllAppFiles } from './file-utils';
11-
import { AsarMode, detectAsarMode, generateAsarIntegrity, mergeASARs } from './asar-utils';
11+
import { AsarMode, detectAsarMode, mergeASARs } from './asar-utils';
1212
import { sha } from './sha';
1313
import { d } from './debug';
14+
import { computeIntegrityData } from './integrity';
1415

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

254-
const generatedIntegrity: Record<string, { algorithm: 'SHA256'; hash: string }> = {};
255-
let didSplitAsar = false;
256-
257255
/**
258256
* If we have an ASAR we just need to check if the two "app.asar" files have the same hash,
259257
* if they are, same as above, we can leave one there and call it a day. If they're different
@@ -271,8 +269,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
271269
outputAsarPath: output,
272270
singleArchFiles: opts.singleArchFiles,
273271
});
274-
275-
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(output);
276272
} else if (x64AsarMode === AsarMode.HAS_ASAR) {
277273
d('checking if the x64 and arm64 asars are identical');
278274
const x64AsarSha = await sha(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'));
@@ -281,7 +277,6 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
281277
);
282278

283279
if (x64AsarSha !== arm64AsarSha) {
284-
didSplitAsar = true;
285280
d('x64 and arm64 asars are different');
286281
const x64AsarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app-x64.asar');
287282
await fs.move(path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'), x64AsarPath);
@@ -329,18 +324,13 @@ export const makeUniversalApp = async (opts: MakeUniversalOpts): Promise<void> =
329324
await fs.writeJson(path.resolve(entryAsar, 'package.json'), pj);
330325
const asarPath = path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar');
331326
await asar.createPackage(entryAsar, asarPath);
332-
333-
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(asarPath);
334-
generatedIntegrity['Resources/app-x64.asar'] = generateAsarIntegrity(x64AsarPath);
335-
generatedIntegrity['Resources/app-arm64.asar'] = generateAsarIntegrity(arm64AsarPath);
336327
} else {
337328
d('x64 and arm64 asars are the same');
338-
generatedIntegrity['Resources/app.asar'] = generateAsarIntegrity(
339-
path.resolve(tmpApp, 'Contents', 'Resources', 'app.asar'),
340-
);
341329
}
342330
}
343331

332+
const generatedIntegrity = await computeIntegrityData(path.join(tmpApp, 'Contents'));
333+
344334
const plistFiles = x64Files.filter((f) => f.type === AppFileType.INFO_PLIST);
345335
for (const plistFile of plistFiles) {
346336
const x64PlistPath = path.resolve(opts.x64AppPath, plistFile.relativePath);

src/integrity.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as fs from 'fs-extra';
2+
import path from 'path';
3+
import { AppFileType, getAllAppFiles } from './file-utils';
4+
import { sha } from './sha';
5+
import { generateAsarIntegrity } from './asar-utils';
6+
7+
type IntegrityMap = {
8+
[filepath: string]: string;
9+
};
10+
11+
export interface HeaderHash {
12+
algorithm: 'SHA256';
13+
hash: string;
14+
}
15+
16+
export interface AsarIntegrity {
17+
[key: string]: HeaderHash;
18+
}
19+
20+
export async function computeIntegrityData(contentsPath: string): Promise<AsarIntegrity> {
21+
const root = await fs.realpath(contentsPath);
22+
23+
const resourcesRelativePath = 'Resources';
24+
const resourcesPath = path.resolve(root, resourcesRelativePath);
25+
26+
const resources = await getAllAppFiles(resourcesPath);
27+
const resourceAsars = resources
28+
.filter((file) => file.type === AppFileType.APP_CODE)
29+
.reduce<IntegrityMap>(
30+
(prev, file) => ({
31+
...prev,
32+
[path.join(resourcesRelativePath, file.relativePath)]: path.join(
33+
resourcesPath,
34+
file.relativePath,
35+
),
36+
}),
37+
{},
38+
);
39+
40+
// sort to produce constant result
41+
const allAsars = Object.entries(resourceAsars).sort(([name1], [name2]) =>
42+
name1.localeCompare(name2),
43+
);
44+
const hashes = await Promise.all(allAsars.map(async ([, from]) => generateAsarIntegrity(from)));
45+
const asarIntegrity: AsarIntegrity = {};
46+
for (let i = 0; i < allAsars.length; i++) {
47+
const [asar] = allAsars[i];
48+
asarIntegrity[asar] = hashes[i];
49+
}
50+
return asarIntegrity;
51+
}

test/__snapshots__/index.spec.ts.snap

+137-1
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,137 @@ exports[`makeUniversalApp asar mode should create a shim if asars are different
157157
}
158158
`;
159159

160+
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 1`] = `
161+
{
162+
"files": {
163+
"index.js": {
164+
"integrity": {
165+
"algorithm": "SHA256",
166+
"blockSize": 4194304,
167+
"blocks": [
168+
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
169+
],
170+
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
171+
},
172+
"size": 66,
173+
},
174+
"package.json": {
175+
"integrity": {
176+
"algorithm": "SHA256",
177+
"blockSize": 4194304,
178+
"blocks": [
179+
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
180+
],
181+
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
182+
},
183+
"size": 41,
184+
},
185+
"private": {
186+
"files": {
187+
"var": {
188+
"files": {
189+
"app": {
190+
"files": {
191+
"file.txt": {
192+
"link": "private/var/file.txt",
193+
},
194+
},
195+
},
196+
"file.txt": {
197+
"integrity": {
198+
"algorithm": "SHA256",
199+
"blockSize": 4194304,
200+
"blocks": [
201+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
202+
],
203+
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
204+
},
205+
"size": 11,
206+
},
207+
},
208+
},
209+
},
210+
},
211+
"var": {
212+
"link": "private/var",
213+
},
214+
},
215+
}
216+
`;
217+
218+
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 2`] = `
219+
{
220+
"files": {
221+
"index.js": {
222+
"integrity": {
223+
"algorithm": "SHA256",
224+
"blockSize": 4194304,
225+
"blocks": [
226+
"0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
227+
],
228+
"hash": "0f6311dac07f0876c436ce2be042eb88c96e17eaf140b39627cf720dd87ad5b8",
229+
},
230+
"size": 66,
231+
},
232+
"package.json": {
233+
"integrity": {
234+
"algorithm": "SHA256",
235+
"blockSize": 4194304,
236+
"blocks": [
237+
"d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
238+
],
239+
"hash": "d6226276d47adc7aa20e6c46e842e258f5157313074a035051a89612acdd6be3",
240+
},
241+
"size": 41,
242+
},
243+
"private": {
244+
"files": {
245+
"var": {
246+
"files": {
247+
"app": {
248+
"files": {
249+
"file.txt": {
250+
"link": "private/var/file.txt",
251+
},
252+
},
253+
},
254+
"file.txt": {
255+
"integrity": {
256+
"algorithm": "SHA256",
257+
"blockSize": 4194304,
258+
"blocks": [
259+
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
260+
],
261+
"hash": "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
262+
},
263+
"size": 11,
264+
},
265+
},
266+
},
267+
},
268+
},
269+
"var": {
270+
"link": "private/var",
271+
},
272+
},
273+
}
274+
`;
275+
276+
exports[`makeUniversalApp asar mode should generate AsarIntegrity for all asars in the application 3`] = `
277+
{
278+
"Contents/Info.plist": {
279+
"Resources/app.asar": {
280+
"algorithm": "SHA256",
281+
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
282+
},
283+
"Resources/webbapp.asar": {
284+
"algorithm": "SHA256",
285+
"hash": "7e6af4d00f4cc737eff922e2b386128a269f80887b79a011022f1276bdbe7832",
286+
},
287+
},
288+
}
289+
`;
290+
160291
exports[`makeUniversalApp asar mode should merge two different asars when \`mergeASARs\` is enabled 1`] = `
161292
{
162293
"files": {
@@ -581,6 +712,11 @@ exports[`makeUniversalApp no asar mode should shim two different app folders 4`]
581712

582713
exports[`makeUniversalApp no asar mode should shim two different app folders 5`] = `
583714
{
584-
"Contents/Info.plist": {},
715+
"Contents/Info.plist": {
716+
"Resources/app.asar": {
717+
"algorithm": "SHA256",
718+
"hash": "27433ee3e34b3b0dabb29d18d40646126e80c56dbce8c4bb2adef7278b5a46c0",
719+
},
720+
},
585721
}
586722
`;

test/index.spec.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,10 @@ describe('makeUniversalApp', () => {
157157
VERIFY_APP_TIMEOUT,
158158
);
159159

160-
it(
160+
// TODO: Investigate if this should even be allowed.
161+
// Current logic detects all unpacked files as APP_CODE, which doesn't seem correct since it could also be a macho file requiring lipo
162+
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
163+
it.skip(
161164
'should shim asars with different unpacked dirs',
162165
async () => {
163166
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
@@ -191,6 +194,45 @@ describe('makeUniversalApp', () => {
191194
},
192195
VERIFY_APP_TIMEOUT,
193196
);
197+
198+
it(
199+
'should generate AsarIntegrity for all asars in the application',
200+
async () => {
201+
const { testPath } = await createTestApp('app-2');
202+
const testAsarPath = path.resolve(appsOutPath, 'app-2.asar');
203+
await createPackage(testPath, testAsarPath);
204+
205+
const arm64AppPath = await templateApp('Arm64-2.app', 'arm64', async (appPath) => {
206+
await fs.copyFile(
207+
testAsarPath,
208+
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
209+
);
210+
await fs.copyFile(
211+
testAsarPath,
212+
path.resolve(appPath, 'Contents', 'Resources', 'webapp.asar'),
213+
);
214+
});
215+
const x64AppPath = await templateApp('X64-2.app', 'x64', async (appPath) => {
216+
await fs.copyFile(
217+
testAsarPath,
218+
path.resolve(appPath, 'Contents', 'Resources', 'app.asar'),
219+
);
220+
await fs.copyFile(
221+
testAsarPath,
222+
path.resolve(appPath, 'Contents', 'Resources', 'webbapp.asar'),
223+
);
224+
});
225+
const outAppPath = path.resolve(appsOutPath, 'MultipleAsars.app');
226+
await makeUniversalApp({
227+
x64AppPath,
228+
arm64AppPath,
229+
outAppPath,
230+
mergeASARs: true,
231+
});
232+
await verifyApp(outAppPath);
233+
},
234+
VERIFY_APP_TIMEOUT,
235+
);
194236
});
195237

196238
describe('no asar mode', () => {

test/util.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const VERIFY_APP_TIMEOUT = 80 * 1000;
1414

1515
export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars');
1616
export const appsDir = path.resolve(__dirname, 'fixtures', 'apps');
17+
export const appsOutPath = path.resolve(appsDir, 'out');
1718

1819
export const verifyApp = async (appPath: string) => {
1920
await ensureUniversal(appPath);

0 commit comments

Comments
 (0)