import zlib from 'node:zlib'; // Minimal reader for the `.intunewin` package produced by the Microsoft Win32 // Content Prep Tool. The package is a ZIP containing: // IntuneWinPackage/Metadata/Detection.xml (encryption info + sizes) // IntuneWinPackage/Contents/IntunePackage.intunewin (AES-encrypted payload) // // We avoid a third-party zip dependency by parsing the central directory and // inflating entries with the built-in zlib. Only stored (0) and deflate (8) // compression methods are supported, which is what the tool emits. const EOCD_SIG = 0x06054b50; const CEN_SIG = 0x02014b50; const LOC_SIG = 0x04034b50; function findEndOfCentralDirectory(buffer) { // EOCD is at the end; scan backwards past any (absent) comment. for (let i = buffer.length - 22; i >= 0; i--) { if (buffer.readUInt32LE(i) === EOCD_SIG) return i; } throw new Error('Not a valid .intunewin (ZIP end-of-central-directory not found)'); } function readCentralDirectory(buffer) { const eocd = findEndOfCentralDirectory(buffer); const count = buffer.readUInt16LE(eocd + 10); let offset = buffer.readUInt32LE(eocd + 16); const entries = []; for (let i = 0; i < count; i++) { if (buffer.readUInt32LE(offset) !== CEN_SIG) break; const method = buffer.readUInt16LE(offset + 10); const compressedSize = buffer.readUInt32LE(offset + 20); const nameLen = buffer.readUInt16LE(offset + 28); const extraLen = buffer.readUInt16LE(offset + 30); const commentLen = buffer.readUInt16LE(offset + 32); const localHeaderOffset = buffer.readUInt32LE(offset + 42); const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLen); entries.push({ name, method, compressedSize, localHeaderOffset }); offset += 46 + nameLen + extraLen + commentLen; } return entries; } function readEntryData(buffer, entry) { const o = entry.localHeaderOffset; if (buffer.readUInt32LE(o) !== LOC_SIG) throw new Error(`Bad local header for ${entry.name}`); const nameLen = buffer.readUInt16LE(o + 26); const extraLen = buffer.readUInt16LE(o + 28); const dataStart = o + 30 + nameLen + extraLen; const raw = buffer.subarray(dataStart, dataStart + entry.compressedSize); if (entry.method === 0) return Buffer.from(raw); if (entry.method === 8) return zlib.inflateRawSync(raw); throw new Error(`Unsupported ZIP compression method ${entry.method} for ${entry.name}`); } function findEntry(entries, suffix) { // Match by path suffix so we tolerate the IntuneWinPackage/ prefix casing. const lower = suffix.toLowerCase(); return entries.find((e) => e.name.toLowerCase().replace(/\\/g, '/').endsWith(lower)); } function xmlValue(xml, tag) { const match = xml.match(new RegExp(`<${tag}>([\\s\\S]*?)`, 'i')); return match ? match[1].trim() : ''; } // Graph's commit step expects camelCase fileEncryptionInfo; map directly from // the Detection.xml EncryptionInfo element. function parseEncryptionInfo(xml) { const block = xml.match(/([\s\S]*?)<\/EncryptionInfo>/i); if (!block) throw new Error('Detection.xml is missing EncryptionInfo'); const info = block[1]; return { encryptionKey: xmlValue(info, 'EncryptionKey'), macKey: xmlValue(info, 'MacKey'), initializationVector: xmlValue(info, 'InitializationVector'), mac: xmlValue(info, 'Mac'), profileIdentifier: xmlValue(info, 'ProfileIdentifier') || 'ProfileVersion1', fileDigest: xmlValue(info, 'FileDigest'), fileDigestAlgorithm: xmlValue(info, 'FileDigestAlgorithm') || 'SHA256' }; } export function parseIntunewin(buffer) { if (!Buffer.isBuffer(buffer)) buffer = Buffer.from(buffer); const entries = readCentralDirectory(buffer); const detectionEntry = findEntry(entries, 'metadata/detection.xml'); if (!detectionEntry) throw new Error('Detection.xml not found in .intunewin package'); const contentEntry = findEntry(entries, 'contents/intunepackage.intunewin'); if (!contentEntry) throw new Error('Encrypted content (IntunePackage.intunewin) not found in package'); const xml = readEntryData(buffer, detectionEntry).toString('utf8'); const encryptedContent = readEntryData(buffer, contentEntry); const unencryptedContentSize = Number(xmlValue(xml, 'UnencryptedContentSize')) || 0; return { setupFile: xmlValue(xml, 'SetupFile') || xmlValue(xml, 'FileName') || '', name: xmlValue(xml, 'Name') || '', unencryptedContentSize, encryptedContent, encryptedContentSize: encryptedContent.length, fileEncryptionInfo: parseEncryptionInfo(xml) }; }