asar-integrity-spec.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import { getRawHeader } from '@electron/asar';
  2. import { flipFuses, FuseV1Config, FuseV1Options, FuseVersion } from '@electron/fuses';
  3. import { resedit } from '@electron/packager/dist/resedit';
  4. import { expect } from 'chai';
  5. import * as originalFs from 'node:original-fs';
  6. import * as cp from 'node:child_process';
  7. import * as nodeCrypto from 'node:crypto';
  8. import * as fs from 'node:fs';
  9. import * as os from 'node:os';
  10. import * as path from 'node:path';
  11. import { copyApp } from './lib/fs-helpers';
  12. import { ifdescribe } from './lib/spec-helpers';
  13. const bufferReplace = (haystack: Buffer, needle: string, replacement: string, throwOnMissing = true): Buffer => {
  14. const needleBuffer = Buffer.from(needle);
  15. const idx = haystack.indexOf(needleBuffer);
  16. if (idx === -1) {
  17. if (throwOnMissing) throw new Error(`Needle ${needle} not found in haystack`);
  18. return haystack;
  19. }
  20. const before = haystack.slice(0, idx);
  21. const after = bufferReplace(haystack.slice(idx + needleBuffer.length), needle, replacement, false);
  22. const len = idx + replacement.length + after.length;
  23. return Buffer.concat([before, Buffer.from(replacement), after], len);
  24. };
  25. type SpawnResult = { code: number | null, out: string, signal: NodeJS.Signals | null };
  26. function spawn (cmd: string, args: string[], opts: any = {}) {
  27. let out = '';
  28. const child = cp.spawn(cmd, args, opts);
  29. child.stdout.on('data', (chunk: Buffer) => {
  30. out += chunk.toString();
  31. });
  32. child.stderr.on('data', (chunk: Buffer) => {
  33. out += chunk.toString();
  34. });
  35. return new Promise<SpawnResult>((resolve) => {
  36. child.on('exit', (code, signal) => {
  37. resolve({
  38. code,
  39. signal,
  40. out
  41. });
  42. });
  43. });
  44. };
  45. const expectToHaveCrashed = (res: SpawnResult) => {
  46. if (process.platform === 'win32') {
  47. expect(res.code).to.not.equal(0);
  48. expect(res.code).to.not.equal(null);
  49. expect(res.signal).to.equal(null);
  50. } else {
  51. expect(res.code).to.equal(null);
  52. expect(res.signal).to.be.oneOf(['SIGABRT', 'SIGTRAP']);
  53. }
  54. };
  55. describe('fuses', function () {
  56. this.timeout(120000);
  57. let tmpDir: string;
  58. let appPath: string;
  59. const launchApp = (args: string[] = []) => {
  60. if (process.platform === 'darwin') {
  61. return spawn(path.resolve(appPath, 'Contents/MacOS/Electron'), args);
  62. }
  63. return spawn(appPath, args);
  64. };
  65. const ensureFusesBeforeEach = (fuses: Omit<FuseV1Config<boolean>, 'version' | 'strictlyRequireAllFuses' | 'resetAdhocDarwinSignature'>) => {
  66. beforeEach(async () => {
  67. await flipFuses(appPath, {
  68. version: FuseVersion.V1,
  69. resetAdHocDarwinSignature: true,
  70. ...fuses
  71. });
  72. });
  73. };
  74. beforeEach(async () => {
  75. tmpDir = await fs.promises.mkdtemp(path.resolve(os.tmpdir(), 'electron-asar-integrity-spec-'));
  76. appPath = await copyApp(tmpDir);
  77. });
  78. afterEach(async () => {
  79. for (let attempt = 0; attempt <= 3; attempt++) {
  80. // Sometimes windows holds on to a DLL during the crash for a little bit, so we try a few times to delete it
  81. if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 500 * attempt));
  82. try {
  83. await originalFs.promises.rm(tmpDir, { recursive: true });
  84. break;
  85. } catch {}
  86. }
  87. });
  88. ifdescribe((process.platform === 'win32' && process.arch !== 'arm64') || process.platform === 'darwin')('ASAR Integrity', () => {
  89. let pathToAsar: string;
  90. beforeEach(async () => {
  91. if (process.platform === 'darwin') {
  92. pathToAsar = path.resolve(appPath, 'Contents', 'Resources', 'default_app.asar');
  93. } else {
  94. pathToAsar = path.resolve(path.dirname(appPath), 'resources', 'default_app.asar');
  95. }
  96. if (process.platform === 'win32') {
  97. await resedit(appPath, {
  98. asarIntegrity: {
  99. 'resources\\default_app.asar': {
  100. algorithm: 'SHA256',
  101. hash: nodeCrypto.createHash('sha256').update(getRawHeader(pathToAsar).headerString).digest('hex')
  102. }
  103. }
  104. });
  105. }
  106. });
  107. describe('when enabled', () => {
  108. ensureFusesBeforeEach({
  109. [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true
  110. });
  111. it('opens normally when unmodified', async () => {
  112. const res = await launchApp([path.resolve(__dirname, 'fixtures/apps/hello/hello.js')]);
  113. expect(res.code).to.equal(0);
  114. expect(res.signal).to.equal(null);
  115. expect(res.out).to.include('alive');
  116. });
  117. it('fatals if the integrity header does not match', async () => {
  118. const asar = await originalFs.promises.readFile(pathToAsar);
  119. // Ensure that the header still starts with the same thing, if build system
  120. // things result in the header changing we should update this test
  121. expect(asar.toString()).to.contain('{"files":{"default_app.js"');
  122. await originalFs.promises.writeFile(pathToAsar, bufferReplace(asar, '{"files":{"default_app.js"', '{"files":{"default_oop.js"'));
  123. const res = await launchApp(['--version']);
  124. expectToHaveCrashed(res);
  125. expect(res.out).to.include('Integrity check failed for asar archive');
  126. });
  127. it('fatals if a loaded main process JS file does not match', async () => {
  128. const asar = await originalFs.promises.readFile(pathToAsar);
  129. // Ensure that the header still starts with the same thing, if build system
  130. // things result in the header changing we should update this test
  131. expect(asar.toString()).to.contain('Invalid Usage');
  132. await originalFs.promises.writeFile(pathToAsar, bufferReplace(asar, 'Invalid Usage', 'VVValid Usage'));
  133. const res = await launchApp(['--version']);
  134. expect(res.code).to.equal(1);
  135. expect(res.signal).to.equal(null);
  136. expect(res.out).to.include('ASAR Integrity Violation: got a hash mismatch');
  137. });
  138. it('fatals if a renderer content file does not match', async () => {
  139. const asar = await originalFs.promises.readFile(pathToAsar);
  140. // Ensure that the header still starts with the same thing, if build system
  141. // things result in the header changing we should update this test
  142. expect(asar.toString()).to.contain('require-trusted-types-for');
  143. await originalFs.promises.writeFile(pathToAsar, bufferReplace(asar, 'require-trusted-types-for', 'require-trusted-types-not'));
  144. const res = await launchApp();
  145. expectToHaveCrashed(res);
  146. expect(res.out).to.include('Failed to validate block while ending ASAR file stream');
  147. });
  148. });
  149. describe('when disabled', () => {
  150. ensureFusesBeforeEach({
  151. [FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: false
  152. });
  153. it('does nothing if the integrity header does not match', async () => {
  154. const asar = await originalFs.promises.readFile(pathToAsar);
  155. // Ensure that the header still starts with the same thing, if build system
  156. // things result in the header changing we should update this test
  157. expect(asar.toString()).to.contain('{"files":{"default_app.js"');
  158. await originalFs.promises.writeFile(pathToAsar, bufferReplace(asar, '{"files":{"default_app.js"', '{"files":{"default_oop.js"'));
  159. const res = await launchApp(['--version']);
  160. expect(res.code).to.equal(0);
  161. });
  162. });
  163. });
  164. });