EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
authenticode.js
Go to the documentation of this file.
1/**
2* @description Authenticode parsing
3* @author Ylian Saint-Hilaire & Bryan Roe
4* @copyright Intel Corporation 2018-2022
5* @license Apache-2.0
6* @version v0.0.1
7*/
8
9/*jslint node: true */
10/*jshint node: true */
11/*jshint strict:false */
12/*jshint -W097 */
13/*jshint esversion: 6 */
14"use strict";
15
16const fs = require('fs');
17const crypto = require('crypto');
18const forge = require('node-forge');
19const pki = forge.pki;
20const p7 = require('./pkcs7-modified');
21
22// Generate a test self-signed certificate with code signing extension
23function createSelfSignedCert(args) {
24 var keys = pki.rsa.generateKeyPair(2048);
25 var cert = pki.createCertificate();
26 cert.publicKey = keys.publicKey;
27 cert.serialNumber = (typeof args.serial == 'string')?args.serial:'012345'; // Serial number must always have a single leading '0', otherwise toPEM/fromPEM will not work right.
28 cert.validity.notBefore = new Date();
29 cert.validity.notAfter = new Date();
30 cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
31 var attrs = [];
32 if (typeof args.cn == 'string') { attrs.push({ name: 'commonName', value: args.cn }); }
33 if (typeof args.country == 'string') { attrs.push({ name: 'countryName', value: args.country }); }
34 if (typeof args.state == 'string') { attrs.push({ name: 'ST', value: args.state }); }
35 if (typeof args.locality == 'string') { attrs.push({ name: 'localityName', value: args.locality }); }
36 if (typeof args.org == 'string') { attrs.push({ name: 'organizationName', value: args.org }); }
37 if (typeof args.orgunit == 'string') { attrs.push({ name: 'OU', value: args.orgunit }); }
38 cert.setSubject(attrs);
39 cert.setIssuer(attrs);
40 cert.setExtensions([{ name: 'basicConstraints', cA: false }, { name: 'keyUsage', keyCertSign: false, digitalSignature: true, nonRepudiation: false, keyEncipherment: false, dataEncipherment: false }, { name: 'extKeyUsage', codeSigning: true }, { name: "subjectKeyIdentifier" }]);
41 cert.sign(keys.privateKey, forge.md.sha384.create());
42 return { cert: cert, key: keys.privateKey, extraCerts: [] };
43}
44
45// Create the output filename if not already specified
46function createOutFile(args, filename) {
47 if (typeof args.out == 'string') return;
48 var outputFileName = filename.split('.');
49 outputFileName[outputFileName.length - 2] += '-out';
50 args.out = outputFileName.join('.');
51}
52
53// Hash an object
54function hashObject(obj) {
55 if (obj == null) { return null; }
56 const hash = crypto.createHash('sha384');
57 if (Buffer.isBuffer(obj)) { hash.update(obj); } else { hash.update(JSON.stringify(obj)); }
58 return hash.digest().toString('hex');
59}
60
61// Load a .bmp file.
62function loadBitmap(bitmapFile) {
63 var bitmapData = null;
64 try { bitmapData = fs.readFileSync(bitmapFile); } catch (ex) { }
65 if ((bitmapData == null) || (bitmapData.length < 14) || (bitmapData[0] != 0x42) || (bitmapData[1] != 0x4D)) return null;
66 return bitmapData.slice(14);
67}
68
69// Load a .ico file. This will load all icons in the file into a icon group object
70function loadIcon(iconFile) {
71 var iconData = null;
72 try { iconData = fs.readFileSync(iconFile); } catch (ex) { }
73 if ((iconData == null) || (iconData.length < 6) || (iconData[0] != 0) || (iconData[1] != 0)) return null;
74 const r = { resType: iconData.readUInt16LE(2), resCount: iconData.readUInt16LE(4), icons: {} };
75 if (r.resType != 1) return null;
76 var ptr = 6;
77 for (var i = 1; i <= r.resCount; i++) {
78 var icon = {};
79 icon.width = iconData[ptr + 0];
80 icon.height = iconData[ptr + 1];
81 icon.colorCount = iconData[ptr + 2];
82 icon.planes = iconData.readUInt16LE(ptr + 4);
83 icon.bitCount = iconData.readUInt16LE(ptr + 6);
84 icon.bytesInRes = iconData.readUInt32LE(ptr + 8);
85 icon.iconCursorId = i;
86 const offset = iconData.readUInt32LE(ptr + 12);
87 icon.icon = iconData.slice(offset, offset + icon.bytesInRes);
88 r.icons[i] = icon;
89 ptr += 16;
90 }
91 return r;
92}
93
94// Load certificates and private key from PEM files
95function loadCertificates(pemFileNames) {
96 var certs = [], keys = [];
97 if (pemFileNames == null) return;
98 if (typeof pemFileNames == 'string') { pemFileNames = [pemFileNames]; }
99 for (var i in pemFileNames) {
100 try {
101 // Read certificate
102 var pem = fs.readFileSync(pemFileNames[i]).toString();
103 var pemCerts = pem.split('-----BEGIN CERTIFICATE-----');
104 for (var j in pemCerts) {
105 var k = pemCerts[j].indexOf('-----END CERTIFICATE-----');
106 if (k >= 0) { certs.push(pki.certificateFromPem('-----BEGIN CERTIFICATE-----' + pemCerts[j].substring(0, k) + '-----END CERTIFICATE-----')); }
107 }
108 var PemKeys = pem.split('-----BEGIN RSA PRIVATE KEY-----');
109 for (var j in PemKeys) {
110 var k = PemKeys[j].indexOf('-----END RSA PRIVATE KEY-----');
111 if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN RSA PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END RSA PRIVATE KEY-----')); }
112 }
113 PemKeys = pem.split('-----BEGIN PRIVATE KEY-----');
114 for (var j in PemKeys) {
115 var k = PemKeys[j].indexOf('-----END PRIVATE KEY-----');
116 if (k >= 0) { keys.push(pki.privateKeyFromPem('-----BEGIN PRIVATE KEY-----' + PemKeys[j].substring(0, k) + '-----END PRIVATE KEY-----')); }
117 }
118 } catch (ex) { }
119 }
120 if ((certs.length == 0) || (keys.length != 1)) return; // No certificates or private keys
121 var r = { cert: certs[0], key: keys[0], extraCerts: [] }
122 if (certs.length > 1) { for (var i = 1; i < certs.length; i++) { r.extraCerts.push(certs[i]); } }
123 return r;
124}
125
126function createAuthenticodeHandler(path) {
127 const obj = {};
128 obj.header = { path: path }
129
130 // Read a file slice
131 function readFileSlice(start, length) {
132 var buffer = Buffer.alloc(length);
133 var len = fs.readSync(obj.fd, buffer, 0, buffer.length, start);
134 if (len < buffer.length) { buffer = buffer.slice(0, len); }
135 return buffer;
136 }
137
138 // Close the file
139 obj.close = function () {
140 if (obj.fd == null) return;
141 fs.closeSync(obj.fd);
142 delete obj.fd;
143 }
144
145 // Private OIDS
146 obj.Oids = {
147 SPC_INDIRECT_DATA_OBJID: '1.3.6.1.4.1.311.2.1.4',
148 SPC_STATEMENT_TYPE_OBJID: '1.3.6.1.4.1.311.2.1.11',
149 SPC_SP_OPUS_INFO_OBJID: '1.3.6.1.4.1.311.2.1.12',
150 SPC_INDIVIDUAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.21',
151 SPC_COMMERCIAL_SP_KEY_PURPOSE_OBJID: '1.3.6.1.4.1.311.2.1.22',
152 SPC_MS_JAVA_SOMETHING: '1.3.6.1.4.1.311.15.1',
153 SPC_PE_IMAGE_DATA_OBJID: '1.3.6.1.4.1.311.2.1.15',
154 SPC_CAB_DATA_OBJID: '1.3.6.1.4.1.311.2.1.25',
155 SPC_TIME_STAMP_REQUEST_OBJID: '1.3.6.1.4.1.311.3.2.1',
156 SPC_SIPINFO_OBJID: '1.3.6.1.4.1.311.2.1.30',
157 SPC_PE_IMAGE_PAGE_HASHES_V1: '1.3.6.1.4.1.311.2.3.1',
158 SPC_PE_IMAGE_PAGE_HASHES_V2: '1.3.6.1.4.1.311.2.3.2',
159 SPC_NESTED_SIGNATURE_OBJID: '1.3.6.1.4.1.311.2.4.1',
160 SPC_RFC3161_OBJID: '1.3.6.1.4.1.311.3.3.1'
161 }
162
163 // Open the file and read header information
164 function openFile() {
165 if (obj.fd != null) return true;
166
167 // Open the file descriptor
168 obj.path = path;
169 try { obj.fd = fs.openSync(path, 'r'); } catch (ex) { return false; } // Unable to open file
170 obj.stats = fs.fstatSync(obj.fd);
171 obj.filesize = obj.stats.size;
172 if (obj.filesize < 64) { obj.close(); return false; } // File too short.
173
174 // Read the DOS header (64 bytes)
175 var buf = readFileSlice(60, 4);
176 obj.header.peHeaderLocation = buf.readUInt32LE(0); // The DOS header is 64 bytes long, the last 4 bytes are a pointer to the PE header.
177 obj.header.peOptionalHeaderLocation = obj.header.peHeaderLocation + 24; // The PE optional header is located just after the PE header which is 24 bytes long.
178
179 // Check file size and signature
180 if (obj.filesize < (160 + obj.header.peHeaderLocation)) { obj.close(); return false; } // Invalid SizeOfHeaders.
181 if (readFileSlice(obj.header.peHeaderLocation, 4).toString('hex') != '50450000') { obj.close(); return false; } // Invalid PE header, must start with "PE" (HEX: 50 45 00 00).
182
183 // Read the COFF header
184 // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image
185 var coffHeader = readFileSlice(obj.header.peHeaderLocation + 4, 20)
186 obj.header.coff = {};
187 obj.header.coff.machine = coffHeader.readUInt16LE(0);
188 obj.header.coff.numberOfSections = coffHeader.readUInt16LE(2);
189 obj.header.coff.timeDateStamp = coffHeader.readUInt32LE(4);
190 obj.header.coff.pointerToSymbolTable = coffHeader.readUInt32LE(8);
191 obj.header.coff.numberOfSymbols = coffHeader.readUInt32LE(12);
192 obj.header.coff.sizeOfOptionalHeader = coffHeader.readUInt16LE(16);
193 obj.header.coff.characteristics = coffHeader.readUInt16LE(18);
194
195 // Read the entire PE optional header
196 var optinalHeader = readFileSlice(obj.header.peOptionalHeaderLocation, obj.header.coff.sizeOfOptionalHeader);
197
198 // Decode the PE optional header standard fields
199 // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only
200 obj.header.peStandard = {};
201 obj.header.peStandard.magic = optinalHeader.readUInt16LE(0);
202 switch (obj.header.peStandard.magic) { // Check magic value
203 case 0x020B: obj.header.pe32plus = 1; break;
204 case 0x010B: obj.header.pe32plus = 0; break;
205 default: { obj.close(); return false; } // Invalid Magic in PE
206 }
207 obj.header.peStandard.majorLinkerVersion = optinalHeader[2];
208 obj.header.peStandard.minorLinkerVersion = optinalHeader[3];
209 obj.header.peStandard.sizeOfCode = optinalHeader.readUInt32LE(4);
210 obj.header.peStandard.sizeOfInitializedData = optinalHeader.readUInt32LE(8);
211 obj.header.peStandard.sizeOfUninitializedData = optinalHeader.readUInt32LE(12);
212 obj.header.peStandard.addressOfEntryPoint = optinalHeader.readUInt32LE(16);
213 obj.header.peStandard.baseOfCode = optinalHeader.readUInt32LE(20);
214 if (obj.header.pe32plus == 0) { obj.header.peStandard.baseOfData = optinalHeader.readUInt32LE(24); }
215
216 // Decode the PE optional header windows fields
217 // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-windows-specific-fields-image-only
218 obj.header.peWindows = {}
219 if (obj.header.pe32plus == 0) {
220 // 32bit header
221 //obj.header.peWindows.imageBase = optinalHeader.readUInt32LE(28);
222 obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32);
223 obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36);
224 obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40);
225 obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42);
226 obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44);
227 obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46);
228 obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48);
229 obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50);
230 obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52);
231 obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56);
232 obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60);
233 obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64);
234 obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68);
235 obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70);
236 //obj.header.peWindows.sizeOfStackReserve = optinalHeader.readUInt32LE(72);
237 //obj.header.peWindows.sizeOfStackCommit = optinalHeader.readUInt32LE(76);
238 //obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readUInt32LE(80);
239 //obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readUInt32LE(84);
240 obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(88);
241 obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(92);
242 } else {
243 // 64bit header
244 //obj.header.peWindows.imageBase = optinalHeader.readBigUInt64LE(24); // TODO: readBigUInt64LE is not supported in older NodeJS versions
245 obj.header.peWindows.sectionAlignment = optinalHeader.readUInt32LE(32);
246 obj.header.peWindows.fileAlignment = optinalHeader.readUInt32LE(36);
247 obj.header.peWindows.majorOperatingSystemVersion = optinalHeader.readUInt16LE(40);
248 obj.header.peWindows.minorOperatingSystemVersion = optinalHeader.readUInt16LE(42);
249 obj.header.peWindows.majorImageVersion = optinalHeader.readUInt16LE(44);
250 obj.header.peWindows.minorImageVersion = optinalHeader.readUInt16LE(46);
251 obj.header.peWindows.majorSubsystemVersion = optinalHeader.readUInt16LE(48);
252 obj.header.peWindows.minorSubsystemVersion = optinalHeader.readUInt16LE(50);
253 obj.header.peWindows.win32VersionValue = optinalHeader.readUInt32LE(52);
254 obj.header.peWindows.sizeOfImage = optinalHeader.readUInt32LE(56);
255 obj.header.peWindows.sizeOfHeaders = optinalHeader.readUInt32LE(60);
256 obj.header.peWindows.checkSum = optinalHeader.readUInt32LE(64);
257 obj.header.peWindows.subsystem = optinalHeader.readUInt16LE(68);
258 obj.header.peWindows.dllCharacteristics = optinalHeader.readUInt16LE(70);
259 //obj.header.peWindows.sizeOfStackReserve = optinalHeader.readBigUInt64LE(72);
260 //obj.header.peWindows.sizeOfStackCommit = optinalHeader.readBigUInt64LE(80);
261 //obj.header.peWindows.sizeOfHeapReserve = optinalHeader.readBigUInt64LE(88);
262 //obj.header.peWindows.sizeOfHeapCommit = optinalHeader.readBigUInt64LE(96);
263 obj.header.peWindows.loaderFlags = optinalHeader.readUInt32LE(104);
264 obj.header.peWindows.numberOfRvaAndSizes = optinalHeader.readUInt32LE(108);
265 }
266
267 // Decode the PE optional header data directories
268 // https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-data-directories-image-only
269 obj.header.dataDirectories = {}
270 const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes.
271 obj.header.dataDirectories.exportTable = { addr: optinalHeader.readUInt32LE(96 + pePlusOffset), size: optinalHeader.readUInt32LE(100 + pePlusOffset) };
272 obj.header.dataDirectories.importTable = { addr: optinalHeader.readUInt32LE(104 + pePlusOffset), size: optinalHeader.readUInt32LE(108 + pePlusOffset) };
273 obj.header.dataDirectories.resourceTable = { addr: optinalHeader.readUInt32LE(112 + pePlusOffset), size: optinalHeader.readUInt32LE(116 + pePlusOffset) }; // Same as .rsrc virtual address & size
274 obj.header.dataDirectories.exceptionTableAddr = { addr: optinalHeader.readUInt32LE(120 + pePlusOffset), size: optinalHeader.readUInt32LE(124 + pePlusOffset) }; // Same as .pdata virtual address & size
275 obj.header.dataDirectories.certificateTable = { addr: optinalHeader.readUInt32LE(128 + pePlusOffset), size: optinalHeader.readUInt32LE(132 + pePlusOffset) };
276 obj.header.dataDirectories.baseRelocationTable = { addr: optinalHeader.readUInt32LE(136 + pePlusOffset), size: optinalHeader.readUInt32LE(140 + pePlusOffset) }; // Same as .reloc virtual address & size
277 obj.header.dataDirectories.debug = { addr: optinalHeader.readUInt32LE(144 + pePlusOffset), size: optinalHeader.readUInt32LE(148 + pePlusOffset) };
278 // obj.header.dataDirectories.architecture = optinalHeader.readBigUInt64LE(152 + pePlusOffset); // Must be zero
279 obj.header.dataDirectories.globalPtr = { addr: optinalHeader.readUInt32LE(160 + pePlusOffset), size: optinalHeader.readUInt32LE(164 + pePlusOffset) };
280 obj.header.dataDirectories.tLSTable = { addr: optinalHeader.readUInt32LE(168 + pePlusOffset), size: optinalHeader.readUInt32LE(172 + pePlusOffset) };
281 obj.header.dataDirectories.loadConfigTable = { addr: optinalHeader.readUInt32LE(176 + pePlusOffset), size: optinalHeader.readUInt32LE(180 + pePlusOffset) };
282 obj.header.dataDirectories.boundImport = { addr: optinalHeader.readUInt32LE(184 + pePlusOffset), size: optinalHeader.readUInt32LE(188 + pePlusOffset) };
283 obj.header.dataDirectories.iAT = { addr: optinalHeader.readUInt32LE(192 + pePlusOffset), size: optinalHeader.readUInt32LE(196 + pePlusOffset) };
284 obj.header.dataDirectories.delayImportDescriptor = { addr: optinalHeader.readUInt32LE(200 + pePlusOffset), size: optinalHeader.readUInt32LE(204 + pePlusOffset) };
285 obj.header.dataDirectories.clrRuntimeHeader = { addr: optinalHeader.readUInt32LE(208 + pePlusOffset), size: optinalHeader.readUInt32LE(212 + pePlusOffset) };
286 // obj.header.dataDirectories.reserved = optinalHeader.readBigUInt64LE(216 + pePlusOffset); // Must be zero
287
288 // Get the certificate table location and size
289 obj.header.sigpos = obj.header.dataDirectories.certificateTable.addr;
290 obj.header.siglen = obj.header.dataDirectories.certificateTable.size
291 obj.header.signed = ((obj.header.sigpos != 0) && (obj.header.siglen != 0));
292
293 // The section headers are located after the optional PE header
294 obj.header.SectionHeadersPtr = obj.header.peOptionalHeaderLocation + obj.header.coff.sizeOfOptionalHeader;
295
296 // Read the sections
297 obj.header.sections = {};
298 for (var i = 0; i < obj.header.coff.numberOfSections; i++) {
299 var section = {};
300 buf = readFileSlice(obj.header.SectionHeadersPtr + (i * 40), 40);
301 if ((buf[0] != 46) && (buf[0] != 95)) { obj.close(); return false; }; // Name of the section must start with a dot or underscore. If not, something is wrong.
302 var sectionName = buf.slice(0, 8).toString().trim('\0');
303 var j = sectionName.indexOf('\0');
304 if (j >= 0) { sectionName = sectionName.substring(0, j); } // Trim any trailing zeroes
305 section.ptr = obj.header.SectionHeadersPtr + (i * 40);
306 section.virtualSize = buf.readUInt32LE(8);
307 section.virtualAddr = buf.readUInt32LE(12);
308 section.rawSize = buf.readUInt32LE(16);
309 section.rawAddr = buf.readUInt32LE(20);
310 section.relocAddr = buf.readUInt32LE(24);
311 section.lineNumbers = buf.readUInt32LE(28);
312 section.relocNumber = buf.readUInt16LE(32);
313 section.lineNumbersNumber = buf.readUInt16LE(34);
314 section.characteristics = buf.readUInt32LE(36);
315 obj.header.sections[sectionName] = section;
316 }
317
318 // Compute the checkSum value for this file
319 obj.header.peWindows.checkSumActual = runChecksum();
320
321 // If there is a .rsrc section, read the resource information and locations
322 if (obj.header.sections['.rsrc'] != null) {
323 obj.resources = readResourceTable(obj.header.sections['.rsrc'].rawAddr, 0); // Read all resources recursively
324 }
325
326 if (obj.header.signed) {
327 // Read signature block
328
329 // Check if the file size allows for the signature block
330 if (obj.filesize < (obj.header.sigpos + obj.header.siglen)) { obj.close(); return false; } // Executable file too short to contain the signature block.
331
332 // Remove the padding if needed
333 var i, pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8);
334 var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
335 if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
336
337 // Decode the signature block and check that it's valid
338 var pkcs7der = null, valid = false;
339 try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(pkcs7raw)); } catch (ex) { }
340 try { valid = ((pkcs7der != null) && (forge.asn1.derToOid(pkcs7der.value[1].value[0].value[2].value[0].value) == "1.3.6.1.4.1.311.2.1.4")); } catch (ex) { }
341 if (pkcs7der == null) {
342 // Can't decode the signature
343 obj.header.sigpos = 0;
344 obj.header.siglen = 0;
345 obj.header.signed = false;
346 } else {
347 // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
348 // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
349 pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
350
351 // Decode the PKCS7 message
352 var pkcs7 = null, pkcs7content = null;
353 try {
354 pkcs7 = p7.messageFromAsn1(pkcs7der);
355 pkcs7content = pkcs7.rawCapture.content.value[0];
356 } catch (ex) { }
357
358 if ((pkcs7 == null) || (pkcs7content == null)) {
359 // Can't decode the signature
360 obj.header.sigpos = 0;
361 obj.header.siglen = 0;
362 obj.header.signed = false;
363 } else {
364 // Verify a PKCS#7 signature
365 // Verify is not currently supported in node-forge, but if implemented in the future, this code could work.
366 //var caStore = forge.pki.createCaStore();
367 //for (var i in obj.certificates) { caStore.addCertificate(obj.certificates[i]); }
368 // Return is true if all signatures are valid and chain up to a provided CA
369 //if (!pkcs7.verify(caStore)) { throw ('Executable file has an invalid signature.'); }
370
371 // Get the signing attributes
372 obj.signingAttribs = [];
373 try {
374 for (var i in pkcs7.rawCapture.authenticatedAttributes) {
375 if (
376 (pkcs7.rawCapture.authenticatedAttributes[i].value != null) &&
377 (pkcs7.rawCapture.authenticatedAttributes[i].value[0] != null) &&
378 (pkcs7.rawCapture.authenticatedAttributes[i].value[0].value != null) &&
379 (pkcs7.rawCapture.authenticatedAttributes[i].value[1] != null) &&
380 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value != null) &&
381 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0] != null) &&
382 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value != null) &&
383 (forge.asn1.derToOid(pkcs7.rawCapture.authenticatedAttributes[i].value[0].value) == obj.Oids.SPC_SP_OPUS_INFO_OBJID)) {
384 for (var j in pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value) {
385 if (
386 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j] != null) &&
387 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value != null) &&
388 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0] != null) &&
389 (pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value != null)
390 ) {
391 var v = pkcs7.rawCapture.authenticatedAttributes[i].value[1].value[0].value[j].value[0].value;
392 if (v.startsWith('http://') || v.startsWith('https://') || ((v.length % 2) == 1)) { obj.signingAttribs.push(v); } else {
393 var r = ''; // This string value is in UCS2 format, convert it to a normal string.
394 for (var k = 0; k < v.length; k += 2) { r += String.fromCharCode((v.charCodeAt(k + 8) << 8) + v.charCodeAt(k + 1)); }
395 obj.signingAttribs.push(r);
396 }
397 }
398 }
399 }
400 }
401 } catch (ex) { }
402
403 // Set the certificate chain
404 obj.certificates = pkcs7.certificates;
405
406 // Set the signature
407 obj.signature = Buffer.from(pkcs7.rawCapture.signature, 'binary');
408
409 // Get the file hashing algorithm
410 var hashAlgoOid = forge.asn1.derToOid(pkcs7content.value[1].value[0].value[0].value);
411 switch (hashAlgoOid) {
412 case forge.pki.oids.sha256: { obj.fileHashAlgo = 'sha256'; break; }
413 case forge.pki.oids.sha384: { obj.fileHashAlgo = 'sha384'; break; }
414 case forge.pki.oids.sha512: { obj.fileHashAlgo = 'sha512'; break; }
415 case forge.pki.oids.sha224: { obj.fileHashAlgo = 'sha224'; break; }
416 case forge.pki.oids.md5: { obj.fileHashAlgo = 'md5'; break; }
417 }
418
419 // Get the signed file hash
420 obj.fileHashSigned = Buffer.from(pkcs7content.value[1].value[1].value, 'binary')
421
422 // Compute the actual file hash
423 if (obj.fileHashAlgo != null) { obj.fileHashActual = obj.getHash(obj.fileHashAlgo); }
424 }
425 }
426 }
427 return true;
428 }
429
430 // Make a timestamp signature request
431 obj.timeStampRequest = function (args, func) {
432 // Create the timestamp request in DER format
433 const asn1 = forge.asn1;
434 const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
435 const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
436 const asn1obj =
437 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
438 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
439 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
440 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
441 asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
442 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, obj.signature.toString('binary')) // Signature here
443 ])
444 ])
445 ]);
446
447 // Serialize an ASN.1 object to DER format in Base64
448 const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
449
450 // Make an HTTP request
451 const options = { url: args.time, proxy: args.proxy };
452
453 // Make a request to the time server
454 httpRequest(options, requestBody, function (err, data) {
455 if (err != null) { func(err); return; }
456
457 // Decode the timestamp signature block
458 var timepkcs7der = null;
459 try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
460
461 // Decode the executable signature block
462 var pkcs7der = null;
463 try {
464 var pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(obj.getRawSignatureBlock(), 'base64').toString('binary')));
465
466 // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
467 // TODO: We could look to see if the certificate is already present in the executable
468 const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
469 for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
470
471 // Remove any existing time stamp signatures
472 var newValues = [];
473 for (var i in pkcs7der.value[1].value[0].value[4].value[0].value) {
474 const j = pkcs7der.value[1].value[0].value[4].value[0].value[i];
475 if ((j.tagClass != 128) || (j.type != 1)) { newValues.push(j); } // If this is not a time stamp, add it to out new list.
476 }
477 pkcs7der.value[1].value[0].value[4].value[0].value = newValues; // Set the new list
478
479 // Get the time signature and add it to the executables PKCS7
480 const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
481 const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
482 const asn1obj2 =
483 asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
484 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
485 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
486 timeasn1Signature
487 ])
488 ]);
489 pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
490
491 // Re-encode the executable signature block
492 const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
493
494 // Open the output file
495 var output = null;
496 try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
497 if (output == null) return false;
498 var tmp, written = 0;
499 var executableSize = obj.header.sigpos ? obj.header.sigpos : this.filesize;
500
501 // Compute pre-header length and copy that to the new file
502 var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
503 var tmp = readFileSlice(written, preHeaderLen);
504 fs.writeSync(output, tmp);
505 written += tmp.length;
506
507 // Quad Align the results, adding padding if necessary
508 var len = executableSize + p7signature.length;
509 var padding = (8 - ((len) % 8)) % 8;
510
511 // Write the signature header
512 var addresstable = Buffer.alloc(8);
513 addresstable.writeUInt32LE(executableSize);
514 addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
515 fs.writeSync(output, addresstable);
516 written += addresstable.length;
517
518 // Copy the rest of the file until the start of the signature block
519 while ((executableSize - written) > 0) {
520 tmp = readFileSlice(written, Math.min(executableSize - written, 65536));
521 fs.writeSync(output, tmp);
522 written += tmp.length;
523 }
524
525 // Write the signature block header and signature
526 var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
527 win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
528 win.writeUInt16LE(512, 4); // WORD revision
529 win.writeUInt16LE(2, 6); // WORD type
530 fs.writeSync(output, win);
531 fs.writeSync(output, p7signature);
532 if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
533 written += (p7signature.length + padding + 8);
534
535 // Compute the checksum and write it in the PE header checksum location
536 var tmp = Buffer.alloc(4);
537 tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
538 fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
539
540 // Close the file
541 fs.closeSync(output);
542
543 // Indicate we are done
544 func(null);
545 } catch (ex) { func('' + ex); return; }
546 });
547 }
548
549 // Read a resource table.
550 // ptr: The pointer to the start of the resource section
551 // offset: The offset start of the resource table to read
552 function readResourceTable(ptr, offset) {
553 var buf = readFileSlice(ptr + offset, 16);
554 var r = {};
555 r.characteristics = buf.readUInt32LE(0);
556 r.timeDateStamp = buf.readUInt32LE(4);
557 r.majorVersion = buf.readUInt16LE(8);
558 r.minorVersion = buf.readUInt16LE(10);
559 var numberOfNamedEntries = buf.readUInt16LE(12);
560 var numberOfIdEntries = buf.readUInt16LE(14);
561
562 r.entries = [];
563 var totalResources = numberOfNamedEntries + numberOfIdEntries;
564 //console.log('readResourceTable', offset, 16 + (totalResources) * 8, offset + (16 + (totalResources) * 8));
565 for (var i = 0; i < totalResources; i++) {
566 buf = readFileSlice(ptr + offset + 16 + (i * 8), 8);
567 var resource = {};
568 resource.name = buf.readUInt32LE(0);
569 var offsetToData = buf.readUInt32LE(4);
570 if ((resource.name & 0x80000000) != 0) {
571 var oname = resource.name;
572 resource.name = readLenPrefixUnicodeString(ptr + (resource.name - 0x80000000));
573 //console.log('readResourceName', offset + (oname - 0x80000000), 2 + (resource.name.length * 2), offset + (oname - 0x80000000) + (2 + resource.name.length * 2), resource.name);
574 }
575 if ((offsetToData & 0x80000000) != 0) { resource.table = readResourceTable(ptr, offsetToData - 0x80000000); } else { resource.item = readResourceItem(ptr, offsetToData); }
576 r.entries.push(resource);
577 }
578 return r;
579 }
580
581 // Read a resource item
582 // ptr: The pointer to the start of the resource section
583 // offset: The offset start of the resource item to read
584 function readResourceItem(ptr, offset) {
585 //console.log('readResourceItem', offset, 16, offset + 16);
586 var buf = readFileSlice(ptr + offset, 16), r = {};
587 r.offsetToData = buf.readUInt32LE(0);
588 r.size = buf.readUInt32LE(4);
589 //console.log('readResourceData', r.offsetToData - obj.header.sections['.rsrc'].virtualAddr, r.size, r.offsetToData + r.size - obj.header.sections['.rsrc'].virtualAddr);
590 r.codePage = buf.readUInt32LE(8);
591 //r.reserved = buf.readUInt32LE(12);
592 return r;
593 }
594
595 // Read a unicode stting that starts with the string length as the first byte.
596 function readLenPrefixUnicodeString(ptr) {
597 var nameLen = readFileSlice(ptr, 2).readUInt16LE(0);
598 var buf = readFileSlice(ptr + 2, nameLen * 2), name = '';
599 for (var i = 0; i < nameLen; i++) { name += String.fromCharCode(buf.readUInt16LE(i * 2)); }
600 return name;
601 }
602
603 // Generate a complete resource section and pad the section
604 function generateResourceSection(resources) {
605 // Call a resursive method the compute the size needed for each element
606 const resSizes = { tables: 0, items: 0, names: 0, data: 0 };
607 getResourceSectionSize(resources, resSizes);
608
609 // Pad the resource section & allocate the buffer
610 const fileAlign = obj.header.peWindows.fileAlignment
611 var resSizeTotal = resSizes.tables + resSizes.items + resSizes.names + resSizes.data;
612 var resNoPadding = resSizeTotal + 4; // TODO: Not sure why this is off by 4
613 if ((resSizeTotal % fileAlign) != 0) { resSizeTotal += (fileAlign - (resSizeTotal % fileAlign)); }
614 const resSectionBuffer = Buffer.alloc(resSizeTotal);
615
616 // Write the resource section, calling a recursive method
617 const resPointers = { tables: 0, items: resSizes.tables, names: resSizes.tables + resSizes.items, data: resSizes.tables + resSizes.items + resSizes.names };
618 createResourceSection(resources, resSectionBuffer, resPointers);
619 //console.log('generateResourceSection', resPointers);
620
621 // Done, return the result
622 return { size: resNoPadding, data: resSectionBuffer };
623 }
624
625 // Return the total size of a resource header, this is a recursive method
626 function getResourceSectionSize(resources, sizes) {
627 sizes.tables += (16 + (resources.entries.length * 8));
628 for (var i in resources.entries) {
629 if (typeof resources.entries[i].name == 'string') {
630 var dataSize = (2 + (resources.entries[i].name.length * 2));
631 if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
632 sizes.names += dataSize;
633 }
634 if (resources.entries[i].table) { getResourceSectionSize(resources.entries[i].table, sizes); }
635 else if (resources.entries[i].item) {
636 sizes.items += 16;
637 if (resources.entries[i].item.buffer) {
638 sizes.data += resources.entries[i].item.buffer.length;
639 } else {
640 var dataSize = resources.entries[i].item.size;
641 if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
642 sizes.data += dataSize;
643 }
644 }
645 }
646 }
647
648 // Write the resource section in the buffer, this is a recursive method
649 function createResourceSection(resources, buf, resPointers) {
650 var numberOfNamedEntries = 0, numberOfIdEntries = 0, ptr = resPointers.tables;
651 //console.log('createResourceSection', resPointers, ptr);
652
653 // Figure out how many items we have to save
654 for (var i in resources.entries) {
655 if (typeof resources.entries[i].name == 'string') { numberOfNamedEntries++; } else { numberOfIdEntries++; }
656 }
657
658 // Move the table pointer forward
659 resPointers.tables += (16 + (8 * numberOfNamedEntries) + (8 * numberOfIdEntries));
660
661 // Write the table header
662 buf.writeUInt32LE(resources.characteristics, ptr);
663 buf.writeUInt32LE(resources.timeDateStamp, ptr + 4);
664 buf.writeUInt16LE(resources.majorVersion, ptr + 8);
665 buf.writeUInt16LE(resources.minorVersion, ptr + 10);
666 buf.writeUInt16LE(numberOfNamedEntries, ptr + 12);
667 buf.writeUInt16LE(numberOfIdEntries, ptr + 14);
668
669 // For each table entry, write the entry for it
670 for (var i in resources.entries) {
671 // Write the name
672 var name = resources.entries[i].name;
673 if (typeof resources.entries[i].name == 'string') {
674 // Set the pointer to the name
675 name = resPointers.names + 0x80000000;
676
677 // Write the name length, followed by the name string in unicode
678 buf.writeUInt16LE(resources.entries[i].name.length, resPointers.names);
679 for (var j = 0; j < resources.entries[i].name.length; j++) {
680 buf.writeUInt16LE(resources.entries[i].name.charCodeAt(j), 2 + resPointers.names + (j * 2));
681 }
682
683 // Move the names pointer forward, 8 byte align
684 var dataSize = (2 + (resources.entries[i].name.length * 2));
685 if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
686 resPointers.names += dataSize;
687 }
688 buf.writeUInt32LE(name, ptr + 16 + (i * 8));
689
690 // Write the data
691 var data;
692 if (resources.entries[i].table) {
693 // This is a pointer to a table entry
694 data = resPointers.tables + 0x80000000;
695 createResourceSection(resources.entries[i].table, buf, resPointers);
696 } else if (resources.entries[i].item) {
697 // This is a pointer to a data entry
698 data = resPointers.items;
699
700 // Write the data
701 var entrySize = 0;
702 if (resources.entries[i].item.buffer) {
703 // Write the data from given buffer
704 resources.entries[i].item.buffer.copy(buf, resPointers.data, 0, resources.entries[i].item.buffer.length);
705 entrySize = resources.entries[i].item.buffer.length;
706 } else {
707 // Write the data from original file
708 const actualPtr = (resources.entries[i].item.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + obj.header.sections['.rsrc'].rawAddr;
709 const tmp = readFileSlice(actualPtr, resources.entries[i].item.size);
710 tmp.copy(buf, resPointers.data, 0, tmp.length);
711 entrySize = resources.entries[i].item.size;;
712 }
713
714 // Write the item entry
715 buf.writeUInt32LE(resPointers.data + obj.header.sections['.rsrc'].virtualAddr, resPointers.items); // Write the pointer relative to the virtual address
716 buf.writeUInt32LE(entrySize, resPointers.items + 4);
717 buf.writeUInt32LE(resources.entries[i].item.codePage, resPointers.items + 8);
718 buf.writeUInt32LE(resources.entries[i].item.reserved, resPointers.items + 12);
719
720 // Move items pointers forward
721 resPointers.items += 16;
722 var dataSize = entrySize;
723 if ((dataSize % 8) != 0) { dataSize += (8 - (dataSize % 8)); }
724 resPointers.data += dataSize;
725 }
726 buf.writeUInt32LE(data, ptr + 20 + (i * 8));
727 }
728 }
729
730 // Convert a unicode buffer to a string
731 function unicodeToString(buf) {
732 var r = '', c;
733 for (var i = 0; i < (buf.length / 2) ; i++) {
734 c = buf.readUInt16LE(i * 2);
735 if (c != 0) { r += String.fromCharCode(c); } else { return r; }
736 }
737 return r;
738 }
739
740 // Convert a string to a unicode buffer
741 // Input is a string, a buffer to write to and the offset in the buffer (0 is default).
742 function stringToUnicode(str, buf, offset) {
743 if (offset == null) { offset = 0; }
744 for (var i = 0; i < str.length; i++) { buf.writeInt16LE(str.charCodeAt(i), offset + (i * 2)); }
745 }
746
747 var resourceDefaultNames = {
748 'bitmaps': 2,
749 'icon': 3,
750 'dialogs': 5,
751 'iconGroups': 14,
752 'versionInfo': 16,
753 'configurationFiles': 24
754 }
755
756 // Return the raw signature block buffer with padding removed
757 obj.getRawSignatureBlock = function () {
758 if ((obj.header.sigpos == 0) || (obj.header.siglen == 0)) return null;
759 var pkcs7raw = readFileSlice(obj.header.sigpos + 8, obj.header.siglen - 8);
760 var derlen = forge.asn1.getBerValueLength(forge.util.createBuffer(pkcs7raw.slice(1, 5))) + 4;
761 if (derlen != pkcs7raw.length) { pkcs7raw = pkcs7raw.slice(0, derlen); }
762 return pkcs7raw;
763 }
764
765
766 // Get bitmaps information from resource
767 obj.getBitmapInfo = function () {
768 const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
769
770 // Find and parse each icon
771 const bitmaps = {}
772 for (var i = 0; i < obj.resources.entries.length; i++) {
773 if (obj.resources.entries[i].name == resourceDefaultNames.bitmaps) {
774 for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
775 const bitmapName = obj.resources.entries[i].table.entries[j].name;
776 const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
777 const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
778 const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
779 bitmaps[bitmapName] = readFileSlice(actualPtr, size);
780 }
781 }
782 }
783
784 return bitmaps;
785 }
786
787 // Get icon information from resource
788 obj.getIconInfo = function () {
789 const r = {}, ptr = obj.header.sections['.rsrc'].rawAddr;
790
791 // Find and parse each icon
792 const icons = {}
793 for (var i = 0; i < obj.resources.entries.length; i++) {
794 if (obj.resources.entries[i].name == resourceDefaultNames.icon) {
795 for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
796 const iconName = obj.resources.entries[i].table.entries[j].name;
797 const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
798 const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
799 const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
800 icons[iconName] = readFileSlice(actualPtr, size);
801 }
802 }
803 }
804
805 // Find and parse each icon group
806 for (var i = 0; i < obj.resources.entries.length; i++) {
807 if (obj.resources.entries[i].name == resourceDefaultNames.iconGroups) {
808 for (var j = 0; j < obj.resources.entries[i].table.entries.length; j++) {
809 const groupName = obj.resources.entries[i].table.entries[j].name;
810 const offsetToData = obj.resources.entries[i].table.entries[j].table.entries[0].item.offsetToData;
811 const size = obj.resources.entries[i].table.entries[j].table.entries[0].item.size;
812 const actualPtr = (offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
813 const group = {};
814 const groupData = readFileSlice(actualPtr, size);
815
816 // Parse NEWHEADER structure: https://docs.microsoft.com/en-us/windows/win32/menurc/newheader
817 group.resType = groupData.readUInt16LE(2);
818 group.resCount = groupData.readUInt16LE(4);
819
820 // Parse many RESDIR structure: https://docs.microsoft.com/en-us/windows/win32/menurc/resdir
821 group.icons = {};
822 for (var p = 6; p < size; p += 14) {
823 var icon = {}
824 icon.width = groupData[p];
825 icon.height = groupData[p + 1];
826 icon.colorCount = groupData[p + 2];
827 icon.planes = groupData.readUInt16LE(p + 4);
828 icon.bitCount = groupData.readUInt16LE(p + 6);
829 icon.bytesInRes = groupData.readUInt32LE(p + 8);
830 icon.iconCursorId = groupData.readUInt16LE(p + 12);
831 icon.icon = icons[icon.iconCursorId];
832 group.icons[icon.iconCursorId] = icon;
833 }
834
835 // Add an icon group
836 r[groupName] = group;
837 }
838 }
839 }
840
841 return r;
842 }
843
844 // Set bitmap information
845 obj.setBitmapInfo = function (bitmapInfo) {
846 // Delete all bitmaps resources
847 var resourcesEntries = [];
848 for (var i = 0; i < obj.resources.entries.length; i++) {
849 if (obj.resources.entries[i].name != resourceDefaultNames.bitmaps) {
850 resourcesEntries.push(obj.resources.entries[i]);
851 }
852 }
853 obj.resources.entries = resourcesEntries;
854
855 // Add all bitmap entries
856 const bitmapEntry = { name: resourceDefaultNames.bitmaps, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } };
857 for (var i in bitmapInfo) {
858 var name = i;
859 if (parseInt(i) == name) { name = parseInt(i); }
860 const bitmapItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: bitmapInfo[i], codePage: 0 } }] } }
861 bitmapEntry.table.entries.push(bitmapItemEntry);
862 }
863 obj.resources.entries.push(bitmapEntry);
864
865 // Sort the resources by name. This is required.
866 function resSort(a, b) {
867 if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; }
868 if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; }
869 if ((typeof a == 'string') && (typeof b == 'number')) { return -1; }
870 return 1;
871 }
872 const names = [];
873 for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); }
874 names.sort(resSort);
875 var newEntryOrder = [];
876 for (var i in names) {
877 for (var j = 0; j < obj.resources.entries.length; j++) {
878 if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); }
879 }
880 }
881 obj.resources.entries = newEntryOrder;
882 }
883
884 // Set icon information
885 obj.setIconInfo = function (iconInfo) {
886 // Delete all icon and icon groups resources
887 var resourcesEntries = [];
888 for (var i = 0; i < obj.resources.entries.length; i++) {
889 if ((obj.resources.entries[i].name != resourceDefaultNames.icon) && (obj.resources.entries[i].name != resourceDefaultNames.iconGroups)) {
890 resourcesEntries.push(obj.resources.entries[i]);
891 }
892 }
893 obj.resources.entries = resourcesEntries;
894
895 // Count the icon groups and re-number all icons
896 var iconGroupCount = 0, nextIconNumber = 1;
897 for (var i in iconInfo) {
898 iconGroupCount++;
899 var xicons = {};
900 for (var j in iconInfo[i].icons) { xicons[nextIconNumber++] = iconInfo[i].icons[j]; }
901 iconInfo[i].icons = xicons;
902 }
903 if (iconGroupCount == 0) return; // If there are no icon groups, we are done
904
905 // Add the new icons entry
906 const iconsEntry = { name: resourceDefaultNames.icon, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } };
907 for (var i in iconInfo) {
908 for (var j in iconInfo[i].icons) {
909 var name = j;
910 if (parseInt(j) == name) { name = parseInt(j); }
911 const iconItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: iconInfo[i].icons[j].icon, codePage: 0 } }] } }
912 iconsEntry.table.entries.push(iconItemEntry);
913 }
914 }
915 obj.resources.entries.push(iconsEntry);
916
917 // Add the new icon group entry
918 const groupEntry = { name: resourceDefaultNames.iconGroups, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [] } };
919 for (var i in iconInfo) {
920 // Build icon group struct
921 var iconCount = 0, p = 6;
922 for (var j in iconInfo[i].icons) { iconCount++; }
923 const buf = Buffer.alloc(6 + (iconCount * 14));
924 buf.writeUInt16LE(iconInfo[i].resType, 2);
925 buf.writeUInt16LE(iconCount, 4);
926 for (var j in iconInfo[i].icons) {
927 buf[p] = iconInfo[i].icons[j].width;
928 buf[p + 1] = iconInfo[i].icons[j].height;
929 buf[p + 2] = iconInfo[i].icons[j].colorCount;
930 buf.writeUInt16LE(iconInfo[i].icons[j].planes, p + 4);
931 buf.writeUInt16LE(iconInfo[i].icons[j].bitCount, p + 6);
932 buf.writeUInt32LE(iconInfo[i].icons[j].bytesInRes, p + 8);
933 buf.writeUInt16LE(j, p + 12);
934 p += 14;
935 }
936 var name = i;
937 if (parseInt(i) == name) { name = parseInt(i); }
938 const groupItemEntry = { name: name, table: { characteristics: 0, timeDateStamp: 0, majorVersion: 0, minorVersion: 0, entries: [{ name: 1033, item: { buffer: buf, codePage: 0 } }] } }
939 groupEntry.table.entries.push(groupItemEntry);
940 }
941 obj.resources.entries.push(groupEntry);
942
943 // Sort the resources by name. This is required.
944 function resSort(a, b) {
945 if ((typeof a == 'string') && (typeof b == 'string')) { if (a < b) return -1; if (a > b) return 1; return 0; }
946 if ((typeof a == 'number') && (typeof b == 'number')) { return a - b; }
947 if ((typeof a == 'string') && (typeof b == 'number')) { return -1; }
948 return 1;
949 }
950 const names = [];
951 for (var i = 0; i < obj.resources.entries.length; i++) { names.push(obj.resources.entries[i].name); }
952 names.sort(resSort);
953 var newEntryOrder = [];
954 for (var i in names) {
955 for (var j = 0; j < obj.resources.entries.length; j++) {
956 if (obj.resources.entries[j].name == names[i]) { newEntryOrder.push(obj.resources.entries[j]); }
957 }
958 }
959 obj.resources.entries = newEntryOrder;
960 }
961
962 // Decode the version information from the resource
963 obj.getVersionInfo = function () {
964 var r = {}, info = readVersionInfo(getVersionInfoData(), 0);
965 if ((info == null) || (info.stringFiles == null)) return null;
966 var StringFileInfo = null;
967 for (var i in info.stringFiles) { if (info.stringFiles[i].szKey == 'StringFileInfo') { StringFileInfo = info.stringFiles[i]; } }
968 if ((StringFileInfo == null) || (StringFileInfo.stringTable == null) || (StringFileInfo.stringTable.strings == null)) return null;
969 const strings = StringFileInfo.stringTable.strings;
970 for (var i in strings) { r[strings[i].key] = strings[i].value; }
971 r['~FileVersion'] = (info.fixedFileInfo.dwFileVersionMS >> 16) + '.' + (info.fixedFileInfo.dwFileVersionMS & 0xFFFF) + '.' + (info.fixedFileInfo.dwFileVersionLS >> 16) + '.' + (info.fixedFileInfo.dwFileVersionLS & 0xFFFF);
972 r['~ProductVersion'] = (info.fixedFileInfo.dwProductVersionMS >> 16) + '.' + (info.fixedFileInfo.dwProductVersionMS & 0xFFFF) + '.' + (info.fixedFileInfo.dwProductVersionLS >> 16) + '.' + (info.fixedFileInfo.dwProductVersionLS & 0xFFFF);
973 return r;
974 }
975
976 // Encode the version information to the resource
977 obj.setVersionInfo = function (versions) {
978 // Convert the version information into a string array
979 const stringArray = [];
980 for (var i in versions) { if (!i.startsWith('~')) { stringArray.push({ key: i, value: versions[i] }); } }
981
982 // Get the existing version data and switch the strings to the new strings
983 var r = {}, info = readVersionInfo(getVersionInfoData(), 0);
984 if ((info == null) || (info.stringFiles == null)) return;
985 var StringFileInfo = null;
986 for (var i in info.stringFiles) { if (info.stringFiles[i].szKey == 'StringFileInfo') { StringFileInfo = info.stringFiles[i]; } }
987 if ((StringFileInfo == null) || (StringFileInfo.stringTable == null) || (StringFileInfo.stringTable.strings == null)) return;
988 StringFileInfo.stringTable.strings = stringArray;
989
990 // Set the file version
991 if (versions['~FileVersion'] != null) {
992 const FileVersionSplit = versions['~FileVersion'].split('.');
993 info.fixedFileInfo.dwFileVersionMS = (parseInt(FileVersionSplit[0]) << 16) + parseInt(FileVersionSplit[1]);
994 info.fixedFileInfo.dwFileVersionLS = (parseInt(FileVersionSplit[2]) << 16) + parseInt(FileVersionSplit[3]);
995 }
996
997 // Set the product version
998 if (versions['~ProductVersion'] != null) {
999 const ProductVersionSplit = versions['~ProductVersion'].split('.');
1000 info.fixedFileInfo.dwProductVersionMS = (parseInt(ProductVersionSplit[0]) << 16) + parseInt(ProductVersionSplit[1]);
1001 info.fixedFileInfo.dwProductVersionLS = (parseInt(ProductVersionSplit[2]) << 16) + parseInt(ProductVersionSplit[3]);
1002 }
1003
1004 // Re-encode the version information into a buffer
1005 var verInfoResBufArray = [];
1006 writeVersionInfo(verInfoResBufArray, info);
1007 var verInfoRes = Buffer.concat(verInfoResBufArray);
1008
1009 // Display all buffers
1010 //console.log('--WRITE BUF ARRAY START--');
1011 //for (var i in verInfoResBufArray) { console.log(verInfoResBufArray[i].toString('hex')); }
1012 //console.log('--WRITE BUF ARRAY END--');
1013 //console.log('OUT', Buffer.concat(verInfoResBufArray).toString('hex'));
1014
1015 // Set the new buffer as part of the resources
1016 for (var i = 0; i < obj.resources.entries.length; i++) {
1017 if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) {
1018 const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item;
1019 delete verInfo.size;
1020 delete verInfo.offsetToData;
1021 verInfo.buffer = verInfoRes;
1022 obj.resources.entries[i].table.entries[0].table.entries[0].item = verInfo;
1023 }
1024 }
1025 }
1026
1027 // Return the version info data block
1028 function getVersionInfoData() {
1029 if (obj.resources == null) return null;
1030 const ptr = obj.header.sections['.rsrc'].rawAddr;
1031 for (var i = 0; i < obj.resources.entries.length; i++) {
1032 if (obj.resources.entries[i].name == resourceDefaultNames.versionInfo) {
1033 const verInfo = obj.resources.entries[i].table.entries[0].table.entries[0].item;
1034 if (verInfo.buffer != null) {
1035 return verInfo.buffer;
1036 } else {
1037 const actualPtr = (verInfo.offsetToData - obj.header.sections['.rsrc'].virtualAddr) + ptr;
1038 return readFileSlice(actualPtr, verInfo.size);
1039 }
1040 }
1041 }
1042 return null;
1043 }
1044
1045 // Create a VS_VERSIONINFO structure as a array of buffer that is ready to be placed in the resource section
1046 // VS_VERSIONINFO structure: https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
1047 function writeVersionInfo(bufArray, info) {
1048 const buf = Buffer.alloc(40);
1049 buf.writeUInt16LE(0, 4); // wType
1050 stringToUnicode('VS_VERSION_INFO', buf, 6);
1051 bufArray.push(buf);
1052
1053 var wLength = 40;
1054 var wValueLength = 0;
1055 if (info.fixedFileInfo != null) {
1056 const buf2 = Buffer.alloc(52);
1057 wLength += 52;
1058 wValueLength += 52;
1059 buf2.writeUInt32LE(info.fixedFileInfo.dwSignature, 0); // dwSignature
1060 buf2.writeUInt32LE(info.fixedFileInfo.dwStrucVersion, 4); // dwStrucVersion
1061 buf2.writeUInt32LE(info.fixedFileInfo.dwFileVersionMS, 8); // dwFileVersionMS
1062 buf2.writeUInt32LE(info.fixedFileInfo.dwFileVersionLS, 12); // dwFileVersionLS
1063 buf2.writeUInt32LE(info.fixedFileInfo.dwProductVersionMS, 16); // dwProductVersionMS
1064 buf2.writeUInt32LE(info.fixedFileInfo.dwProductVersionLS, 20); // dwProductVersionLS
1065 buf2.writeUInt32LE(info.fixedFileInfo.dwFileFlagsMask, 24); // dwFileFlagsMask
1066 buf2.writeUInt32LE(info.fixedFileInfo.dwFileFlags, 28); // dwFileFlags
1067 buf2.writeUInt32LE(info.fixedFileInfo.dwFileOS, 32); // dwFileOS
1068 buf2.writeUInt32LE(info.fixedFileInfo.dwFileType, 36); // dwFileType
1069 buf2.writeUInt32LE(info.fixedFileInfo.dwFileSubtype, 40); // dwFileSubtype
1070 buf2.writeUInt32LE(info.fixedFileInfo.dwFileDateMS, 44); // dwFileDateMS
1071 buf2.writeUInt32LE(info.fixedFileInfo.dwFileDateLS, 48); // dwFileDateLS
1072 bufArray.push(buf2);
1073 }
1074
1075 if (info.stringFiles != null) { wLength += writeStringFileInfo(bufArray, info.stringFiles); }
1076
1077 buf.writeUInt16LE(Buffer.concat(bufArray).length, 0); // wLength
1078 buf.writeUInt16LE(wValueLength, 2); // wValueLength
1079 return wLength;
1080 }
1081
1082 // StringFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo
1083 function writeStringFileInfo(bufArray, stringFiles) {
1084 var totalLen = 0;
1085 for (var i in stringFiles) {
1086 var l = 6 + (stringFiles[i].szKey.length * 2);
1087 if (stringFiles[i].szKey == 'VarFileInfo') { l += 4; } // TODO: This is a hack, not sure what the correct code should be
1088 const buf2 = Buffer.alloc(padPointer(l));
1089 buf2.writeUInt16LE(1, 4); // wType
1090 stringToUnicode(stringFiles[i].szKey, buf2, 6);
1091 bufArray.push(buf2);
1092
1093 var wLength = 0, wValueLength = 0;
1094
1095 if (stringFiles[i].szKey == 'StringFileInfo') { wLength += writeStringTableStruct(bufArray, stringFiles[i].stringTable); }
1096 if (stringFiles[i].szKey == 'VarFileInfo') { wLength += writeVarFileInfoStruct(bufArray, stringFiles[i].varFileInfo); }
1097
1098 buf2.writeUInt16LE(l + wLength, 0); // wLength
1099 buf2.writeUInt16LE(wValueLength, 2); // wValueLength
1100 totalLen += buf2.length + wLength;
1101 }
1102 return totalLen;
1103 }
1104
1105 // VarFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/var-str
1106 function writeVarFileInfoStruct(bufArray, varFileInfo) {
1107 var l = 8 + (varFileInfo.szKey.length * 2);
1108 const buf = Buffer.alloc(padPointer(l));
1109 buf.writeUInt16LE(0, 4); // wType
1110 stringToUnicode(varFileInfo.szKey, buf, 6);
1111 bufArray.push(buf);
1112
1113 var wLength = 0;
1114 var wValueLength = 0;
1115
1116 if (varFileInfo.value) {
1117 bufArray.push(varFileInfo.value);
1118 wLength += varFileInfo.value.length;
1119 wValueLength += varFileInfo.value.length;
1120 }
1121 buf.writeUInt16LE(buf.length + wLength, 0); // wLength
1122 buf.writeUInt16LE(wValueLength, 2); // wValueLength
1123 return buf.length + wLength;
1124 }
1125
1126 // StringTable structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable
1127 function writeStringTableStruct(bufArray, stringTable) {
1128 //console.log('writeStringTableStruct', stringTable);
1129 var l = 6 + (stringTable.szKey.length * 2);
1130 const buf = Buffer.alloc(padPointer(l));
1131 buf.writeUInt16LE(1, 4); // wType
1132 stringToUnicode(stringTable.szKey, buf, 6);
1133 bufArray.push(buf);
1134
1135 var wLength = 0;
1136 var wValueLength = 0;
1137
1138 if (stringTable.strings) { wLength += writeStringStructs(bufArray, stringTable.strings); }
1139 buf.writeUInt16LE(l + wLength, 0); // wLength
1140 buf.writeUInt16LE(wValueLength, 2); // wValueLength
1141 return buf.length + wLength;
1142 }
1143
1144 // String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str
1145 function writeStringStructs(bufArray, stringTable) {
1146 //console.log('writeStringStructs', stringTable);
1147 var totalLen = 0, bufadd = 0;
1148 for (var i in stringTable) {
1149 //console.log('writeStringStructs', stringTable[i]);
1150 const buf = Buffer.alloc(padPointer(6 + ((stringTable[i].key.length + 1) * 2)));
1151 var buf2, wLength = buf.length;
1152 var wValueLength = 0;
1153 stringToUnicode(stringTable[i].key, buf, 6);
1154 bufArray.push(buf);
1155 bufadd += buf.length;
1156 if (typeof stringTable[i].value == 'string') {
1157 // wType (string)
1158 buf.writeUInt16LE(1, 4);
1159 var l = (stringTable[i].value.length + 1) * 2;
1160 buf2 = Buffer.alloc(padPointer(l));
1161 stringToUnicode(stringTable[i].value, buf2, 0);
1162 bufArray.push(buf2);
1163 bufadd += buf2.length;
1164 wValueLength = stringTable[i].value.length + 1;
1165 wLength += l;
1166 }
1167 if (typeof stringTable[i].value == 'object') {
1168 // wType (binary)
1169 buf.writeUInt16LE(2, 4); // TODO: PADDING
1170 bufArray.push(stringTable[i].value);
1171 bufadd += stringTable[i].value.length;
1172 wValueLength = stringTable[i].value.length;
1173 wLength += wValueLength;
1174 }
1175 buf.writeUInt16LE(wLength, 0); // wLength
1176 buf.writeUInt16LE(wValueLength, 2); // wValueLength
1177 //console.log('WStringStruct', buf.toString('hex'), buf2.toString('hex'));
1178 totalLen += wLength;
1179 }
1180 //return totalLen;
1181 return bufadd;
1182 }
1183
1184 // VS_VERSIONINFO structure: https://docs.microsoft.com/en-us/windows/win32/menurc/vs-versioninfo
1185 function readVersionInfo(buf, ptr) {
1186 const r = {};
1187 if (buf.length < 2) return null;
1188 const wLength = buf.readUInt16LE(ptr);
1189 if (buf.length < wLength) return null;
1190 const wValueLength = buf.readUInt16LE(ptr + 2);
1191 const wType = buf.readUInt16LE(ptr + 4);
1192 r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 36));
1193 if (r.szKey != 'VS_VERSION_INFO') return null;
1194 //console.log('getVersionInfo', wLength, wValueLength, wType, r.szKey.toString());
1195 if (wValueLength == 52) { r.fixedFileInfo = readFixedFileInfoStruct(buf, ptr + 40); }
1196 r.stringFiles = readStringFilesStruct(buf, ptr + 40 + wValueLength, wLength - 40 - wValueLength);
1197 return r;
1198 }
1199
1200 // VS_FIXEDFILEINFO structure: https://docs.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
1201 function readFixedFileInfoStruct(buf, ptr) {
1202 if (buf.length - ptr < 50) return null;
1203 var r = {};
1204 r.dwSignature = buf.readUInt32LE(ptr);
1205 if (r.dwSignature != 0xFEEF04BD) return null;
1206 r.dwStrucVersion = buf.readUInt32LE(ptr + 4);
1207 r.dwFileVersionMS = buf.readUInt32LE(ptr + 8);
1208 r.dwFileVersionLS = buf.readUInt32LE(ptr + 12);
1209 r.dwProductVersionMS = buf.readUInt32LE(ptr + 16);
1210 r.dwProductVersionLS = buf.readUInt32LE(ptr + 20);
1211 r.dwFileFlagsMask = buf.readUInt32LE(ptr + 24);
1212 r.dwFileFlags = buf.readUInt32LE(ptr + 28);
1213 r.dwFileOS = buf.readUInt32LE(ptr + 32);
1214 r.dwFileType = buf.readUInt32LE(ptr + 36);
1215 r.dwFileSubtype = buf.readUInt32LE(ptr + 40);
1216 r.dwFileDateMS = buf.readUInt32LE(ptr + 44);
1217 r.dwFileDateLS = buf.readUInt32LE(ptr + 48);
1218 return r;
1219 }
1220
1221 // StringFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringfileinfo
1222 function readStringFilesStruct(buf, ptr, len) {
1223 var t = [], startPtr = ptr;
1224 while (ptr < (startPtr + len)) {
1225 const r = {};
1226 const wLength = buf.readUInt16LE(ptr);
1227 if (wLength == 0) return t;
1228 const wValueLength = buf.readUInt16LE(ptr + 2);
1229 const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary
1230 r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + (wLength - 6))); // String value
1231 //console.log('readStringFileStruct', wLength, wValueLength, wType, r.szKey);
1232 if (r.szKey == 'StringFileInfo') { r.stringTable = readStringTableStruct(buf, ptr + 36); }
1233 if (r.szKey == 'VarFileInfo') { r.varFileInfo = readVarFileInfoStruct(buf, ptr + 32); }
1234 t.push(r);
1235 ptr += wLength;
1236 ptr = padPointer(ptr);
1237 }
1238 return t;
1239 }
1240
1241 // VarFileInfo structure: https://docs.microsoft.com/en-us/windows/win32/menurc/var-str
1242 function readVarFileInfoStruct(buf, ptr) {
1243 const r = {};
1244 const wLength = buf.readUInt16LE(ptr);
1245 const wValueLength = buf.readUInt16LE(ptr + 2);
1246 const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary
1247 r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + wLength)); // "VarFileInfo"
1248 r.value = buf.slice(ptr + wLength - wValueLength, ptr + wLength)
1249 //console.log('readVarFileInfoStruct', wLength, wValueLength, wType, r.szKey, r.value.toString('hex'));
1250 return r;
1251 }
1252
1253 // StringTable structure: https://docs.microsoft.com/en-us/windows/win32/menurc/stringtable
1254 function readStringTableStruct(buf, ptr) {
1255 const r = {};
1256 const wLength = buf.readUInt16LE(ptr);
1257 const wValueLength = buf.readUInt16LE(ptr + 2);
1258 const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary
1259 //console.log('RStringTableStruct', buf.slice(ptr, ptr + wLength).toString('hex'));
1260 r.szKey = unicodeToString(buf.slice(ptr + 6, ptr + 6 + 16)); // An 8-digit hexadecimal number stored as a Unicode string.
1261 //console.log('readStringTableStruct', wLength, wValueLength, wType, r.szKey);
1262 r.strings = readStringStructs(buf, ptr + 24 + wValueLength, wLength - 22);
1263 return r;
1264 }
1265
1266 // String structure: https://docs.microsoft.com/en-us/windows/win32/menurc/string-str
1267 function readStringStructs(buf, ptr, len) {
1268 var t = [], startPtr = ptr;
1269 while ((ptr + 6) < (startPtr + len)) {
1270 const r = {};
1271 const wLength = buf.readUInt16LE(ptr);
1272 if (wLength == 0) return t;
1273
1274 //console.log('RStringStruct', buf.slice(ptr, ptr + wLength).toString('hex'));
1275
1276 const wValueLength = buf.readUInt16LE(ptr + 2);
1277 const wType = buf.readUInt16LE(ptr + 4); // 1 = Text, 2 = Binary
1278
1279 //console.log('R', buf.slice(ptr, ptr + wLength).toString('hex'));
1280
1281 r.key = unicodeToString(buf.slice(ptr + 6, ptr + (wLength - (wValueLength * 2)) - 2)); // Key
1282 if (wType == 1) { r.value = unicodeToString(buf.slice(ptr + wLength - (wValueLength * 2), ptr + wLength - 2)); } // String value
1283 if (wType == 2) { r.value = buf.slice(ptr + wLength - (wValueLength * 2), ptr + wLength); } // Binary value
1284 //console.log('readStringStruct', wLength, wValueLength, wType, r.key, r.value);
1285 t.push(r);
1286 ptr += wLength;
1287 ptr = padPointer(ptr);
1288 }
1289 return t;
1290 }
1291
1292 // Return the next 4 byte aligned number
1293 function padPointer(ptr) { return ptr + (((ptr % 4) == 0) ? 0 : (4 - (ptr % 4))); }
1294 //function padPointer(ptr) { return ptr + (ptr % 4); }
1295
1296 // Hash the file using the selected hashing system
1297 // This hash skips the executables CRC and code signing data and signing block
1298 obj.getHash = function(algo) {
1299 const hash = crypto.createHash(algo);
1300 runHash(hash, 0, obj.header.peHeaderLocation + 88);
1301 runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1302 runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize);
1303 return hash.digest();
1304 }
1305
1306 // Hash of an open file using the selected hashing system
1307 // This hash skips the executables CRC and code signing data and signing block
1308 obj.getHashOfFile = function(fd, algo, filesize) {
1309 const hash = crypto.createHash(algo);
1310 runHashOnFile(fd, hash, 0, obj.header.peHeaderLocation + 88);
1311 runHashOnFile(fd, hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1312 runHashOnFile(fd, hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, obj.header.sigpos > 0 ? obj.header.sigpos : filesize);
1313 return hash.digest();
1314 }
1315
1316 // Hash the file using the selected hashing system skipping resource section
1317 // This hash skips the executables CRC, sections table, resource section, code signing data and signing block
1318 obj.getHashNoResources = function (algo) {
1319 if (obj.header.sections['.rsrc'] == null) { return obj.getHash(algo); } // No resources in this executable, return a normal hash
1320
1321 // Get the sections table start and size
1322 const sectionHeaderPtr = obj.header.SectionHeadersPtr;
1323 const sectionHeaderSize = obj.header.coff.numberOfSections * 40;
1324
1325 // Get the resource section start and size
1326 const resPtr = obj.header.sections['.rsrc'].rawAddr;
1327 const resSize = obj.header.sections['.rsrc'].rawSize;
1328
1329 // Get the end-of-file location
1330 const eof = obj.header.sigpos > 0 ? obj.header.sigpos : obj.filesize;
1331
1332 // Hash the remaining data
1333 const hash = crypto.createHash(algo);
1334 runHash(hash, 0, obj.header.peHeaderLocation + 88);
1335 runHash(hash, obj.header.peHeaderLocation + 88 + 4, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1336 runHash(hash, obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16) + 8, sectionHeaderPtr);
1337 runHash(hash, sectionHeaderPtr + sectionHeaderSize, resPtr);
1338 runHash(hash, resPtr + resSize, eof);
1339 return hash.digest();
1340 }
1341
1342 // Hash the file using the selected hashing system skipping resource section
1343 // This hash skips the executables CRC, sections table, resource section, code signing data and signing block
1344 obj.getHashOfSection = function (algo, sectionName) {
1345 if (obj.header.sections[sectionName] == null) return null;
1346
1347 // Get the section start and size
1348 const sectionPtr = obj.header.sections[sectionName].rawAddr;
1349 const sectionSize = obj.header.sections[sectionName].rawSize;
1350
1351 // Hash the remaining data
1352 const hash = crypto.createHash(algo);
1353 runHash(hash, sectionPtr, sectionPtr + sectionSize);
1354 return hash.digest();
1355 }
1356
1357 // Hash the file from start to end loading 64k chunks
1358 function runHash(hash, start, end) {
1359 var ptr = start;
1360 while (ptr < end) { const buf = readFileSlice(ptr, Math.min(65536, end - ptr)); hash.update(buf); ptr += buf.length; }
1361 }
1362
1363 // Hash the open file loading 64k chunks
1364 // TODO: Do chunks on this!!!
1365 function runHashOnFile(fd, hash, start, end) {
1366 const buf = Buffer.alloc(end - start);
1367 const len = fs.readSync(fd, buf, 0, buf.length, start);
1368 if (len != buf.length) { console.log('BAD runHashOnFile'); }
1369 hash.update(buf);
1370 }
1371
1372 // Checksum the file loading 64k chunks
1373 function runChecksum() {
1374 var ptr = 0, c = createChecksum(((obj.header.peOptionalHeaderLocation + 64) / 4));
1375 while (ptr < obj.filesize) { const buf = readFileSlice(ptr, Math.min(65536, obj.filesize - ptr)); c.update(buf); ptr += buf.length; }
1376 return c.digest();
1377 }
1378
1379 // Checksum the open file loading 64k chunks
1380 function runChecksumOnFile(fd, filesize, checksumLocation) {
1381 var ptr = 0, c = createChecksum(checksumLocation), buf = Buffer.alloc(65536);
1382 while (ptr < filesize) { var len = fs.readSync(fd, buf, 0, Math.min(65536, filesize - ptr), ptr); c.update(buf, len); ptr += len; }
1383 return c.digest();
1384 }
1385
1386 // Steaming checksum methods
1387 // TODO: Works only with files padded to 4 byte.
1388 function createChecksum(checksumLocation) {
1389 const obj = { checksum: 0, length: 0 };
1390 obj.update = function (data, len) {
1391 if (!len) { len = data.length; }
1392 for (var i = 0; i < (len / 4) ; i++) {
1393 if (((obj.length / 4) + i) == checksumLocation) continue; // Skip PE checksum location
1394 const dword = data.readUInt32LE(i * 4);
1395 var checksumlo = (obj.checksum > 4294967296) ? (obj.checksum - 4294967296) : obj.checksum;
1396 var checksumhi = (obj.checksum > 4294967296) ? 1 : 0;
1397 obj.checksum = checksumlo + dword + checksumhi;
1398 if (obj.checksum > 4294967296) {
1399 checksumlo = (obj.checksum > 4294967296) ? (obj.checksum - 4294967296) : obj.checksum;
1400 checksumhi = (obj.checksum > 4294967296) ? 1 : 0;
1401 obj.checksum = checksumlo + checksumhi;
1402 }
1403 }
1404 obj.length += len;
1405 }
1406 obj.digest = function () {
1407 obj.checksum = (obj.checksum & 0xffff) + (obj.checksum >>> 16);
1408 obj.checksum = (obj.checksum) + (obj.checksum >>> 16);
1409 obj.checksum = obj.checksum & 0xffff;
1410 obj.checksum += obj.length;
1411 return obj.checksum;
1412 }
1413 return obj;
1414 }
1415
1416 // Compute the PE checksum of an entire file
1417 function getChecksum(data, checksumLocation) {
1418 var checksum = 0;
1419 for (var i = 0; i < (data.length / 4) ; i++) {
1420 if (i == (checksumLocation / 4)) continue; // Skip PE checksum location
1421 var dword = data.readUInt32LE(i * 4);
1422 var checksumlo = (checksum > 4294967296) ? (checksum - 4294967296) : checksum;
1423 var checksumhi = (checksum > 4294967296) ? 1 : 0;
1424 checksum = checksumlo + dword + checksumhi;
1425 if (checksum > 4294967296) {
1426 checksumlo = (checksum > 4294967296) ? (checksum - 4294967296) : checksum;
1427 checksumhi = (checksum > 4294967296) ? 1 : 0;
1428 checksum = checksumlo + checksumhi;
1429 }
1430 }
1431 checksum = (checksum & 0xffff) + (checksum >>> 16);
1432 checksum = (checksum) + (checksum >>> 16);
1433 checksum = checksum & 0xffff;
1434 checksum += data.length;
1435 return checksum;
1436 }
1437
1438 // Sign the file using the certificate and key. If none is specified, generate a dummy one
1439 obj.sign = function (cert, args, func) {
1440 if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); }
1441
1442 // Set the hash algorithm hash OID
1443 var hashOid = null, fileHash = null;
1444 if (args.hash == null) { args.hash = 'sha384'; }
1445 if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHash('sha256'); }
1446 if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHash('sha384'); }
1447 if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHash('sha512'); }
1448 if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHash('sha224'); }
1449 if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHash('md5'); }
1450 if (hashOid == null) { func('Invalid signing hash: ' + args.hash); return; };
1451
1452 // Create the signature block
1453 var xp7 = forge.pkcs7.createSignedData();
1454 var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
1455 xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
1456 xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
1457 xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
1458 xp7.addCertificate(cert.cert);
1459 if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
1460
1461 // Build authenticated attributes
1462 var authenticatedAttributes = [
1463 { type: forge.pki.oids.contentType, value: forge.pki.oids.data },
1464 { type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge
1465 ]
1466 if ((typeof args.desc == 'string') || (typeof args.url == 'string')) {
1467 var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] };
1468 if (args.desc != null) { // Encode description as big-endian unicode.
1469 var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString()
1470 for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); }
1471 codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] });
1472 }
1473 if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); }
1474 authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes });
1475 }
1476
1477 // Add the signer and sign
1478 xp7.addSigner({
1479 key: cert.key,
1480 certificate: cert.cert,
1481 digestAlgorithm: forge.pki.oids.sha384,
1482 authenticatedAttributes: authenticatedAttributes
1483 });
1484 xp7.sign();
1485 var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
1486
1487 if (args.time == null) {
1488 // Sign the executable without timestamp
1489 signEx(args, p7signature, obj.filesize, func);
1490 } else {
1491 // Decode the signature block
1492 var pkcs7der = null;
1493 try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
1494
1495 // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
1496 // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
1497 pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
1498
1499 // Decode the PKCS7 message
1500 var pkcs7 = p7.messageFromAsn1(pkcs7der);
1501
1502 // Create the timestamp request in DER format
1503 const asn1 = forge.asn1;
1504 const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
1505 const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
1506 const asn1obj =
1507 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1508 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
1509 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1510 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
1511 asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
1512 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here
1513 ])
1514 ])
1515 ]);
1516
1517 // Re-decode the PKCS7 from the executable, this time, no workaround needed
1518 try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
1519
1520 // Serialize an ASN.1 object to DER format in Base64
1521 const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
1522
1523 // Make an HTTP request
1524 const options = { url: args.time, proxy: args.proxy };
1525
1526 // Make a request to the time server
1527 httpRequest(options, requestBody, function (err, data) {
1528 if (err != null) { func(err); return; }
1529
1530 // Decode the timestamp signature block
1531 var timepkcs7der = null;
1532 try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
1533
1534 try {
1535 // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
1536 // TODO: We could look to see if the certificate is already present in the executable
1537 const timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
1538 for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
1539
1540 // Get the time signature and add it to the executables PKCS7
1541 const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
1542 const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
1543 const asn1obj2 =
1544 asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
1545 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1546 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
1547 timeasn1Signature
1548 ])
1549 ]);
1550 pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
1551
1552 // Re-encode the executable signature block
1553 const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
1554
1555 // Write the file with the signature block
1556 signEx(args, p7signature, obj.filesize, func);
1557 } catch (ex) { func('' + ex); }
1558 });
1559 }
1560 }
1561
1562 // Make a HTTP request, use a proxy if needed
1563 function httpRequest(options, requestBody, func) {
1564 // Decode the URL
1565 const timeServerUrl = new URL(options.url);
1566 options.protocol = timeServerUrl.protocol;
1567 options.hostname = timeServerUrl.hostname;
1568 options.path = timeServerUrl.pathname;
1569 let http = require("http")
1570 if (options.protocol === "https:"){
1571 http = require("https")
1572 }
1573 options.port = ((timeServerUrl.port == '') ? (options.protocol === "https:" ? 443 : 80) : parseInt(timeServerUrl.port));
1574
1575 if (options.proxy == null) {
1576 // No proxy needed
1577
1578 // Setup the options
1579 delete options.url;
1580 options.method = 'POST';
1581 options.headers = {
1582 'accept': 'application/octet-stream',
1583 'cache-control': 'no-cache',
1584 'user-agent': 'Transport',
1585 'content-type': 'application/octet-stream',
1586 'content-length': Buffer.byteLength(requestBody)
1587 };
1588
1589 // Set up the request
1590 var responseAccumulator = '';
1591 var req = http.request(options, function (res) {
1592 res.setEncoding('utf8');
1593 res.on('data', function (chunk) { responseAccumulator += chunk; });
1594 res.on('end', function () { func(null, responseAccumulator); });
1595 });
1596
1597 // Post the data
1598 req.on('error', function (err) { func('' + err); });
1599 req.write(requestBody);
1600 req.end();
1601 } else {
1602 // We are using a proxy
1603 // This is a fairly basic proxy implementation, should work most of the time.
1604
1605 // Setup the options and decode the proxy URL
1606 var proxyOptions = { method: 'CONNECT' };
1607 if (options.proxy) {
1608 const proxyUrl = new URL(options.proxy);
1609 proxyOptions.protocol = proxyUrl.protocol;
1610 proxyOptions.hostname = proxyUrl.hostname;
1611 proxyOptions.path = options.hostname + ':' + options.port;
1612 proxyOptions.port = ((proxyUrl.port == '') ? (options.protocol === "https:" ? 443 : 80) : parseInt(proxyUrl.port));
1613 }
1614
1615 // Set up the proxy request
1616 var responseAccumulator = '';
1617 var req = http.request(proxyOptions);
1618 req.on('error', function (err) { func('' + err); });
1619 req.on('connect', function (res, socket, head) {
1620 // Make a request over the HTTP tunnel
1621 socket.write('POST ' + options.path + ' HTTP/1.1\r\n' +
1622 'host: ' + options.hostname + ':' + options.port + '\r\n' +
1623 'accept: application/octet-stream\r\n' +
1624 'cache-control: no-cache\r\n' +
1625 'user-agent: Transport\r\n' +
1626 'content-type: application/octet-stream\r\n' +
1627 'content-length: ' + Buffer.byteLength(requestBody) + '\r\n' +
1628 '\r\n' + requestBody);
1629 socket.on('data', function (chunk) {
1630 responseAccumulator += chunk.toString();
1631 var responseData = parseHttpResponse(responseAccumulator);
1632 if (responseData != null) { try { socket.end(); } catch (ex) { console.log('ex', ex); } socket.xdone = true; func(null, responseData); }
1633 });
1634 socket.on('end', function () {
1635 if (socket.xdone == true) return;
1636 var responseData = parseHttpResponse(responseAccumulator);
1637 if (responseData != null) { func(null, responseData); } else { func("Unable to parse response."); }
1638 });
1639 });
1640 req.end();
1641 }
1642 }
1643
1644 // Parse the HTTP response and return data if available
1645 function parseHttpResponse(data) {
1646 var dataSplit = data.split('\r\n\r\n');
1647 if (dataSplit.length < 2) return null;
1648
1649 // Parse the HTTP header
1650 var headerSplit = dataSplit[0].split('\r\n'), headers = {};
1651 for (var i in headerSplit) {
1652 if (i != 0) {
1653 var x = headerSplit[i].indexOf(':');
1654 headers[headerSplit[i].substring(0, x).toLowerCase()] = headerSplit[i].substring(x + 2);
1655 }
1656 }
1657
1658 // If there is a content-length in the header, keep accumulating data until we have the right length
1659 if (headers['content-length'] != null) {
1660 const contentLength = parseInt(headers['content-length']);
1661 if (dataSplit[1].length < contentLength) return null; // Wait for more data
1662 return dataSplit[1];
1663 }
1664 return dataSplit[1];
1665 }
1666
1667 // Complete the signature of an executable
1668 function signEx(args, p7signature, filesize, func) {
1669 // Open the output file
1670 var output = null;
1671 try { output = fs.openSync(args.out, 'w+'); } catch (ex) { }
1672 if (output == null) { func('Unable to open output file: ' + args.out); return; }
1673 var tmp, written = 0, executableSize = obj.header.sigpos ? obj.header.sigpos : filesize;
1674
1675 // Compute pre-header length and copy that to the new file
1676 var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1677 var tmp = readFileSlice(written, preHeaderLen);
1678 fs.writeSync(output, tmp);
1679 written += tmp.length;
1680
1681 // Quad Align the results, adding padding if necessary
1682 var len = executableSize + p7signature.length;
1683 var padding = (8 - ((len) % 8)) % 8;
1684
1685 // Write the signature header
1686 var addresstable = Buffer.alloc(8);
1687 addresstable.writeUInt32LE(executableSize);
1688 addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
1689 fs.writeSync(output, addresstable);
1690 written += addresstable.length;
1691
1692 // Copy the rest of the file until the start of the signature block
1693 while ((executableSize - written) > 0) {
1694 tmp = readFileSlice(written, Math.min(executableSize - written, 65536));
1695 fs.writeSync(output, tmp);
1696 written += tmp.length;
1697 }
1698
1699 // Write the signature block header and signature
1700 var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
1701 win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
1702 win.writeUInt16LE(512, 4); // WORD revision
1703 win.writeUInt16LE(2, 6); // WORD type
1704 fs.writeSync(output, win);
1705 fs.writeSync(output, p7signature);
1706 if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
1707 written += (p7signature.length + padding + 8);
1708
1709 // Compute the checksum and write it in the PE header checksum location
1710 var tmp = Buffer.alloc(4);
1711 tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
1712 fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
1713
1714 // Close the file
1715 fs.closeSync(output);
1716 func(null);
1717 }
1718
1719 // Save an executable without the signature
1720 obj.unsign = function (args) {
1721 // Open the file
1722 var output = fs.openSync(args.out, 'w+');
1723 var written = 0, totalWrite = obj.header.sigpos;
1724
1725 // Compute pre-header length and copy that to the new file
1726 var preHeaderLen = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
1727 var tmp = readFileSlice(written, preHeaderLen);
1728 fs.writeSync(output, tmp);
1729 written += tmp.length;
1730
1731 // Write the new signature header
1732 fs.writeSync(output, Buffer.alloc(8));
1733 written += 8;
1734
1735 // Copy the rest of the file until the start of the signature block
1736 while ((totalWrite - written) > 0) {
1737 tmp = readFileSlice(written, Math.min(totalWrite - written, 65536));
1738 fs.writeSync(output, tmp);
1739 written += tmp.length;
1740 }
1741
1742 // Compute the checksum and write it in the PE checksum header at position
1743 var tmp = Buffer.alloc(4);
1744 tmp.writeUInt32LE(runChecksumOnFile(output, written));
1745 fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
1746
1747 fs.closeSync(output);
1748 }
1749
1750 // Find where a directory value is in the old sections and map it to the new sections
1751 function correctDirectoryValue(oldSections, newSections, value) {
1752 for (var i in oldSections) {
1753 if ((value >= oldSections[i].virtualAddr) && (value < (oldSections[i].virtualAddr + oldSections[i].virtualSize))) {
1754 return newSections[i].virtualAddr + (value - oldSections[i].virtualAddr);
1755 }
1756 }
1757 return 0;
1758 }
1759
1760 // Save the executable
1761 obj.writeExecutable = function (args, cert, func) {
1762 // Open the file
1763 var output = fs.openSync(args.out, 'w+');
1764 var tmp, written = 0;
1765
1766 // Compute the size of the complete executable header up to after the sections header
1767 var fullHeaderLen = obj.header.SectionHeadersPtr + (obj.header.coff.numberOfSections * 40);
1768 var fullHeader = readFileSlice(written, fullHeaderLen);
1769
1770 // Create the resource section
1771 const rsrcSectionX = generateResourceSection(obj.resources); // This section is created with padding already included
1772 var rsrcSection = rsrcSectionX.data;
1773 var rsrcSectionVirtualSize = rsrcSectionX.size;
1774 var rsrcSectionRawSize = rsrcSection.length;
1775
1776 // Calculate the location and original and new size of the resource segment
1777 var fileAlign = obj.header.peWindows.fileAlignment
1778 var resPtr = obj.header.sections['.rsrc'].rawAddr;
1779 var oldResSize = obj.header.sections['.rsrc'].rawSize;
1780 var newResSize = rsrcSection.length;
1781 var resDeltaSize = newResSize - oldResSize;
1782
1783 // Compute the sizeOfInitializedData
1784 var sizeOfInitializedData = 0;
1785 for (var i in obj.header.sections) {
1786 if (i != '.text') {
1787 if (i == '.rsrc') {
1788 sizeOfInitializedData += rsrcSectionRawSize;
1789 } else {
1790 sizeOfInitializedData += obj.header.sections[i].rawSize;
1791 }
1792 }
1793 }
1794
1795 // Change PE optional header sizeOfInitializedData standard field
1796 fullHeader.writeUInt32LE(sizeOfInitializedData, obj.header.peOptionalHeaderLocation + 8);
1797
1798 // Update the checksum to zero
1799 fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 64);
1800
1801 // We are going to setup the old a new sections here, we need this to correct directory values
1802 var oldSections = obj.header.sections;
1803 var newSections = {};
1804
1805 // Make changes to the segments table
1806 var virtualAddress = 4096;
1807 for (var i in obj.header.sections) {
1808 const section = obj.header.sections[i];
1809 newSections[i] = { virtualSize: section.virtualSize };
1810 if (i == '.rsrc') {
1811 // Change the size of the resource section
1812 fullHeader.writeUInt32LE(rsrcSectionVirtualSize, section.ptr + 8); // virtualSize
1813 fullHeader.writeUInt32LE(rsrcSectionRawSize, section.ptr + 16); // rawSize
1814
1815 // Set the virtual address of the section
1816 fullHeader.writeUInt32LE(virtualAddress, section.ptr + 12); // Virtual address
1817 newSections[i].virtualAddr = virtualAddress;
1818 var virtualAddressPadding = (rsrcSectionVirtualSize % 4096);
1819 virtualAddress += rsrcSectionVirtualSize;
1820 if (virtualAddressPadding != 0) { virtualAddress += (4096 - virtualAddressPadding); }
1821 } else {
1822 // Change the location of any other section if located after the resource section
1823 if (section.rawAddr > resPtr) { fullHeader.writeUInt32LE(section.rawAddr + resDeltaSize, section.ptr + 20); }
1824
1825 // Set the virtual address of the section
1826 fullHeader.writeUInt32LE(virtualAddress, section.ptr + 12); // Virtual address
1827 newSections[i].virtualAddr = virtualAddress;
1828 var virtualAddressPadding = (section.virtualSize % 4096);
1829 virtualAddress += section.virtualSize;
1830 if (virtualAddressPadding != 0) { virtualAddress += (4096 - virtualAddressPadding); }
1831 }
1832 }
1833
1834 // Make change to the data directories header to fix resource segment size and add/remove signature
1835 const pePlusOffset = (obj.header.pe32plus == 0) ? 0 : 16; // This header is the same for 32 and 64 bit, but 64bit is offset by 16 bytes.
1836 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.exportTable.addr), obj.header.peOptionalHeaderLocation + 96 + pePlusOffset);
1837 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.importTable.addr), obj.header.peOptionalHeaderLocation + 104 + pePlusOffset);
1838 fullHeader.writeUInt32LE(rsrcSectionVirtualSize, obj.header.peOptionalHeaderLocation + 116 + pePlusOffset); // Change the resource segment size
1839 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.exceptionTableAddr.addr), obj.header.peOptionalHeaderLocation + 120 + pePlusOffset);
1840 fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 128 + pePlusOffset); // certificate table addr (TODO)
1841 fullHeader.writeUInt32LE(0, obj.header.peOptionalHeaderLocation + 132 + pePlusOffset); // certificate table size (TODO)
1842 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.baseRelocationTable.addr), obj.header.peOptionalHeaderLocation + 136 + pePlusOffset);
1843 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.debug.addr), obj.header.peOptionalHeaderLocation + 144 + pePlusOffset);
1844 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.globalPtr.addr), obj.header.peOptionalHeaderLocation + 160 + pePlusOffset);
1845 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.tLSTable.addr), obj.header.peOptionalHeaderLocation + 168 + pePlusOffset);
1846 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.loadConfigTable.addr), obj.header.peOptionalHeaderLocation + 176 + pePlusOffset);
1847 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.boundImport.addr), obj.header.peOptionalHeaderLocation + 184 + pePlusOffset);
1848 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.iAT.addr), obj.header.peOptionalHeaderLocation + 192 + pePlusOffset);
1849 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.delayImportDescriptor.addr), obj.header.peOptionalHeaderLocation + 200 + pePlusOffset);
1850 fullHeader.writeUInt32LE(correctDirectoryValue(oldSections, newSections, obj.header.dataDirectories.clrRuntimeHeader.addr), obj.header.peOptionalHeaderLocation + 208 + pePlusOffset);
1851
1852 // Write size of image. We put the next virtual address.
1853 fullHeader.writeUInt32LE(virtualAddress, obj.header.peOptionalHeaderLocation + 56); // sizeOfImage
1854
1855 // Write the entire header to the destination file
1856 //console.log('Write header', fullHeader.length, written);
1857 fs.writeSync(output, fullHeader);
1858 written += fullHeader.length;
1859
1860 // Write the entire executable until the start to the resource segment
1861 var totalWrite = resPtr;
1862 //console.log('Write until res', totalWrite, written);
1863 while ((totalWrite - written) > 0) {
1864 tmp = readFileSlice(written, Math.min(totalWrite - written, 65536));
1865 fs.writeSync(output, tmp);
1866 written += tmp.length;
1867 }
1868
1869 // Write the new resource section
1870 fs.writeSync(output, rsrcSection);
1871 written += rsrcSection.length;
1872 //console.log('Write res', rsrcSection.length, written);
1873
1874 // Write until the signature block
1875 if (obj.header.sigpos > 0) {
1876 // Since the original file was signed, write from the end of the resources to the start of the signature block.
1877 totalWrite = obj.header.sigpos + resDeltaSize;
1878 } else {
1879 // The original file was not signed, write from the end of the resources to the end of the file.
1880 totalWrite = obj.filesize + resDeltaSize;
1881 }
1882
1883 //console.log('Write until signature', totalWrite, written);
1884 while ((totalWrite - written) > 0) {
1885 tmp = readFileSlice(written - resDeltaSize, Math.min(totalWrite - written, 65536));
1886 fs.writeSync(output, tmp);
1887 written += tmp.length;
1888 }
1889 //console.log('Write to signature', written);
1890
1891 // Write the signature if needed
1892 if (cert != null) {
1893 //if (cert == null) { cert = createSelfSignedCert({ cn: 'Test' }); }
1894
1895 // Set the hash algorithm hash OID
1896 var hashOid = null, fileHash = null;
1897 if (args.hash == null) { args.hash = 'sha384'; }
1898 if (args.hash == 'sha256') { hashOid = forge.pki.oids.sha256; fileHash = obj.getHashOfFile(output, 'sha256', written); }
1899 if (args.hash == 'sha384') { hashOid = forge.pki.oids.sha384; fileHash = obj.getHashOfFile(output, 'sha384', written); }
1900 if (args.hash == 'sha512') { hashOid = forge.pki.oids.sha512; fileHash = obj.getHashOfFile(output, 'sha512', written); }
1901 if (args.hash == 'sha224') { hashOid = forge.pki.oids.sha224; fileHash = obj.getHashOfFile(output, 'sha224', written); }
1902 if (args.hash == 'md5') { hashOid = forge.pki.oids.md5; fileHash = obj.getHashOfFile(output, 'md5', written); }
1903 if (hashOid == null) { func('Bad hash method OID'); return; }
1904
1905 // Create the signature block
1906 var xp7 = forge.pkcs7.createSignedData();
1907 var content = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.15').data }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000', 'bitStringContents': '\u0000', 'original': { 'tagClass': 0, 'type': 3, 'constructed': false, 'composed': false, 'value': '\u0000' } }, { 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 2, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': '' }] }] }] }] }, { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 0, 'type': 6, 'constructed': false, 'composed': false, 'value': forge.asn1.oidToDer(hashOid).data }, { 'tagClass': 0, 'type': 5, 'constructed': false, 'composed': false, 'value': '' }] }, { 'tagClass': 0, 'type': 4, 'constructed': false, 'composed': false, 'value': fileHash.toString('binary') }] }] };
1908 xp7.contentInfo = forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.SEQUENCE, true, [forge.asn1.create(forge.asn1.Class.UNIVERSAL, forge.asn1.Type.OID, false, forge.asn1.oidToDer('1.3.6.1.4.1.311.2.1.4').getBytes())]);
1909 xp7.contentInfo.value.push(forge.asn1.create(forge.asn1.Class.CONTEXT_SPECIFIC, 0, true, [content]));
1910 xp7.content = {}; // We set .contentInfo and have .content empty to bypass node-forge limitation on the type of content it can sign.
1911 xp7.addCertificate(cert.cert);
1912 if (cert.extraCerts) { for (var i = 0; i < cert.extraCerts.length; i++) { xp7.addCertificate(cert.extraCerts[0]); } } // Add any extra certificates that form the cert chain
1913
1914 // Build authenticated attributes
1915 var authenticatedAttributes = [
1916 { type: forge.pki.oids.contentType, value: forge.pki.oids.data },
1917 { type: forge.pki.oids.messageDigest } // This value will populated at signing time by node-forge
1918 ]
1919 if ((typeof args.desc == 'string') || (typeof args.url == 'string')) {
1920 var codeSigningAttributes = { 'tagClass': 0, 'type': 16, 'constructed': true, 'composed': true, 'value': [] };
1921 if (args.desc != null) { // Encode description as big-endian unicode.
1922 var desc = "", ucs = Buffer.from(args.desc, 'ucs2').toString()
1923 for (var k = 0; k < ucs.length; k += 2) { desc += String.fromCharCode(ucs.charCodeAt(k + 1), ucs.charCodeAt(k)); }
1924 codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 0, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': desc }] });
1925 }
1926 if (args.url != null) { codeSigningAttributes.value.push({ 'tagClass': 128, 'type': 1, 'constructed': true, 'composed': true, 'value': [{ 'tagClass': 128, 'type': 0, 'constructed': false, 'composed': false, 'value': args.url }] }); }
1927 authenticatedAttributes.push({ type: obj.Oids.SPC_SP_OPUS_INFO_OBJID, value: codeSigningAttributes });
1928 }
1929
1930 // Add the signer and sign
1931 xp7.addSigner({
1932 key: cert.key,
1933 certificate: cert.cert,
1934 digestAlgorithm: forge.pki.oids.sha384,
1935 authenticatedAttributes: authenticatedAttributes
1936 });
1937 xp7.sign();
1938 var p7signature = Buffer.from(forge.pkcs7.messageToPem(xp7).split('-----BEGIN PKCS7-----')[1].split('-----END PKCS7-----')[0], 'base64');
1939 //console.log('Signature', Buffer.from(p7signature, 'binary').toString('base64'));
1940
1941 if (args.time == null) {
1942 // Write the signature block to the output executable without time stamp
1943 writeExecutableEx(output, p7signature, written, func);
1944 } else {
1945 // Decode the signature block
1946 var pkcs7der = null;
1947 try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
1948
1949 // To work around ForgeJS PKCS#7 limitation, this may break PKCS7 verify if ForgeJS adds support for it in the future
1950 // Switch content type from "1.3.6.1.4.1.311.2.1.4" to "1.2.840.113549.1.7.1"
1951 pkcs7der.value[1].value[0].value[2].value[0].value = forge.asn1.oidToDer(forge.pki.oids.data).data;
1952
1953 // Decode the PKCS7 message
1954 var pkcs7 = p7.messageFromAsn1(pkcs7der);
1955
1956 // Create the timestamp request in DER format
1957 const asn1 = forge.asn1;
1958 const pkcs7dataOid = asn1.oidToDer('1.2.840.113549.1.7.1').data;
1959 const microsoftCodeSigningOid = asn1.oidToDer('1.3.6.1.4.1.311.3.2.1').data;
1960 const asn1obj =
1961 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1962 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, microsoftCodeSigningOid),
1963 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
1964 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, pkcs7dataOid),
1965 asn1.create(asn1.Class.CONTEXT_SPECIFIC, 0, true, [
1966 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OCTETSTRING, false, pkcs7.rawCapture.signature.toString('binary')) // Signature here
1967 ])
1968 ])
1969 ]);
1970
1971 // Re-decode the PKCS7 from the executable, this time, no workaround needed
1972 try { pkcs7der = forge.asn1.fromDer(forge.util.createBuffer(p7signature)); } catch (ex) { func('' + ex); return; }
1973
1974 // Serialize an ASN.1 object to DER format in Base64
1975 const requestBody = Buffer.from(asn1.toDer(asn1obj).data, 'binary').toString('base64');
1976
1977 // Make an HTTP request
1978 const options = { url: args.time, proxy: args.proxy };
1979
1980 // Make a request to the time server
1981 httpRequest(options, requestBody, function (err, data) {
1982 if (err != null) { func(err); return; }
1983
1984 // Decode the timestamp signature block
1985 var timepkcs7der = null;
1986 try { timepkcs7der = forge.asn1.fromDer(forge.util.createBuffer(Buffer.from(data, 'base64').toString('binary'))); } catch (ex) { func("Unable to parse time-stamp response: " + ex); return; }
1987
1988 // Get the ASN1 certificates used to sign the timestamp and add them to the certs in the PKCS7 of the executable
1989 // TODO: We could look to see if the certificate is already present in the executable
1990 try {
1991 var timeasn1Certs = timepkcs7der.value[1].value[0].value[3].value;
1992 for (var i in timeasn1Certs) { pkcs7der.value[1].value[0].value[3].value.push(timeasn1Certs[i]); }
1993
1994 // Get the time signature and add it to the executables PKCS7
1995 const timeasn1Signature = timepkcs7der.value[1].value[0].value[4];
1996 const countersignatureOid = asn1.oidToDer('1.2.840.113549.1.9.6').data;
1997 const asn1obj2 =
1998 asn1.create(asn1.Class.CONTEXT_SPECIFIC, 1, true, [
1999 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.SEQUENCE, true, [
2000 asn1.create(asn1.Class.UNIVERSAL, asn1.Type.OID, false, countersignatureOid),
2001 timeasn1Signature
2002 ])
2003 ]);
2004 pkcs7der.value[1].value[0].value[4].value[0].value.push(asn1obj2);
2005
2006 // Re-encode the executable signature block
2007 const p7signature = Buffer.from(forge.asn1.toDer(pkcs7der).data, 'binary');
2008
2009 // Write the file with the signature block
2010 writeExecutableEx(output, p7signature, written, func);
2011 } catch (ex) { func('' + ex); return; } // Something failed
2012 });
2013 }
2014 return;
2015 }
2016
2017 // Close the file
2018 fs.closeSync(output);
2019
2020 // Indicate success
2021 func(null);
2022 }
2023
2024 function writeExecutableEx(output, p7signature, written, func) {
2025 // Quad Align the results, adding padding if necessary
2026 var len = written + p7signature.length;
2027 var padding = (8 - ((len) % 8)) % 8;
2028
2029 // Write the signature block header and signature
2030 var win = Buffer.alloc(8); // WIN CERTIFICATE Structure
2031 win.writeUInt32LE(p7signature.length + padding + 8); // DWORD length
2032 win.writeUInt16LE(512, 4); // WORD revision
2033 win.writeUInt16LE(2, 6); // WORD type
2034 fs.writeSync(output, win);
2035 fs.writeSync(output, p7signature);
2036 if (padding > 0) { fs.writeSync(output, Buffer.alloc(padding, 0)); }
2037
2038 // Write the signature header
2039 var addresstable = Buffer.alloc(8);
2040 addresstable.writeUInt32LE(written);
2041 addresstable.writeUInt32LE(8 + p7signature.length + padding, 4);
2042 var signatureHeaderLocation = (obj.header.peHeaderLocation + 152 + (obj.header.pe32plus * 16));
2043 fs.writeSync(output, addresstable, 0, 8, signatureHeaderLocation);
2044 written += (p7signature.length + padding + 8); // Add the signature block to written counter
2045
2046 // Compute the checksum and write it in the PE header checksum location
2047 var tmp = Buffer.alloc(4);
2048 tmp.writeUInt32LE(runChecksumOnFile(output, written, ((obj.header.peOptionalHeaderLocation + 64) / 4)));
2049 fs.writeSync(output, tmp, 0, 4, obj.header.peOptionalHeaderLocation + 64);
2050
2051 // Close the file
2052 fs.closeSync(output);
2053
2054 // Indicate success
2055 func(null);
2056 }
2057
2058 // Return null if we could not open the file
2059 return (openFile() ? obj : null);
2060}
2061
2062function start() {
2063 // Parse the arguments
2064 const args = require('minimist')(process.argv.slice(2));
2065
2066 // Show tool help
2067 if (process.argv.length < 3) {
2068 console.log("MeshCentral Authenticode Tool.");
2069 console.log("Usage:");
2070 console.log(" node authenticode.js [command] [options]");
2071 console.log("Commands:");
2072 console.log(" info: Show information about an executable.");
2073 console.log(" --exe [file] Required executable to view information.");
2074 console.log(" --json Show information in JSON format.");
2075 console.log(" sign: Sign an executable.");
2076 console.log(" --exe [file] Required executable to sign.");
2077 console.log(" --out [file] Resulting signed executable.");
2078 console.log(" --pem [pemfile] Certificate & private key to sign the executable with.");
2079 console.log(" --desc [description] Description string to embbed into signature.");
2080 console.log(" --url [url] URL to embbed into signature.");
2081 console.log(" --hash [method] Default is SHA384, possible value: MD5, SHA224, SHA256, SHA384 or SHA512.");
2082 console.log(" --time [url] The time signing server URL.");
2083 console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
2084 console.log(" unsign: Remove the signature from the executable.");
2085 console.log(" --exe [file] Required executable to un-sign.");
2086 console.log(" --out [file] Resulting executable with signature removed.");
2087 console.log(" createcert: Create a code signging self-signed certificate and key.");
2088 console.log(" --out [pemfile] Required certificate file to create.");
2089 console.log(" --cn [value] Required certificate common name.");
2090 console.log(" --country [value] Certificate country name.");
2091 console.log(" --state [value] Certificate state name.");
2092 console.log(" --locality [value] Certificate locality name.");
2093 console.log(" --org [value] Certificate organization name.");
2094 console.log(" --ou [value] Certificate organization unit name.");
2095 console.log(" --serial [value] Certificate serial number.");
2096 console.log(" timestamp: Add a signed timestamp to an already signed executable.");
2097 console.log(" --exe [file] Required executable to timestamp.");
2098 console.log(" --out [file] Resulting signed executable.");
2099 console.log(" --time [url] The time signing server URL.");
2100 console.log(" --proxy [url] The HTTP proxy to use to contact the time signing server, must start with http://");
2101 console.log(" bitmaps: Show bitmap resources in the executable.");
2102 console.log(" --exe [file] Input executable.");
2103 console.log(" savebitmap: Save a single bitmap to a .bmp file.");
2104 console.log(" --exe [file] Input executable.");
2105 console.log(" --out [file] Resulting .ico file.");
2106 console.log(" --bitmap [number] Bitmap number to save to file.");
2107 console.log(" icons: Show the icon resources in the executable.");
2108 console.log(" --exe [file] Input executable.");
2109 console.log(" saveicon: Save a single icon bitmap to a .ico file.");
2110 console.log(" --exe [file] Input executable.");
2111 console.log(" --out [file] Resulting .ico file.");
2112 console.log(" --icon [number] Icon number to save to file.");
2113 console.log(" saveicons: Save an icon group to a .ico file.");
2114 console.log(" --exe [file] Input executable.");
2115 console.log(" --out [file] Resulting .ico file.");
2116 console.log(" --icongroup [groupNumber] Icon groupnumber to save to file.");
2117 console.log("");
2118 console.log("Note that certificate PEM files must first have the signing certificate,");
2119 console.log("followed by all certificates that form the trust chain.");
2120 console.log("");
2121 console.log("When doing sign/unsign, you can also change resource properties of the generated file.");
2122 console.log("");
2123 console.log(" --fileversionnumber n.n.n.n");
2124 console.log(" --productversionnumber n.n.n.n");
2125 console.log(" --filedescription [value]");
2126 console.log(" --fileversion [value]");
2127 console.log(" --internalname [value]");
2128 console.log(" --legalcopyright [value]");
2129 console.log(" --originalfilename [value]");
2130 console.log(" --productname [value]");
2131 console.log(" --productversion [value]");
2132 console.log(" --removeicongroup [number]");
2133 console.log(" --removebitmap [number]");
2134 console.log(" --icon [groupNumber],[filename.ico]");
2135 console.log(" --bitmap [number],[filename.bmp]");
2136 return;
2137 }
2138
2139 // Check that a valid command is passed in
2140 if (['info', 'sign', 'unsign', 'createcert', 'icons', 'bitmaps', 'saveicon', 'saveicons', 'savebitmap', 'header', 'sections', 'timestamp', 'signblock'].indexOf(process.argv[2].toLowerCase()) == -1) {
2141 console.log("Invalid command: " + process.argv[2]);
2142 console.log("Valid commands are: info, sign, unsign, createcert, timestamp");
2143 return;
2144 }
2145
2146 var exe = null;
2147 if (args.exe) {
2148 // Check the file exists and open the file
2149 var stats = null;
2150 try { stats = require('fs').statSync(args.exe); } catch (ex) { }
2151 if (stats == null) { console.log("Unable to executable open file: " + args.exe); return; }
2152 exe = createAuthenticodeHandler(args.exe);
2153 if (exe == null) { console.log("Unable to parse executable file: " + args.exe); return; }
2154 }
2155
2156 // Parse the string resources and make any required changes
2157 var resChanges = false, versionStrings = null;
2158 if (exe != null) {
2159 versionStrings = exe.getVersionInfo();
2160 var versionProperties = ['FileDescription', 'FileVersion', 'InternalName', 'LegalCopyright', 'OriginalFilename', 'ProductName', 'ProductVersion'];
2161 for (var i in versionProperties) {
2162 const prop = versionProperties[i], propl = prop.toLowerCase();
2163 if (args[propl] && (args[propl] != versionStrings[prop])) { versionStrings[prop] = args[propl]; resChanges = true; }
2164 }
2165 if (args['fileversionnumber'] != null) {
2166 const fileVerSplit = args['fileversionnumber'].split('.');
2167 if (fileVerSplit.length != 4) { console.log("--fileversionnumber must be of format n.n.n.n, for example: 1.2.3.4"); return; }
2168 for (var i in fileVerSplit) { var n = parseInt(fileVerSplit[i]); if ((n < 0) || (n > 65535)) { console.log("--fileversionnumber numbers must be between 0 and 65535."); return; } }
2169 if (args['fileversionnumber'] != versionStrings['~FileVersion']) { versionStrings['~FileVersion'] = args['fileversionnumber']; resChanges = true; }
2170 }
2171 if (args['productversionnumber'] != null) {
2172 const productVerSplit = args['productversionnumber'].split('.');
2173 if (productVerSplit.length != 4) { console.log("--productversionnumber must be of format n.n.n.n, for example: 1.2.3.4"); return; }
2174 for (var i in productVerSplit) { var n = parseInt(productVerSplit[i]); if ((n < 0) || (n > 65535)) { console.log("--productversionnumber numbers must be between 0 and 65535."); return; } }
2175 if (args['productversionnumber'] != versionStrings['~ProductVersion']) { versionStrings['~ProductVersion'] = args['productversionnumber']; resChanges = true; }
2176 }
2177 if (resChanges == true) { exe.setVersionInfo(versionStrings); }
2178 }
2179
2180 // Parse the icon changes
2181 resChanges = false;
2182 var icons = null, bitmaps = null;
2183 if (exe != null) {
2184 icons = exe.getIconInfo();
2185 bitmaps = exe.getBitmapInfo();
2186 if (typeof args['removeicongroup'] == 'string') { // If --removeicongroup is used, it's to remove an existing icon group
2187 const groupsToRemove = args['removeicongroup'].split(',');
2188 for (var i in groupsToRemove) { if (icons[groupsToRemove[i]] != null) { delete icons[groupsToRemove[i]]; resChanges = true; } }
2189 } else if (typeof args['removeicongroup'] == 'number') {
2190 if (icons[args['removeicongroup']] != null) { delete icons[args['removeicongroup']]; resChanges = true; }
2191 }
2192 if (typeof args['removebitmap'] == 'string') { // If --removebitmap is used
2193 const bitmapsToRemove = args['removebitmap'].split(',');
2194 for (var i in bitmapsToRemove) { if (bitmaps[bitmapsToRemove[i]] != null) { delete bitmaps[bitmapsToRemove[i]]; resChanges = true; } }
2195 } else if (typeof args['removebitmap'] == 'number') {
2196 if (bitmaps[args['removebitmap']] != null) { delete bitmaps[args['removebitmap']]; resChanges = true; }
2197 }
2198 if (typeof args['icon'] == 'string') { // If --icon is used, it's to add or replace an existing icon group
2199 const iconToAddSplit = args['icon'].split(',');
2200 if (iconToAddSplit.length != 2) { console.log("The --icon format is: --icon [number],[file]."); return; }
2201 const iconName = parseInt(iconToAddSplit[0]);
2202 const iconFile = iconToAddSplit[1];
2203 const icon = loadIcon(iconFile);
2204 if (icon == null) { console.log("Unable to load icon: " + iconFile); return; }
2205 if (icons[iconName] != null) {
2206 const iconHash = hashObject(icon); // Compute the new icon group hash
2207 const iconHash2 = hashObject(icons[iconName]); // Computer the old icon group hash
2208 if (iconHash != iconHash2) { icons[iconName] = icon; resChanges = true; } // If different, replace the icon group
2209 } else {
2210 icons[iconName] = icon; // We are adding an icon group
2211 resChanges = true;
2212 }
2213 }
2214 if (typeof args['bitmap'] == 'string') { // If --bitmap is used, it's to add or replace an existing bitmap
2215 const bitmapToAddSplit = args['bitmap'].split(',');
2216 if (bitmapToAddSplit.length != 2) { console.log("The --bitmap format is: --bitmap [number],[file]."); return; }
2217 const bitmapName = parseInt(bitmapToAddSplit[0]);
2218 const bitmapFile = bitmapToAddSplit[1];
2219 const bitmap = loadBitmap(bitmapFile);
2220 if (bitmap == null) { console.log("Unable to load bitmap: " + bitmapFile); return; }
2221 if (bitmaps[bitmapName] != null) {
2222 const bitmapHash = hashObject(bitmap); // Compute the new bitmap hash
2223 const bitmapHash2 = hashObject(bitmaps[bitmapName]); // Computer the old bitmap hash
2224 if (bitmapHash != bitmapHash2) { bitmaps[bitmapName] = bitmap; resChanges = true; } // If different, replace the new bitmap
2225 } else {
2226 bitmaps[bitmapName] = bitmap; // We are adding an new bitmap
2227 resChanges = true;
2228 }
2229 }
2230 if (resChanges == true) {
2231 exe.setIconInfo(icons);
2232 exe.setBitmapInfo(bitmaps);
2233 }
2234 }
2235
2236 // Execute the command
2237 var command = process.argv[2].toLowerCase();
2238 if (command == 'info') { // Get signature information about an executable
2239 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2240 if (args.json) {
2241 var r = {}, stringInfo = exe.getVersionInfo();
2242 if (stringInfo != null) {
2243 r.versionInfo = {};
2244 r.stringInfo = {};
2245 for (var i in stringInfo) { if (i.startsWith('~')) { r.versionInfo[i.substring(1)] = stringInfo[i]; } else { r.stringInfo[i] = stringInfo[i]; } }
2246 }
2247 if (exe.fileHashAlgo != null) {
2248 r.signture = {};
2249 if (exe.fileHashAlgo != null) { r.signture.hashMethod = exe.fileHashAlgo; }
2250 if (exe.fileHashSigned != null) { r.signture.hashSigned = exe.fileHashSigned.toString('hex'); }
2251 if (exe.fileHashActual != null) { r.signture.hashActual = exe.fileHashActual.toString('hex'); }
2252 if (exe.signingAttribs && exe.signingAttribs.length > 0) { r.signture.attributes = exe.signingAttribs; }
2253 }
2254 console.log(JSON.stringify(r, null, 2));
2255 } else {
2256 var versionInfo = exe.getVersionInfo();
2257 if (versionInfo != null) {
2258 console.log("Version Information:");
2259 for (var i in versionInfo) { if (i.startsWith('~') == true) { console.log(' ' + i.substring(1) + ': ' + versionInfo[i] + ''); } }
2260 console.log("String Information:");
2261 for (var i in versionInfo) { if (i.startsWith('~') == false) { if (versionInfo[i] == null) { console.log(' ' + i + ': (Empty)'); } else { console.log(' ' + i + ': \"' + versionInfo[i] + '\"'); } } }
2262 }
2263 console.log("Checksum Information:");
2264 console.log(" Header CheckSum: 0x" + exe.header.peWindows.checkSum.toString(16));
2265 console.log(" Actual CheckSum: 0x" + exe.header.peWindows.checkSumActual.toString(16));
2266 console.log("Signature Information:");
2267 if (exe.fileHashAlgo != null) {
2268 console.log(" Hash Method:", exe.fileHashAlgo);
2269 if (exe.fileHashSigned != null) { console.log(" Signed Hash:", exe.fileHashSigned.toString('hex')); }
2270 if (exe.fileHashActual != null) { console.log(" Actual Hash:", exe.fileHashActual.toString('hex')); }
2271 } else {
2272 console.log(" This file is not signed.");
2273 }
2274 if (exe.signingAttribs && exe.signingAttribs.length > 0) { console.log("Signature Attributes:"); for (var i in exe.signingAttribs) { console.log(' ' + exe.signingAttribs[i]); } }
2275 }
2276 }
2277 if (command == 'header') { // Display the full executable header in JSON format
2278 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2279 console.log(exe.header);
2280 // Check that the header is valid
2281 var ptr = 1024, sizeOfCode = 0, sizeOfInitializedData = 0;
2282 for (var i in exe.header.sections) {
2283 if (i == '.text') { sizeOfCode += exe.header.sections[i].rawSize; } else { sizeOfInitializedData += exe.header.sections[i].rawSize; }
2284 if (exe.header.sections[i].rawAddr != ptr) { console.log('WARNING: ' + i + ' section should have a rawAddr or ' + ptr + ', but has ' + exe.header.sections[i].rawAddr + ' instead.'); }
2285 ptr += exe.header.sections[i].rawSize;
2286 }
2287 if (exe.header.peStandard.sizeOfCode != sizeOfCode) { console.log('WARNING: Size of code is ' + exe.header.peStandard.sizeOfCode + ', should be ' + sizeOfCode + '.'); }
2288 if (exe.header.peStandard.sizeOfInitializedData != sizeOfInitializedData) { console.log('WARNING: Size of initialized data is ' + exe.header.peStandard.sizeOfInitializedData + ', should be ' + sizeOfInitializedData + '.'); }
2289 }
2290 if (command == 'sections') { // Display sections in CSV format
2291 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2292 var csvHeader = 'section';
2293 for (var i in exe.header.sections['.text']) { csvHeader += ',' + i; }
2294 console.log(csvHeader);
2295 for (var i in exe.header.sections) {
2296 var csvData = i;
2297 for (var j in exe.header.sections[i]) { csvData += ',' + exe.header.sections[i][j]; }
2298 console.log(csvData);
2299 }
2300 }
2301 if (command == 'sign') { // Sign an executable
2302 if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; }
2303 if (typeof args.hash == 'string') { args.hash = args.hash.toLowerCase(); if (['md5', 'sha224', 'sha256', 'sha384', 'sha512'].indexOf(args.hash) == -1) { console.log("Invalid hash method, must be SHA256 or SHA384"); return; } }
2304 if (args.hash == null) { args.hash = 'sha384'; }
2305 createOutFile(args, args.exe);
2306 var cert = loadCertificates(args.pem);
2307 if (cert == null) { console.log("Unable to load certificate and/or private key, generating test certificate."); cert = createSelfSignedCert({ cn: 'Test' }); }
2308 if (resChanges == false) {
2309 console.log("Signing to " + args.out);
2310 exe.sign(cert, args, function (err) { // Simple signing, copy most of the original file.
2311 if (err == null) { console.log("Done."); } else { console.log(err); }
2312 if (exe != null) { exe.close(); }
2313 });
2314 return;
2315 } else {
2316 console.log("Changing resources and signing to " + args.out);
2317 exe.writeExecutable(args, cert, function (err) { // Signing with resources decoded and re-encoded.
2318 if (err == null) { console.log("Done."); } else { console.log(err); }
2319 if (exe != null) { exe.close(); }
2320 });
2321 return;
2322 }
2323 }
2324 if (command == 'unsign') { // Unsign an executable
2325 if (typeof args.exe != 'string') { console.log("Missing --exe [filename]"); return; }
2326 createOutFile(args, args.exe);
2327 if (resChanges == false) {
2328 if (exe.header.signed) {
2329 console.log("Unsigning to " + args.out);
2330 exe.unsign(args); // Simple unsign, copy most of the original file.
2331 console.log("Done.");
2332 } else {
2333 console.log("Executable is not signed.");
2334 }
2335 } else {
2336 console.log("Changing resources and unsigning to " + args.out);
2337 exe.writeExecutable(args, null, function (err) { // Unsigning with resources decoded and re-encoded.
2338 if (err == null) { console.log("Done."); } else { console.log(err); }
2339 if (exe != null) { exe.close(); }
2340 });
2341 }
2342 }
2343 if (command == 'createcert') { // Create a code signing certificate and private key
2344 if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
2345 if (typeof args.cn != 'string') { console.log("Missing --cn [name]"); return; }
2346 if (typeof args.serial == 'string') { if (args.serial != parseInt(args.serial)) { console.log("Invalid serial number."); return; } else { args.serial = parseInt(args.serial); } }
2347 if (typeof args.serial == 'number') { args.serial = '0' + args.serial; } // Serial number must be a integer string with a single leading '0'
2348 const cert = createSelfSignedCert(args);
2349 console.log("Writing to " + args.out);
2350 fs.writeFileSync(args.out, pki.certificateToPem(cert.cert) + '\r\n' + pki.privateKeyToPem(cert.key));
2351 console.log("Done.");
2352 }
2353 if (command == 'bitmaps') { // Show bitmaps in the executable
2354 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2355 if (args.json) {
2356 var bitmapInfo = exe.getBitmapInfo();
2357 console.log(JSON.stringify(bitmapInfo, null, 2));
2358 } else {
2359 var bitmapInfo = exe.getBitmapInfo();
2360 if (bitmapInfo != null) {
2361 console.log("Bitmap Information:");
2362 for (var i in bitmapInfo) { console.log(' ' + i + ': ' + bitmapInfo[i].length + ' byte' + ((bitmapInfo[i].length > 1) ? 's' : '') + '.'); }
2363 }
2364 }
2365 }
2366 if (command == 'savebitmap') { // Save an bitmap to file
2367 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2368 if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
2369 if (typeof args.bitmap != 'number') { console.log("Missing or incorrect --bitmap [number]"); return; }
2370 const bitmapInfo = exe.getBitmapInfo();
2371 if (bitmapInfo[args.bitmap] == null) { console.log("Unknown bitmap: " + args.bitmap); return; }
2372
2373 console.log("Writing to " + args.out);
2374 var bitmapHeader = Buffer.from('424D000000000000000036000000', 'hex');
2375 bitmapHeader.writeUInt32LE(14 + bitmapInfo[args.bitmap].length, 2); // Write the full size of the bitmap file
2376 fs.writeFileSync(args.out, Buffer.concat([bitmapHeader, bitmapInfo[args.bitmap]]));
2377 console.log("Done.");
2378 }
2379 if (command == 'icons') { // Show icons in the executable
2380 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2381 if (args.json) {
2382 var r = {}, iconInfo = exe.getIconInfo();
2383 if (iconInfo != null) { r.iconInfo = iconInfo; }
2384 console.log(JSON.stringify(r, null, 2));
2385 } else {
2386 var iconInfo = exe.getIconInfo();
2387 if (iconInfo != null) {
2388 console.log("Icon Information:");
2389 for (var i in iconInfo) { console.log(' Group ' + i + ':'); for (var j in iconInfo[i].icons) { console.log(' Icon ' + j + ': ' + ((iconInfo[i].icons[j].width == 0) ? 256 : iconInfo[i].icons[j].width) + 'x' + ((iconInfo[i].icons[j].height == 0) ? 256 : iconInfo[i].icons[j].height) + ', size: ' + iconInfo[i].icons[j].icon.length); } }
2390 }
2391 }
2392 }
2393 if (command == 'saveicon') { // Save an icon to file
2394 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2395 if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
2396 if (typeof args.icon != 'number') { console.log("Missing or incorrect --icon [number]"); return; }
2397 const iconInfo = exe.getIconInfo();
2398 var icon = null;
2399 for (var i in iconInfo) { if (iconInfo[i].icons[args.icon]) { icon = iconInfo[i].icons[args.icon]; } }
2400 if (icon == null) { console.log("Unknown icon: " + args.icon); return; }
2401
2402 // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format)
2403 var buf = Buffer.alloc(22);
2404 buf.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor
2405 buf.writeUInt16LE(1, 4); // Icon Count, always 1 in our case
2406 buf[6] = icon.width; // Width (0 = 256)
2407 buf[7] = icon.height; // Height (0 = 256)
2408 buf[8] = icon.colorCount; // Colors
2409 buf.writeUInt16LE(icon.planes, 10); // Color planes
2410 buf.writeUInt16LE(icon.bitCount, 12); // Bits per pixel
2411 buf.writeUInt32LE(icon.icon.length, 14); // Size
2412 buf.writeUInt32LE(22, 18); // Offset, always 22 in our case
2413
2414 console.log("Writing to " + args.out);
2415 fs.writeFileSync(args.out, Buffer.concat([buf, icon.icon]));
2416 console.log("Done.");
2417 }
2418 if (command == 'saveicons') { // Save an icon group to file
2419 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2420 if (typeof args.out != 'string') { console.log("Missing --out [filename]"); return; }
2421 if (typeof args.icongroup != 'number') { console.log("Missing or incorrect --icongroup [number]"); return; }
2422 const iconInfo = exe.getIconInfo();
2423 const iconGroup = iconInfo[args.icongroup];
2424 if (iconGroup == null) { console.log("Invalid or incorrect --icongroup [number]"); return; }
2425
2426 // Count the number of icons in the group
2427 var iconCount = 0;
2428 for (var i in iconGroup.icons) { iconCount++; }
2429
2430 // .ico header: https://en.wikipedia.org/wiki/ICO_(file_format)
2431 const iconFileData = [];
2432 const header = Buffer.alloc(6);
2433 header.writeUInt16LE(1, 2); // 1 = Icon, 2 = Cursor
2434 header.writeUInt16LE(iconCount, 4); // Icon Count, always 1 in our case
2435 iconFileData.push(header);
2436
2437 // Store each icon header
2438 var offsetPtr = 6 + (16 * iconCount);
2439 for (var i in iconGroup.icons) {
2440 const buf = Buffer.alloc(16);
2441 buf[0] = iconGroup.icons[i].width; // Width (0 = 256)
2442 buf[1] = iconGroup.icons[i].height; // Height (0 = 256)
2443 buf[2] = iconGroup.icons[i].colorCount; // Colors
2444 buf.writeUInt16LE(iconGroup.icons[i].planes, 4); // Color planes
2445 buf.writeUInt16LE(iconGroup.icons[i].bitCount, 6); // Bits per pixel
2446 buf.writeUInt32LE(iconGroup.icons[i].icon.length, 8); // Size
2447 buf.writeUInt32LE(offsetPtr, 12); // Offset
2448 offsetPtr += iconGroup.icons[i].icon.length;
2449 iconFileData.push(buf);
2450 }
2451
2452 // Store each icon
2453 for (var i in iconGroup.icons) { iconFileData.push(iconGroup.icons[i].icon); }
2454
2455 // Write the .ico file
2456 console.log("Writing to " + args.out);
2457 fs.writeFileSync(args.out, Buffer.concat(iconFileData));
2458 console.log("Done.");
2459 }
2460 if (command == 'signblock') { // Display the raw signature block of the executable in hex
2461 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2462 var buf = exe.getRawSignatureBlock();
2463 if (buf == null) { console.log("Executable is not signed."); return } else { console.log(buf.toString('hex')); return }
2464 }
2465 if (command == 'timestamp') {
2466 if (exe == null) { console.log("Missing --exe [filename]"); return; }
2467 if (exe.signature == null) { console.log("Executable is not signed."); return; }
2468 if (typeof args.time != 'string') { console.log("Missing --time [url]"); return; }
2469 createOutFile(args, args.exe);
2470 console.log("Requesting time signature...");
2471 exe.timeStampRequest(args, function (err) {
2472 if (err == null) { console.log("Done."); } else { console.log(err); }
2473 if (exe != null) { exe.close(); }
2474 })
2475 return;
2476 }
2477
2478 // Close the file
2479 if (exe != null) { exe.close(); }
2480}
2481
2482// If this is the main module, run the command line version
2483if (require.main === module) { start(); }
2484
2485// Exports
2486module.exports.createAuthenticodeHandler = createAuthenticodeHandler;
2487module.exports.loadCertificates = loadCertificates;
2488module.exports.loadIcon = loadIcon;
2489module.exports.loadBitmap = loadBitmap;
2490module.exports.hashObject = hashObject;