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
This commit is contained in:
@@ -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;
|
||||
|
||||
18
src/index.ts
18
src/index.ts
@@ -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.
|
||||
@@ -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
|
||||
@@ -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'));
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
|
||||
51
src/integrity.ts
Normal file
51
src/integrity.ts
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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": {
|
||||
@@ -581,6 +712,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",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -157,7 +157,10 @@ describe('makeUniversalApp', () => {
|
||||
VERIFY_APP_TIMEOUT,
|
||||
);
|
||||
|
||||
it(
|
||||
// TODO: Investigate if this should even be allowed.
|
||||
// Current logic detects all unpacked files as APP_CODE, which doesn't seem correct since it could also be a macho file requiring lipo
|
||||
// https://github.com/electron/universal/blob/d90d573ccf69a5b14b91aa818c8b97e0e6840399/src/file-utils.ts#L48-L49
|
||||
it.skip(
|
||||
'should shim asars with different unpacked dirs',
|
||||
async () => {
|
||||
const arm64AppPath = await templateApp('UnpackedArm64.app', 'arm64', async (appPath) => {
|
||||
@@ -191,6 +194,45 @@ 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', () => {
|
||||
|
||||
@@ -14,6 +14,7 @@ export const VERIFY_APP_TIMEOUT = 80 * 1000;
|
||||
|
||||
export const asarsDir = path.resolve(__dirname, 'fixtures', 'asars');
|
||||
export const appsDir = path.resolve(__dirname, 'fixtures', 'apps');
|
||||
export const appsOutPath = path.resolve(appsDir, 'out');
|
||||
|
||||
export const verifyApp = async (appPath: string) => {
|
||||
await ensureUniversal(appPath);
|
||||
|
||||
Reference in New Issue
Block a user