diff --git a/src/asar.ts b/src/asar.ts index ed5d0cd..2af8e57 100644 --- a/src/asar.ts +++ b/src/asar.ts @@ -409,6 +409,7 @@ export function extractAll(archivePath: string, dest: string) { } } +export { getArchiveIntegrity, getAsarIntegrityInfo } 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..b9e878f 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,59 @@ export async function getFileIntegrity( blocks: blockHashes, }; } + +export type ArchiveIntegrity = Pick; + +export function getArchiveIntegrity(archivePath: string): ArchiveIntegrity { + const { headerString } = getRawHeader(archivePath); + return { + algorithm: 'SHA256', + hash: crypto.createHash('SHA256').update(headerString).digest('hex'), + }; +} + +// 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}`); + } +}