@@ -2,17 +2,38 @@ import { App } from './platform.js';
22import { debug , sanitizeAppName , subOptionWarning , warning } from './common.js' ;
33import fs from 'graceful-fs' ;
44import { promisifiedGracefulFs } from './util.js' ;
5+ import crypto from 'node:crypto' ;
56import path from 'node:path' ;
6- import plist , { PlistValue } from 'plist' ;
7+ import plist , { PlistObject , PlistValue } from 'plist' ;
78import { notarize , NotarizeOptions } from '@electron/notarize' ;
89import { ElectronMacPlatform , sign , SignOptions } from '@electron/osx-sign' ;
10+ import semver from 'semver' ;
911import { ProcessedOptionsWithSinglePlatformArch } from './types.js' ;
1012import { generateAssetCatalogForIcon } from './icon-composer.js' ;
1113
1214type NSUsageDescription = {
1315 [ key in `NS${string } UsageDescription`] : string ;
1416} ;
1517
18+ type AsarIntegrity = NonNullable < App [ 'asarIntegrity' ] > ;
19+
20+ function isAsarIntegrity ( value : unknown ) : value is AsarIntegrity {
21+ if ( typeof value !== 'object' || value === null ) return false ;
22+ const entries = Object . values ( value ) ;
23+ if ( entries . length === 0 ) return false ;
24+ return entries . every (
25+ ( entry ) =>
26+ typeof entry === 'object' &&
27+ entry !== null &&
28+ 'algorithm' in entry &&
29+ typeof entry . algorithm === 'string' &&
30+ entry . algorithm . length > 0 &&
31+ 'hash' in entry &&
32+ typeof entry . hash === 'string' &&
33+ entry . hash . length > 0 ,
34+ ) ;
35+ }
36+
1637type BasePList = {
1738 CFBundleDisplayName : string ;
1839 CFBundleExecutable : string ;
@@ -510,6 +531,135 @@ export class MacApp extends App implements Plists {
510531 await fs . promises . rename ( this . electronAppPath , this . renamedAppPath ) ;
511532 }
512533
534+ /**
535+ * Sentinel string embedded in the Electron Framework binary, used as a marker
536+ * for the integrity digest storage location.
537+ * @see https://github.com/electron/electron/blob/2d5597b1b0fa697905380184e26c9f0947e05c5d/shell/common/asar/integrity_digest.mm#L24
538+ */
539+ static INTEGRITY_DIGEST_SENTINEL = 'AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A' ;
540+
541+ async setIntegrityDigest ( ) {
542+ if (
543+ ! this . opts . electronVersion ||
544+ ! semver . valid ( this . opts . electronVersion )
545+ ) {
546+ debug (
547+ `Cannot determine Electron version (got "${ this . opts . electronVersion } "), skipping integrity digest` ,
548+ ) ;
549+ return ;
550+ }
551+ if ( ! semver . gte ( this . opts . electronVersion , '41.0.0-alpha.1' ) ) {
552+ return ;
553+ }
554+
555+ const appPath = this . renamedAppPath ?? this . electronAppPath ;
556+ let integrity = this . asarIntegrity ;
557+
558+ // For universal builds, asarIntegrity isn't set on the shell App instance.
559+ // Fall back to reading it from the merged app's Info.plist.
560+ if ( ! integrity || Object . keys ( integrity ) . length === 0 ) {
561+ const plistPath = path . join ( appPath , 'Contents' , 'Info.plist' ) ;
562+ if ( fs . existsSync ( plistPath ) ) {
563+ try {
564+ const plistData = plist . parse (
565+ ( await fs . promises . readFile ( plistPath ) ) . toString ( ) ,
566+ ) as PlistObject ;
567+ if ( isAsarIntegrity ( plistData . ElectronAsarIntegrity ) ) {
568+ integrity = plistData . ElectronAsarIntegrity ;
569+ }
570+ } catch ( err ) {
571+ warning (
572+ `Failed to read asar integrity from ${ plistPath } : ${ err } . The integrity digest will not be written.` ,
573+ this . opts . quiet ,
574+ ) ;
575+ return ;
576+ }
577+ }
578+ }
579+
580+ if ( ! integrity || Object . keys ( integrity ) . length === 0 ) {
581+ return ;
582+ }
583+ const frameworkPath = path . join (
584+ appPath ,
585+ 'Contents' ,
586+ 'Frameworks' ,
587+ 'Electron Framework.framework' ,
588+ 'Electron Framework' ,
589+ ) ;
590+ if ( ! fs . existsSync ( frameworkPath ) ) {
591+ warning (
592+ `Electron Framework binary not found at ${ frameworkPath } . The asar integrity digest will not be written, which may cause runtime failures.` ,
593+ this . opts . quiet ,
594+ ) ;
595+ return ;
596+ }
597+
598+ // Calculate v1 integrity digest: SHA256 over sorted (key, algorithm, hash) tuples
599+ // @see https://github.com/electron/electron/blob/2d5597b1b0fa697905380184e26c9f0947e05c5d/shell/common/asar/integrity_digest.mm#L52-L66
600+ const integrityHash = crypto . createHash ( 'SHA256' ) ;
601+ for ( const key of Object . keys ( integrity ) . sort ( ) ) {
602+ const { algorithm, hash } = integrity [ key ] ;
603+ integrityHash . update ( key ) ;
604+ integrityHash . update ( algorithm ) ;
605+ integrityHash . update ( hash ) ;
606+ }
607+ const digest = integrityHash . digest ( ) ;
608+
609+ // Write digest into every sentinel location in the Electron Framework binary
610+ const frameworkBinary = await fs . promises . readFile ( frameworkPath ) ;
611+ const sentinel = Buffer . from ( MacApp . INTEGRITY_DIGEST_SENTINEL ) ;
612+ let searchOffset = 0 ;
613+ let found = false ;
614+ let written = false ;
615+
616+ // eslint-disable-next-line no-constant-condition
617+ while ( true ) {
618+ const sentinelIndex = frameworkBinary . indexOf ( sentinel , searchOffset ) ;
619+ if ( sentinelIndex === - 1 ) break ;
620+ found = true ;
621+
622+ const base = sentinelIndex + sentinel . length ;
623+ if ( base + 34 > frameworkBinary . length ) {
624+ warning (
625+ `Insufficient space after integrity digest sentinel at offset ${ sentinelIndex } in Electron Framework binary. The binary may be corrupted or incompatible.` ,
626+ this . opts . quiet ,
627+ ) ;
628+ searchOffset = base ;
629+ continue ;
630+ }
631+ frameworkBinary . writeUInt8 ( 1 , base ) ; // used = true
632+ frameworkBinary . writeUInt8 ( 1 , base + 1 ) ; // version = 1
633+ digest . copy ( frameworkBinary , base + 2 ) ; // 32-byte SHA256 digest
634+ written = true ;
635+
636+ searchOffset = base + 2 + 32 ;
637+ }
638+
639+ if ( found && ! written ) {
640+ throw new Error (
641+ 'Found integrity digest sentinel(s) in Electron Framework binary but could not write to any of them. The binary may be corrupted.' ,
642+ ) ;
643+ }
644+
645+ if ( written ) {
646+ try {
647+ await fs . promises . writeFile ( frameworkPath , frameworkBinary ) ;
648+ } catch ( err ) {
649+ throw new Error (
650+ `Failed to write integrity digest to Electron Framework binary at ${ frameworkPath } : ${ err } ` ,
651+ ) ;
652+ }
653+ debug ( 'Wrote integrity digest to Electron Framework binary' ) ;
654+ } else {
655+ warning (
656+ `No integrity digest sentinel found in Electron Framework binary at ${ frameworkPath } . ` +
657+ 'This is unexpected for Electron >= 41.0.0. The asar integrity digest was not written.' ,
658+ this . opts . quiet ,
659+ ) ;
660+ }
661+ }
662+
513663 async signAppIfSpecified ( ) {
514664 const osxSignOpt = this . opts . osxSign ;
515665 const platform = this . opts . platform ;
@@ -575,6 +725,7 @@ export class MacApp extends App implements Plists {
575725 await this . renameElectron ( ) ;
576726 await this . renameAppAndHelpers ( ) ;
577727 await this . copyExtraResources ( ) ;
728+ await this . setIntegrityDigest ( ) ;
578729 await this . signAppIfSpecified ( ) ;
579730 await this . notarizeAppIfSpecified ( ) ;
580731 return this . move ( ) ;
0 commit comments