Initial
This commit is contained in:
104
server/services/intunewinParser.js
Normal file
104
server/services/intunewinParser.js
Normal file
@@ -0,0 +1,104 @@
|
||||
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]*?)</${tag}>`, '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(/<EncryptionInfo>([\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)
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user