From ce16569063c212eaee570bcf1cf6e174d2e0c1c6 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Sat, 25 Oct 2025 22:54:38 -0400 Subject: [PATCH 1/3] feat: add integrity digest management commands and functionality --- bin/asar.mjs | 21 +++ package.json | 4 +- src/integrity-digest.ts | 289 ++++++++++++++++++++++++++++++++++++++++ yarn.lock | 44 ++++++ 4 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 src/integrity-digest.ts diff --git a/bin/asar.mjs b/bin/asar.mjs index 9724964..d54d1a3 100755 --- a/bin/asar.mjs +++ b/bin/asar.mjs @@ -2,6 +2,7 @@ import packageJSON from '../package.json' with { type: 'json' }; import { createPackageWithOptions, listPackage, extractFile, extractAll } from '../lib/asar.js'; +import { enableIntegrityDigestForApp, disableIntegrityDigestForApp, verifyIntegrityDigestForApp, printStoredIntegrityDigestForApp } from '../lib/integrity-digest.js'; import { program } from 'commander'; import fs from 'node:fs'; import path from 'node:path'; @@ -71,6 +72,26 @@ program.command('extract ') extractAll(archive, dest) }) +program.command('integrity-digest ') + .alias('id') + .description('manage integrity digest in app binary') + .action(async function (app, command) { + const allowedCommands = ['on', 'off', 'status', 'verify'] + switch (command) { + case 'on': await enableIntegrityDigestForApp(app) + break + case 'off': await disableIntegrityDigestForApp(app) + break + case 'status': await printStoredIntegrityDigestForApp(app) + break + case 'verify': await verifyIntegrityDigestForApp(app) + break + default: + console.log('Unknown integrity digest command: %s. Allowed commands are: %s', command, allowedCommands.join(', ')) + process.exit(1) + } + }) + program.command('*', { hidden: true}) .action(function (_cmd, args) { console.log('asar: \'%s\' is not an asar command. See \'asar --help\'.', args[0]) diff --git a/package.json b/package.json index 6993b8a..177b474 100644 --- a/package.json +++ b/package.json @@ -42,11 +42,13 @@ "dependencies": { "commander": "^13.1.0", "glob": "^11.0.1", - "minimatch": "^10.0.1" + "minimatch": "^10.0.1", + "plist": "^3.1.0" }, "devDependencies": { "@tsconfig/node22": "^22.0.1", "@types/node": "^22.12.0", + "@types/plist": "^3", "electron": "^35.7.5", "prettier": "^3.3.3", "typedoc": "~0.25.13", diff --git a/src/integrity-digest.ts b/src/integrity-digest.ts new file mode 100644 index 0000000..6e83840 --- /dev/null +++ b/src/integrity-digest.ts @@ -0,0 +1,289 @@ +import path from 'node:path'; +import crypto from 'node:crypto'; +import plist from 'plist'; + +import { wrappedFs as fs } from './wrapped-fs.js'; +import { FileRecord } from './disk.js'; + + +// Integrity digest type definitions + +type IntegrityDigest = + | { used: false } + | ({ used: true; version: Version } & AdditionalParams); + +type IntegrityDigestV1 = IntegrityDigest<1, { sha256Digest: Buffer }>; + +type AnyIntegrityDigest = IntegrityDigestV1; // Extend this union type as new versions are added + + +// Integrity digest calculation functions + +type AsarIntegrity = Record< + string, + Pick +>; + +function calculateIntegrityDigestV1( + asarIntegrity: AsarIntegrity, +): IntegrityDigestV1 { + const integrityHash = crypto.createHash('SHA256'); + for (const key of Object.keys(asarIntegrity).sort()) { + const { algorithm, hash } = asarIntegrity[key]; + integrityHash.update(key); + integrityHash.update(algorithm); + integrityHash.update(hash); + } + return { + used: true, + version: 1, + sha256Digest: integrityHash.digest(), + } +} + +export function calculateIntegrityDigestV1ForApp( + appPath: string, +): IntegrityDigestV1 { + const plistPath = path.join(appPath, 'Contents', 'Info.plist'); + const plistBuffer = fs.readFileSync(plistPath); + const plistData = plist.parse(plistBuffer.toString()) as Record; + const asarIntegrity = plistData['ElectronAsarIntegrity'] as AsarIntegrity; + return calculateIntegrityDigestV1(asarIntegrity); +} + + +/// Integrity digest handling errors + +const UnknownIntegrityDigestVersionError = class extends Error { + constructor(version: number) { + super(`Unknown integrity digest version: ${version}`); + this.name = 'UnknownIntegrityDigestVersionError'; + } +}; + + +// Integrity digest storage and retrieval functions + +const INTEGRITY_DIGEST_SENTINEL = 'AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A'; + +function pathToIntegrityDigestFile(appPath: string) { + if (appPath.endsWith('.app')) { + return path.resolve( + appPath, + 'Contents', + 'Frameworks', + 'Electron Framework.framework', + 'Electron Framework', + ); + } + throw new Error('App path must be an .app bundle'); +} + +function forEachSentinelInApp( + appPath: string, + callback: (sentinelIndex: number, integrityFile: Buffer) => void, + writeBack: boolean = false, +) { + const integrityFilePath = pathToIntegrityDigestFile(appPath); + const integrityFile = fs.readFileSync(integrityFilePath); + let searchCursor = 0; + const sentinelAsBuffer = Buffer.from(INTEGRITY_DIGEST_SENTINEL); + do { + const sentinelIndex = integrityFile.indexOf(sentinelAsBuffer, searchCursor); + if (sentinelIndex === -1) break; + callback(sentinelIndex, integrityFile); + searchCursor = sentinelIndex + sentinelAsBuffer.length; + } while (true); + if (writeBack) { + fs.writeFileSync(integrityFilePath, integrityFile); + } +} + +export function doDigestsMatch( + digestA: AnyIntegrityDigest, + digestB: AnyIntegrityDigest, +): boolean { + if (digestA.used !== digestB.used) return false; + if (digestA.used && digestB.used) { + if (digestA.version !== digestB.version) return false; + switch (digestA.version) { + case 1: + return digestA.sha256Digest.equals(digestB.sha256Digest); + default: + throw new UnknownIntegrityDigestVersionError(digestA.version); + } + } else return true; +} + +function sentinelIndexToDigest( + integrityFile: Buffer, + sentinelIndex: number, +): T { + const used = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length) === 1; + if (!used) { + return { used: false } as T; + } else { + const version = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1); + switch (version) { + case 1: { + const sha256Digest = integrityFile.subarray( + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2 + 32, // SHA256 digest size + ); + return { + used: true, + version: 1, + sha256Digest, + } as T; + } + default: + throw new UnknownIntegrityDigestVersionError(version); + } + } +} + +export async function getStoredIntegrityDigestForApp( + appPath: string, +): Promise { + let lastDigestFound: T | null = null; + forEachSentinelInApp(appPath, (sentinelIndex, integrityFile) => { + const currentDigest = sentinelIndexToDigest( + integrityFile, + sentinelIndex, + ); + if (lastDigestFound === null) { + lastDigestFound = currentDigest; + } else if (!doDigestsMatch(currentDigest, lastDigestFound)) { + throw new Error('Multiple differing integrity digests found in the binary'); + } + lastDigestFound = currentDigest; + }); + if (lastDigestFound === null) { + throw new Error('No integrity digest found in the binary'); + } + return lastDigestFound; +} + +export async function setStoredIntegrityDigestForApp( + appPath: string, + digest: T, +): Promise { + if (digest.used === true && digest.version !== 1) { + throw new UnknownIntegrityDigestVersionError(digest.version); + } + forEachSentinelInApp(appPath, (sentinelIndex, integrityFile) => { + integrityFile.writeUInt8(digest.used ? 1 : 0, sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length); + const oldVersion = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1); + switch (oldVersion) { + case 1: + integrityFile.fill( + 0, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2 + 32, // SHA256 digest size + ); + break; + } + if (digest.used) { + integrityFile.writeUInt8( + digest.version, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1, + ); + switch (digest.version) { + case 1: { + const v1Digest = digest as IntegrityDigestV1 & { used: true }; + v1Digest.sha256Digest.copy( + integrityFile, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, + ); + break; + } + default: + throw new UnknownIntegrityDigestVersionError(digest.version); + } + } + }, true); +} + + +// High-level integrity digest management functions + +export function printDigest(digest: AnyIntegrityDigest, prefix: string = '') { + const digestLogger = prefix ? (s: string, ...args: any[]) => console.log(prefix + s, ...args) : console.log; + if (!digest.used) { + digestLogger('Integrity digest is OFF'); + return; + } + digestLogger('Integrity digest is ON (version: %d)', digest.version); + switch (digest.version) { + case 1: + digestLogger('\tDigest (SHA256): %s', digest.sha256Digest.toString('hex')); + break; + default: + digestLogger('\tUnknown metadata for digest version: %d', digest.version); + } +} + +export async function enableIntegrityDigestForApp( + appPath: string, +): Promise { + try { + console.log('Calculating integrity digest...'); + const digest = calculateIntegrityDigestV1ForApp(appPath); + console.log('Turning integrity digest ON...'); + await setStoredIntegrityDigestForApp(appPath, digest); + console.log('Integrity digest turned ON'); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to turn ON integrity digest: %s', errorMessage); + } +} + +export async function disableIntegrityDigestForApp( + appPath: string, +): Promise { + try { + console.log('Turning integrity digest OFF...'); + await setStoredIntegrityDigestForApp(appPath, { used: false }); + console.log('Integrity digest turned OFF'); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to turn OFF integrity digest: %s', errorMessage); + } +} + +export async function printStoredIntegrityDigestForApp( + appPath: string, +): Promise { + try { + const storedDigest = await getStoredIntegrityDigestForApp(appPath); + printDigest(storedDigest); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to read integrity digest: %s', errorMessage); + } +} + +export async function verifyIntegrityDigestForApp( + appPath: string, +): Promise { + try { + const storedDigest = await getStoredIntegrityDigestForApp(appPath); + if (!storedDigest.used) { + console.log('Integrity digest is off, verification SKIPPED'); + return; + } + const calculatedDigest = calculateIntegrityDigestV1ForApp(appPath); + if (doDigestsMatch(storedDigest, calculatedDigest)) { + console.log('Integrity digest verification PASSED'); + } else { + console.log('Integrity digest verification FAILED'); + console.log('Expected digest:'); + printDigest(calculatedDigest, '\t'); + console.log('Actual digest:'); + printDigest(storedDigest, '\t'); + } + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to verify integrity digest: %s', errorMessage); + } +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 7e76957..cbd9feb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,10 +11,12 @@ __metadata: dependencies: "@tsconfig/node22": "npm:^22.0.1" "@types/node": "npm:^22.12.0" + "@types/plist": "npm:^3" commander: "npm:^13.1.0" electron: "npm:^35.7.5" glob: "npm:^11.0.1" minimatch: "npm:^10.0.1" + plist: "npm:^3.1.0" prettier: "npm:^3.3.3" typedoc: "npm:~0.25.13" typescript: "npm:^5.5.4" @@ -531,6 +533,16 @@ __metadata: languageName: node linkType: hard +"@types/plist@npm:^3": + version: 3.0.5 + resolution: "@types/plist@npm:3.0.5" + dependencies: + "@types/node": "npm:*" + xmlbuilder: "npm:>=11.0.1" + checksum: 10c0/2a929f4482e3bea8c3288a46ae589a2ae2d01df5b7841ead7032d7baa79d79af6c875a5798c90705eea9306c2fb1544d7ed12ab3c905c5626d5dd5dc9f464b94 + languageName: node + linkType: hard + "@types/responselike@npm:^1.0.0": version: 1.0.0 resolution: "@types/responselike@npm:1.0.0" @@ -632,6 +644,13 @@ __metadata: languageName: node linkType: hard +"@xmldom/xmldom@npm:^0.8.8": + version: 0.8.11 + resolution: "@xmldom/xmldom@npm:0.8.11" + checksum: 10c0/e768623de72c95d3dae6b5da8e33dda0d81665047811b5498d23a328d45b13feb5536fe921d0308b96a4a8dd8addf80b1f6ef466508051c0b581e63e0dc74ed5 + languageName: node + linkType: hard + "abbrev@npm:^3.0.0": version: 3.0.1 resolution: "abbrev@npm:3.0.1" @@ -697,6 +716,13 @@ __metadata: languageName: node linkType: hard +"base64-js@npm:^1.5.1": + version: 1.5.1 + resolution: "base64-js@npm:1.5.1" + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + languageName: node + linkType: hard + "boolean@npm:^3.0.1": version: 3.2.0 resolution: "boolean@npm:3.2.0" @@ -1907,6 +1933,17 @@ __metadata: languageName: node linkType: hard +"plist@npm:^3.1.0": + version: 3.1.0 + resolution: "plist@npm:3.1.0" + dependencies: + "@xmldom/xmldom": "npm:^0.8.8" + base64-js: "npm:^1.5.1" + xmlbuilder: "npm:^15.1.1" + checksum: 10c0/db19ba50faafc4103df8e79bcd6b08004a56db2a9dd30b3e5c8b0ef30398ef44344a674e594d012c8fc39e539a2b72cb58c60a76b4b4401cbbc7c8f6b028d93d + languageName: node + linkType: hard + "postcss@npm:^8.5.6": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -2665,6 +2702,13 @@ __metadata: languageName: node linkType: hard +"xmlbuilder@npm:>=11.0.1, xmlbuilder@npm:^15.1.1": + version: 15.1.1 + resolution: "xmlbuilder@npm:15.1.1" + checksum: 10c0/665266a8916498ff8d82b3d46d3993913477a254b98149ff7cff060d9b7cc0db7cf5a3dae99aed92355254a808c0e2e3ec74ad1b04aa1061bdb8dfbea26c18b8 + languageName: node + linkType: hard + "xvfb-maybe@npm:^0.2.1": version: 0.2.1 resolution: "xvfb-maybe@npm:0.2.1" From 3f027aa0ea9a7666d54f93a3553a2efe026854a2 Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Thu, 30 Oct 2025 12:54:25 -0400 Subject: [PATCH 2/3] fix: don't export functions that don't need to be exported --- src/integrity-digest.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/integrity-digest.ts b/src/integrity-digest.ts index 6e83840..b6d37d4 100644 --- a/src/integrity-digest.ts +++ b/src/integrity-digest.ts @@ -41,7 +41,7 @@ function calculateIntegrityDigestV1( } } -export function calculateIntegrityDigestV1ForApp( +function calculateIntegrityDigestV1ForApp( appPath: string, ): IntegrityDigestV1 { const plistPath = path.join(appPath, 'Contents', 'Info.plist'); @@ -99,7 +99,7 @@ function forEachSentinelInApp( } } -export function doDigestsMatch( +function doDigestsMatch( digestA: AnyIntegrityDigest, digestB: AnyIntegrityDigest, ): boolean { @@ -142,7 +142,7 @@ function sentinelIndexToDigest( } } -export async function getStoredIntegrityDigestForApp( +async function getStoredIntegrityDigestForApp( appPath: string, ): Promise { let lastDigestFound: T | null = null; @@ -164,7 +164,7 @@ export async function getStoredIntegrityDigestForApp( +async function setStoredIntegrityDigestForApp( appPath: string, digest: T, ): Promise { @@ -207,7 +207,7 @@ export async function setStoredIntegrityDigestForApp console.log(prefix + s, ...args) : console.log; if (!digest.used) { digestLogger('Integrity digest is OFF'); From 611dd4d6eacff113a5345d18a9d093ce81cc297d Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Fri, 14 Nov 2025 11:49:03 -0500 Subject: [PATCH 3/3] fix: run prettier --- src/integrity-digest.ts | 425 +++++++++++++++++++--------------------- 1 file changed, 205 insertions(+), 220 deletions(-) diff --git a/src/integrity-digest.ts b/src/integrity-digest.ts index b6d37d4..83a169d 100644 --- a/src/integrity-digest.ts +++ b/src/integrity-digest.ts @@ -5,285 +5,270 @@ import plist from 'plist'; import { wrappedFs as fs } from './wrapped-fs.js'; import { FileRecord } from './disk.js'; - // Integrity digest type definitions type IntegrityDigest = - | { used: false } - | ({ used: true; version: Version } & AdditionalParams); + | { used: false } + | ({ used: true; version: Version } & AdditionalParams); type IntegrityDigestV1 = IntegrityDigest<1, { sha256Digest: Buffer }>; type AnyIntegrityDigest = IntegrityDigestV1; // Extend this union type as new versions are added - // Integrity digest calculation functions -type AsarIntegrity = Record< - string, - Pick ->; +type AsarIntegrity = Record>; -function calculateIntegrityDigestV1( - asarIntegrity: AsarIntegrity, -): IntegrityDigestV1 { - const integrityHash = crypto.createHash('SHA256'); - for (const key of Object.keys(asarIntegrity).sort()) { - const { algorithm, hash } = asarIntegrity[key]; - integrityHash.update(key); - integrityHash.update(algorithm); - integrityHash.update(hash); - } - return { - used: true, - version: 1, - sha256Digest: integrityHash.digest(), - } +function calculateIntegrityDigestV1(asarIntegrity: AsarIntegrity): IntegrityDigestV1 { + const integrityHash = crypto.createHash('SHA256'); + for (const key of Object.keys(asarIntegrity).sort()) { + const { algorithm, hash } = asarIntegrity[key]; + integrityHash.update(key); + integrityHash.update(algorithm); + integrityHash.update(hash); + } + return { + used: true, + version: 1, + sha256Digest: integrityHash.digest(), + }; } -function calculateIntegrityDigestV1ForApp( - appPath: string, -): IntegrityDigestV1 { - const plistPath = path.join(appPath, 'Contents', 'Info.plist'); - const plistBuffer = fs.readFileSync(plistPath); - const plistData = plist.parse(plistBuffer.toString()) as Record; - const asarIntegrity = plistData['ElectronAsarIntegrity'] as AsarIntegrity; - return calculateIntegrityDigestV1(asarIntegrity); +function calculateIntegrityDigestV1ForApp(appPath: string): IntegrityDigestV1 { + const plistPath = path.join(appPath, 'Contents', 'Info.plist'); + const plistBuffer = fs.readFileSync(plistPath); + const plistData = plist.parse(plistBuffer.toString()) as Record; + const asarIntegrity = plistData['ElectronAsarIntegrity'] as AsarIntegrity; + return calculateIntegrityDigestV1(asarIntegrity); } - /// Integrity digest handling errors const UnknownIntegrityDigestVersionError = class extends Error { - constructor(version: number) { - super(`Unknown integrity digest version: ${version}`); - this.name = 'UnknownIntegrityDigestVersionError'; - } + constructor(version: number) { + super(`Unknown integrity digest version: ${version}`); + this.name = 'UnknownIntegrityDigestVersionError'; + } }; - // Integrity digest storage and retrieval functions const INTEGRITY_DIGEST_SENTINEL = 'AGbevlPCksUGKNL8TSn7wGmJEuJsXb2A'; function pathToIntegrityDigestFile(appPath: string) { - if (appPath.endsWith('.app')) { - return path.resolve( - appPath, - 'Contents', - 'Frameworks', - 'Electron Framework.framework', - 'Electron Framework', - ); - } - throw new Error('App path must be an .app bundle'); + if (appPath.endsWith('.app')) { + return path.resolve( + appPath, + 'Contents', + 'Frameworks', + 'Electron Framework.framework', + 'Electron Framework', + ); + } + throw new Error('App path must be an .app bundle'); } function forEachSentinelInApp( - appPath: string, - callback: (sentinelIndex: number, integrityFile: Buffer) => void, - writeBack: boolean = false, + appPath: string, + callback: (sentinelIndex: number, integrityFile: Buffer) => void, + writeBack: boolean = false, ) { - const integrityFilePath = pathToIntegrityDigestFile(appPath); - const integrityFile = fs.readFileSync(integrityFilePath); - let searchCursor = 0; - const sentinelAsBuffer = Buffer.from(INTEGRITY_DIGEST_SENTINEL); - do { - const sentinelIndex = integrityFile.indexOf(sentinelAsBuffer, searchCursor); - if (sentinelIndex === -1) break; - callback(sentinelIndex, integrityFile); - searchCursor = sentinelIndex + sentinelAsBuffer.length; - } while (true); - if (writeBack) { - fs.writeFileSync(integrityFilePath, integrityFile); - } + const integrityFilePath = pathToIntegrityDigestFile(appPath); + const integrityFile = fs.readFileSync(integrityFilePath); + let searchCursor = 0; + const sentinelAsBuffer = Buffer.from(INTEGRITY_DIGEST_SENTINEL); + do { + const sentinelIndex = integrityFile.indexOf(sentinelAsBuffer, searchCursor); + if (sentinelIndex === -1) break; + callback(sentinelIndex, integrityFile); + searchCursor = sentinelIndex + sentinelAsBuffer.length; + } while (true); + if (writeBack) { + fs.writeFileSync(integrityFilePath, integrityFile); + } } -function doDigestsMatch( - digestA: AnyIntegrityDigest, - digestB: AnyIntegrityDigest, -): boolean { - if (digestA.used !== digestB.used) return false; - if (digestA.used && digestB.used) { - if (digestA.version !== digestB.version) return false; - switch (digestA.version) { - case 1: - return digestA.sha256Digest.equals(digestB.sha256Digest); - default: - throw new UnknownIntegrityDigestVersionError(digestA.version); - } - } else return true; +function doDigestsMatch(digestA: AnyIntegrityDigest, digestB: AnyIntegrityDigest): boolean { + if (digestA.used !== digestB.used) return false; + if (digestA.used && digestB.used) { + if (digestA.version !== digestB.version) return false; + switch (digestA.version) { + case 1: + return digestA.sha256Digest.equals(digestB.sha256Digest); + default: + throw new UnknownIntegrityDigestVersionError(digestA.version); + } + } else return true; } function sentinelIndexToDigest( - integrityFile: Buffer, - sentinelIndex: number, + integrityFile: Buffer, + sentinelIndex: number, ): T { - const used = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length) === 1; - if (!used) { - return { used: false } as T; - } else { - const version = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1); - switch (version) { - case 1: { - const sha256Digest = integrityFile.subarray( - sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, - sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2 + 32, // SHA256 digest size - ); - return { - used: true, - version: 1, - sha256Digest, - } as T; - } - default: - throw new UnknownIntegrityDigestVersionError(version); - } + const used = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length) === 1; + if (!used) { + return { used: false } as T; + } else { + const version = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1); + switch (version) { + case 1: { + const sha256Digest = integrityFile.subarray( + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2 + 32, // SHA256 digest size + ); + return { + used: true, + version: 1, + sha256Digest, + } as T; + } + default: + throw new UnknownIntegrityDigestVersionError(version); } + } } async function getStoredIntegrityDigestForApp( - appPath: string, + appPath: string, ): Promise { - let lastDigestFound: T | null = null; - forEachSentinelInApp(appPath, (sentinelIndex, integrityFile) => { - const currentDigest = sentinelIndexToDigest( - integrityFile, - sentinelIndex, - ); - if (lastDigestFound === null) { - lastDigestFound = currentDigest; - } else if (!doDigestsMatch(currentDigest, lastDigestFound)) { - throw new Error('Multiple differing integrity digests found in the binary'); - } - lastDigestFound = currentDigest; - }); + let lastDigestFound: T | null = null; + forEachSentinelInApp(appPath, (sentinelIndex, integrityFile) => { + const currentDigest = sentinelIndexToDigest(integrityFile, sentinelIndex); if (lastDigestFound === null) { - throw new Error('No integrity digest found in the binary'); + lastDigestFound = currentDigest; + } else if (!doDigestsMatch(currentDigest, lastDigestFound)) { + throw new Error('Multiple differing integrity digests found in the binary'); } - return lastDigestFound; + lastDigestFound = currentDigest; + }); + if (lastDigestFound === null) { + throw new Error('No integrity digest found in the binary'); + } + return lastDigestFound; } async function setStoredIntegrityDigestForApp( - appPath: string, - digest: T, + appPath: string, + digest: T, ): Promise { - if (digest.used === true && digest.version !== 1) { - throw new UnknownIntegrityDigestVersionError(digest.version); - } - forEachSentinelInApp(appPath, (sentinelIndex, integrityFile) => { - integrityFile.writeUInt8(digest.used ? 1 : 0, sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length); - const oldVersion = integrityFile.readUInt8(sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1); - switch (oldVersion) { - case 1: - integrityFile.fill( - 0, - sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, - sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2 + 32, // SHA256 digest size - ); - break; - } - if (digest.used) { - integrityFile.writeUInt8( - digest.version, - sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1, + if (digest.used === true && digest.version !== 1) { + throw new UnknownIntegrityDigestVersionError(digest.version); + } + forEachSentinelInApp( + appPath, + (sentinelIndex, integrityFile) => { + integrityFile.writeUInt8( + digest.used ? 1 : 0, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length, + ); + const oldVersion = integrityFile.readUInt8( + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1, + ); + switch (oldVersion) { + case 1: + integrityFile.fill( + 0, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2 + 32, // SHA256 digest size + ); + break; + } + if (digest.used) { + integrityFile.writeUInt8( + digest.version, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 1, + ); + switch (digest.version) { + case 1: { + const v1Digest = digest as IntegrityDigestV1 & { used: true }; + v1Digest.sha256Digest.copy( + integrityFile, + sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, ); - switch (digest.version) { - case 1: { - const v1Digest = digest as IntegrityDigestV1 & { used: true }; - v1Digest.sha256Digest.copy( - integrityFile, - sentinelIndex + INTEGRITY_DIGEST_SENTINEL.length + 2, - ); - break; - } - default: - throw new UnknownIntegrityDigestVersionError(digest.version); - } + break; + } + default: + throw new UnknownIntegrityDigestVersionError(digest.version); } - }, true); + } + }, + true, + ); } - // High-level integrity digest management functions function printDigest(digest: AnyIntegrityDigest, prefix: string = '') { - const digestLogger = prefix ? (s: string, ...args: any[]) => console.log(prefix + s, ...args) : console.log; - if (!digest.used) { - digestLogger('Integrity digest is OFF'); - return; - } - digestLogger('Integrity digest is ON (version: %d)', digest.version); - switch (digest.version) { - case 1: - digestLogger('\tDigest (SHA256): %s', digest.sha256Digest.toString('hex')); - break; - default: - digestLogger('\tUnknown metadata for digest version: %d', digest.version); - } + const digestLogger = prefix + ? (s: string, ...args: any[]) => console.log(prefix + s, ...args) + : console.log; + if (!digest.used) { + digestLogger('Integrity digest is OFF'); + return; + } + digestLogger('Integrity digest is ON (version: %d)', digest.version); + switch (digest.version) { + case 1: + digestLogger('\tDigest (SHA256): %s', digest.sha256Digest.toString('hex')); + break; + default: + digestLogger('\tUnknown metadata for digest version: %d', digest.version); + } } -export async function enableIntegrityDigestForApp( - appPath: string, -): Promise { - try { - console.log('Calculating integrity digest...'); - const digest = calculateIntegrityDigestV1ForApp(appPath); - console.log('Turning integrity digest ON...'); - await setStoredIntegrityDigestForApp(appPath, digest); - console.log('Integrity digest turned ON'); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - console.log('Failed to turn ON integrity digest: %s', errorMessage); - } +export async function enableIntegrityDigestForApp(appPath: string): Promise { + try { + console.log('Calculating integrity digest...'); + const digest = calculateIntegrityDigestV1ForApp(appPath); + console.log('Turning integrity digest ON...'); + await setStoredIntegrityDigestForApp(appPath, digest); + console.log('Integrity digest turned ON'); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to turn ON integrity digest: %s', errorMessage); + } } -export async function disableIntegrityDigestForApp( - appPath: string, -): Promise { - try { - console.log('Turning integrity digest OFF...'); - await setStoredIntegrityDigestForApp(appPath, { used: false }); - console.log('Integrity digest turned OFF'); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - console.log('Failed to turn OFF integrity digest: %s', errorMessage); - } +export async function disableIntegrityDigestForApp(appPath: string): Promise { + try { + console.log('Turning integrity digest OFF...'); + await setStoredIntegrityDigestForApp(appPath, { used: false }); + console.log('Integrity digest turned OFF'); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to turn OFF integrity digest: %s', errorMessage); + } } -export async function printStoredIntegrityDigestForApp( - appPath: string, -): Promise { - try { - const storedDigest = await getStoredIntegrityDigestForApp(appPath); - printDigest(storedDigest); - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - console.log('Failed to read integrity digest: %s', errorMessage); - } +export async function printStoredIntegrityDigestForApp(appPath: string): Promise { + try { + const storedDigest = await getStoredIntegrityDigestForApp(appPath); + printDigest(storedDigest); + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to read integrity digest: %s', errorMessage); + } } -export async function verifyIntegrityDigestForApp( - appPath: string, -): Promise { - try { - const storedDigest = await getStoredIntegrityDigestForApp(appPath); - if (!storedDigest.used) { - console.log('Integrity digest is off, verification SKIPPED'); - return; - } - const calculatedDigest = calculateIntegrityDigestV1ForApp(appPath); - if (doDigestsMatch(storedDigest, calculatedDigest)) { - console.log('Integrity digest verification PASSED'); - } else { - console.log('Integrity digest verification FAILED'); - console.log('Expected digest:'); - printDigest(calculatedDigest, '\t'); - console.log('Actual digest:'); - printDigest(storedDigest, '\t'); - } - } catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - console.log('Failed to verify integrity digest: %s', errorMessage); +export async function verifyIntegrityDigestForApp(appPath: string): Promise { + try { + const storedDigest = await getStoredIntegrityDigestForApp(appPath); + if (!storedDigest.used) { + console.log('Integrity digest is off, verification SKIPPED'); + return; + } + const calculatedDigest = calculateIntegrityDigestV1ForApp(appPath); + if (doDigestsMatch(storedDigest, calculatedDigest)) { + console.log('Integrity digest verification PASSED'); + } else { + console.log('Integrity digest verification FAILED'); + console.log('Expected digest:'); + printDigest(calculatedDigest, '\t'); + console.log('Actual digest:'); + printDigest(storedDigest, '\t'); } -} \ No newline at end of file + } catch (e) { + const errorMessage = e instanceof Error ? e.message : String(e); + console.log('Failed to verify integrity digest: %s', errorMessage); + } +}