1+ import path from 'node:path' ;
2+ import crypto from 'node:crypto' ;
3+ import plist from 'plist' ;
4+
5+ import { wrappedFs as fs } from './wrapped-fs.js' ;
6+ import { FileRecord } from './disk.js' ;
7+
8+
9+ // Integrity digest type definitions
10+
11+ type IntegrityDigest < Version extends number , AdditionalParams > =
12+ | { used : false }
13+ | ( { used : true ; version : Version } & AdditionalParams ) ;
14+
15+ type IntegrityDigestV1 = IntegrityDigest < 1 , { sha256Digest : Buffer } > ;
16+
17+ type AnyIntegrityDigest = IntegrityDigestV1 ; // Extend this union type as new versions are added
18+
19+
20+ // Integrity digest calculation functions
21+
22+ type AsarIntegrity = Record <
23+ string ,
24+ Pick < FileRecord [ 'integrity' ] , 'algorithm' | 'hash' >
25+ > ;
26+
27+ function calculateIntegrityDigestV1 (
28+ asarIntegrity : AsarIntegrity ,
29+ ) : IntegrityDigestV1 {
30+ const integrityHash = crypto . createHash ( 'SHA256' ) ;
31+ for ( const key of Object . keys ( asarIntegrity ) . sort ( ) ) {
32+ const { algorithm, hash } = asarIntegrity [ key ] ;
33+ integrityHash . update ( key ) ;
34+ integrityHash . update ( algorithm ) ;
35+ integrityHash . update ( hash ) ;
36+ }
37+ return {
38+ used : true ,
39+ version : 1 ,
40+ sha256Digest : integrityHash . digest ( ) ,
41+ }
42+ }
43+
44+ export function calculateIntegrityDigestV1ForApp (
45+ appPath : string ,
46+ ) : IntegrityDigestV1 {
47+ const plistPath = path . join ( appPath , 'Contents' , 'Info.plist' ) ;
48+ const plistBuffer = fs . readFileSync ( plistPath ) ;
49+ const plistData = plist . parse ( plistBuffer . toString ( ) ) as Record < string , any > ;
50+ const asarIntegrity = plistData [ 'ElectronAsarIntegrity' ] as AsarIntegrity ;
51+ return calculateIntegrityDigestV1 ( asarIntegrity ) ;
52+ }
53+
54+
55+ /// Integrity digest handling errors
56+
57+ const UnknownIntegrityDigestVersionError = class extends Error {
58+ constructor ( version : number ) {
59+ super ( `Unknown integrity digest version: ${ version } ` ) ;
60+ this . name = 'UnknownIntegrityDigestVersionError' ;
61+ }
62+ } ;
63+
64+
65+ // Integrity digest storage and retrieval functions
66+
67+ const INTEGRITY_DIGEST_SENTINEL = 'AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A' ;
68+
69+ function pathToIntegrityDigestFile ( appPath : string ) {
70+ if ( appPath . endsWith ( '.app' ) ) {
71+ return path . resolve (
72+ appPath ,
73+ 'Contents' ,
74+ 'Frameworks' ,
75+ 'Electron Framework.framework' ,
76+ 'Electron Framework' ,
77+ ) ;
78+ }
79+ throw new Error ( 'App path must be an .app bundle' ) ;
80+ }
81+
82+ function forEachSentinelInApp (
83+ appPath : string ,
84+ callback : ( sentinelIndex : number , integrityFile : Buffer ) => void ,
85+ writeBack : boolean = false ,
86+ ) {
87+ const integrityFilePath = pathToIntegrityDigestFile ( appPath ) ;
88+ const integrityFile = fs . readFileSync ( integrityFilePath ) ;
89+ let searchCursor = 0 ;
90+ const sentinelAsBuffer = Buffer . from ( INTEGRITY_DIGEST_SENTINEL ) ;
91+ do {
92+ const sentinelIndex = integrityFile . indexOf ( sentinelAsBuffer , searchCursor ) ;
93+ if ( sentinelIndex === - 1 ) break ;
94+ callback ( sentinelIndex , integrityFile ) ;
95+ searchCursor = sentinelIndex + sentinelAsBuffer . length ;
96+ } while ( true ) ;
97+ if ( writeBack ) {
98+ fs . writeFileSync ( integrityFilePath , integrityFile ) ;
99+ }
100+ }
101+
102+ export function doDigestsMatch (
103+ digestA : AnyIntegrityDigest ,
104+ digestB : AnyIntegrityDigest ,
105+ ) : boolean {
106+ if ( digestA . used !== digestB . used ) return false ;
107+ if ( digestA . used && digestB . used ) {
108+ if ( digestA . version !== digestB . version ) return false ;
109+ switch ( digestA . version ) {
110+ case 1 :
111+ return digestA . sha256Digest . equals ( digestB . sha256Digest ) ;
112+ default :
113+ throw new UnknownIntegrityDigestVersionError ( digestA . version ) ;
114+ }
115+ } else return true ;
116+ }
117+
118+ function sentinelIndexToDigest < T extends AnyIntegrityDigest > (
119+ integrityFile : Buffer ,
120+ sentinelIndex : number ,
121+ ) : T {
122+ const used = integrityFile . readUInt8 ( sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length ) === 1 ;
123+ if ( ! used ) {
124+ return { used : false } as T ;
125+ } else {
126+ const version = integrityFile . readUInt8 ( sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 1 ) ;
127+ switch ( version ) {
128+ case 1 : {
129+ const sha256Digest = integrityFile . subarray (
130+ sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 2 ,
131+ sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 2 + 32 , // SHA256 digest size
132+ ) ;
133+ return {
134+ used : true ,
135+ version : 1 ,
136+ sha256Digest,
137+ } as T ;
138+ }
139+ default :
140+ throw new UnknownIntegrityDigestVersionError ( version ) ;
141+ }
142+ }
143+ }
144+
145+ export async function getStoredIntegrityDigestForApp < T extends AnyIntegrityDigest > (
146+ appPath : string ,
147+ ) : Promise < T > {
148+ let lastDigestFound : T | null = null ;
149+ forEachSentinelInApp ( appPath , ( sentinelIndex , integrityFile ) => {
150+ const currentDigest = sentinelIndexToDigest < T > (
151+ integrityFile ,
152+ sentinelIndex ,
153+ ) ;
154+ if ( lastDigestFound === null ) {
155+ lastDigestFound = currentDigest ;
156+ } else if ( ! doDigestsMatch ( currentDigest , lastDigestFound ) ) {
157+ throw new Error ( 'Multiple differing integrity digests found in the binary' ) ;
158+ }
159+ lastDigestFound = currentDigest ;
160+ } ) ;
161+ if ( lastDigestFound === null ) {
162+ throw new Error ( 'No integrity digest found in the binary' ) ;
163+ }
164+ return lastDigestFound ;
165+ }
166+
167+ export async function setStoredIntegrityDigestForApp < T extends AnyIntegrityDigest > (
168+ appPath : string ,
169+ digest : T ,
170+ ) : Promise < void > {
171+ if ( digest . used === true && digest . version !== 1 ) {
172+ throw new UnknownIntegrityDigestVersionError ( digest . version ) ;
173+ }
174+ forEachSentinelInApp ( appPath , ( sentinelIndex , integrityFile ) => {
175+ integrityFile . writeUInt8 ( digest . used ? 1 : 0 , sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length ) ;
176+ const oldVersion = integrityFile . readUInt8 ( sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 1 ) ;
177+ switch ( oldVersion ) {
178+ case 1 :
179+ integrityFile . fill (
180+ 0 ,
181+ sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 2 ,
182+ sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 2 + 32 , // SHA256 digest size
183+ ) ;
184+ break ;
185+ }
186+ if ( digest . used ) {
187+ integrityFile . writeUInt8 (
188+ digest . version ,
189+ sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 1 ,
190+ ) ;
191+ switch ( digest . version ) {
192+ case 1 : {
193+ const v1Digest = digest as IntegrityDigestV1 & { used : true } ;
194+ v1Digest . sha256Digest . copy (
195+ integrityFile ,
196+ sentinelIndex + INTEGRITY_DIGEST_SENTINEL . length + 2 ,
197+ ) ;
198+ break ;
199+ }
200+ default :
201+ throw new UnknownIntegrityDigestVersionError ( digest . version ) ;
202+ }
203+ }
204+ } , true ) ;
205+ }
206+
207+
208+ // High-level integrity digest management functions
209+
210+ export function printDigest ( digest : AnyIntegrityDigest , prefix : string = '' ) {
211+ const digestLogger = prefix ? ( s : string , ...args : any [ ] ) => console . log ( prefix + s , ...args ) : console . log ;
212+ if ( ! digest . used ) {
213+ digestLogger ( 'Integrity digest is OFF' ) ;
214+ return ;
215+ }
216+ digestLogger ( 'Integrity digest is ON (version: %d)' , digest . version ) ;
217+ switch ( digest . version ) {
218+ case 1 :
219+ digestLogger ( '\tDigest (SHA256): %s' , digest . sha256Digest . toString ( 'hex' ) ) ;
220+ break ;
221+ default :
222+ digestLogger ( '\tUnknown metadata for digest version: %d' , digest . version ) ;
223+ }
224+ }
225+
226+ export async function enableIntegrityDigestForApp (
227+ appPath : string ,
228+ ) : Promise < void > {
229+ try {
230+ console . log ( 'Calculating integrity digest...' ) ;
231+ const digest = calculateIntegrityDigestV1ForApp ( appPath ) ;
232+ console . log ( 'Turning integrity digest ON...' ) ;
233+ await setStoredIntegrityDigestForApp ( appPath , digest ) ;
234+ console . log ( 'Integrity digest turned ON' ) ;
235+ } catch ( e ) {
236+ const errorMessage = e instanceof Error ? e . message : String ( e ) ;
237+ console . log ( 'Failed to turn ON integrity digest: %s' , errorMessage ) ;
238+ }
239+ }
240+
241+ export async function disableIntegrityDigestForApp (
242+ appPath : string ,
243+ ) : Promise < void > {
244+ try {
245+ console . log ( 'Turning integrity digest OFF...' ) ;
246+ await setStoredIntegrityDigestForApp ( appPath , { used : false } ) ;
247+ console . log ( 'Integrity digest turned OFF' ) ;
248+ } catch ( e ) {
249+ const errorMessage = e instanceof Error ? e . message : String ( e ) ;
250+ console . log ( 'Failed to turn OFF integrity digest: %s' , errorMessage ) ;
251+ }
252+ }
253+
254+ export async function printStoredIntegrityDigestForApp (
255+ appPath : string ,
256+ ) : Promise < void > {
257+ try {
258+ const storedDigest = await getStoredIntegrityDigestForApp ( appPath ) ;
259+ printDigest ( storedDigest ) ;
260+ } catch ( e ) {
261+ const errorMessage = e instanceof Error ? e . message : String ( e ) ;
262+ console . log ( 'Failed to read integrity digest: %s' , errorMessage ) ;
263+ }
264+ }
265+
266+ export async function verifyIntegrityDigestForApp (
267+ appPath : string ,
268+ ) : Promise < void > {
269+ try {
270+ const storedDigest = await getStoredIntegrityDigestForApp ( appPath ) ;
271+ if ( ! storedDigest . used ) {
272+ console . log ( 'Integrity digest is off, verification SKIPPED' ) ;
273+ return ;
274+ }
275+ const calculatedDigest = calculateIntegrityDigestV1ForApp ( appPath ) ;
276+ if ( doDigestsMatch ( storedDigest , calculatedDigest ) ) {
277+ console . log ( 'Integrity digest verification PASSED' ) ;
278+ } else {
279+ console . log ( 'Integrity digest verification FAILED' ) ;
280+ console . log ( 'Expected digest:' ) ;
281+ printDigest ( calculatedDigest , '\t' ) ;
282+ console . log ( 'Actual digest:' ) ;
283+ printDigest ( storedDigest , '\t' ) ;
284+ }
285+ } catch ( e ) {
286+ const errorMessage = e instanceof Error ? e . message : String ( e ) ;
287+ console . log ( 'Failed to verify integrity digest: %s' , errorMessage ) ;
288+ }
289+ }
0 commit comments