From 575b32dc55d775e5abcd90012624f2454f9eb5fb Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Wed, 18 Mar 2026 15:31:24 -0400 Subject: [PATCH 1/3] feat: add canonical asar integrity information calculator --- src/asar.ts | 1 + src/integrity-digest.ts | 4 ++-- src/integrity.ts | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/asar.ts b/src/asar.ts index ed5d0cd..6a41890 100644 --- a/src/asar.ts +++ b/src/asar.ts @@ -409,6 +409,7 @@ export function extractAll(archivePath: string, dest: string) { } } +export { getArchiveIntegrity } from './integrity.js'; export { uncacheAll, uncacheFilesystem as uncache, FileRecord, DirectoryRecord } from './disk.js'; export { calculateIntegrityDigestForApp, diff --git a/src/integrity-digest.ts b/src/integrity-digest.ts index 881b0ba..8a643bd 100644 --- a/src/integrity-digest.ts +++ b/src/integrity-digest.ts @@ -3,7 +3,7 @@ import crypto from 'node:crypto'; import plist from 'plist'; import { wrappedFs as fs } from './wrapped-fs.js'; -import { FileRecord } from './disk.js'; +import { ArchiveIntegrity } from './integrity.js'; // Integrity digest type definitions @@ -34,7 +34,7 @@ type AnyIntegrityDigest = DigestByVersion[keyof DigestByVersion]; // Integrity digest calculation functions -type AsarIntegrity = Record>; +type AsarIntegrity = Record; function isValidAsarIntegrity(asarIntegrity: any): asarIntegrity is AsarIntegrity { if (typeof asarIntegrity !== 'object' || asarIntegrity === null) return false; diff --git a/src/integrity.ts b/src/integrity.ts index e8c83db..a2eb1f6 100644 --- a/src/integrity.ts +++ b/src/integrity.ts @@ -1,6 +1,7 @@ import crypto from 'node:crypto'; import stream from 'node:stream'; import streamPromises from 'node:stream/promises'; +import { FileRecord, getRawHeader } from './asar.js'; const ALGORITHM = 'SHA256'; // 4MB default block size @@ -64,3 +65,13 @@ export async function getFileIntegrity( blocks: blockHashes, }; } + +export type ArchiveIntegrity = Pick; + +export function getArchiveIntegrity(archivePath: string) { + const { headerString } = getRawHeader(archivePath); + return { + algorithm: 'SHA256', + hash: crypto.createHash('SHA256').update(headerString).digest('hex'), + }; +} \ No newline at end of file From 8597ff3ee34b20254a3e94df7c211c89aa2aafee Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Wed, 18 Mar 2026 16:17:04 -0400 Subject: [PATCH 2/3] fix: add explicit return type to `getArchiveIntegrity` --- src/integrity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/integrity.ts b/src/integrity.ts index a2eb1f6..004b761 100644 --- a/src/integrity.ts +++ b/src/integrity.ts @@ -68,7 +68,7 @@ export async function getFileIntegrity( export type ArchiveIntegrity = Pick; -export function getArchiveIntegrity(archivePath: string) { +export function getArchiveIntegrity(archivePath: string): ArchiveIntegrity { const { headerString } = getRawHeader(archivePath); return { algorithm: 'SHA256', From a49361f9303f049e42e5913330efd89e8ced742f Mon Sep 17 00:00:00 2001 From: Noah Gregory Date: Wed, 18 Mar 2026 16:28:42 -0400 Subject: [PATCH 3/3] feat: add `getAsarIntegrityInfo` function --- src/asar.ts | 2 +- src/integrity.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/asar.ts b/src/asar.ts index 6a41890..2af8e57 100644 --- a/src/asar.ts +++ b/src/asar.ts @@ -409,7 +409,7 @@ export function extractAll(archivePath: string, dest: string) { } } -export { getArchiveIntegrity } from './integrity.js'; +export { getArchiveIntegrity, getAsarIntegrityInfo } from './integrity.js'; export { uncacheAll, uncacheFilesystem as uncache, FileRecord, DirectoryRecord } from './disk.js'; export { calculateIntegrityDigestForApp, diff --git a/src/integrity.ts b/src/integrity.ts index 004b761..b9e878f 100644 --- a/src/integrity.ts +++ b/src/integrity.ts @@ -74,4 +74,50 @@ export function getArchiveIntegrity(archivePath: string): ArchiveIntegrity { algorithm: 'SHA256', hash: crypto.createHash('SHA256').update(headerString).digest('hex'), }; -} \ No newline at end of file +} + +// To be inserted into Info.plist of the app. +export type AsarIntegrityInfoMacOS = Record; + +// To be added as a resource to the app. +export type AsarIntegrityInfoWindows = { resourceType: 'Integrity', resourceName: 'ElectronAsar', resourceData: Buffer }; +export type AsarIntegrityInfoWindowsFiles = {file: string, alg: string, value: string}[]; + +type ASARIntegrityPlatformInfoMap = { + macos: AsarIntegrityInfoMacOS; + windows: AsarIntegrityInfoWindows; +}; + +export function getAsarIntegrityInfo( + files: { relativePath: string; fullPath: string }[], + platform: 'macos', +): AsarIntegrityInfoMacOS; +export function getAsarIntegrityInfo( + files: { relativePath: string; fullPath: string }[], + platform: 'windows', +): AsarIntegrityInfoWindows; +export function getAsarIntegrityInfo( + files: { relativePath: string; fullPath: string }[], + platform: keyof ASARIntegrityPlatformInfoMap, +) { + switch (platform) { + case 'macos': + return Object.fromEntries( + files.map((file) => [file.relativePath, getArchiveIntegrity(file.fullPath)]), + ); + case 'windows': { + const filesJson: AsarIntegrityInfoWindowsFiles = files.map((file) => ({ + file: file.relativePath, + alg: 'SHA256', + value: getArchiveIntegrity(file.fullPath).hash, + })); + return { + resourceType: 'Integrity', + resourceName: 'ElectronAsar', + resourceData: Buffer.from(JSON.stringify(filesJson), 'utf-8'), + } as AsarIntegrityInfoWindows; + } + default: + throw new Error(`Invalid platform: ${platform}`); + } +}