diff --git a/index.js b/index.js index 65996e2..520f345 100644 --- a/index.js +++ b/index.js @@ -5,15 +5,19 @@ const forge = require("node-forge"); const { spawn } = require("child_process"); const archiver = require("archiver"); const async = require("async"); - -let _configuration = Object.freeze(require("./config.json")); +const stream = require("stream"); const supportedTypesOfPass = /(boardingPass|eventTicket|coupon|generic|storeCard)/i; -const passModelsDir = _configuration.models.dir; -const outputDir = _configuration.output.dir; const Certificates = { status: false }; +const Configuration = { + passModelsDir: null, + output: { + shouldWrite: false, + dir: null, + } +} /** Apply a filter to arg0 to remove hidden files names (starting with dot) @@ -30,8 +34,7 @@ function capitalizeFirst(str) { return str[0].toUpperCase()+str.slice(1); } -function loadConfiguration(configurationPath) { - let setup = require(path.resolve(__dirname, configurationPath)); +function loadConfiguration(setup) { let reqFilesKeys = ["wwdr", "signerCert", "signerKey"]; // Node-Forge also accepts .cer certificates @@ -129,152 +132,67 @@ function checkSignatureRequirements() { return Promise.all([checkCertificate, checkKey]); } -/** - Generates a unique UUID - From Github Gist: https://gist.github.com/jed/982883 -*/ - -function UUIDGen(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,UUIDGen)} - /** Generates the cryptografic signature for the manifest file. Spawns Openssl process since Node.js has no support for PKCSs. - @function generateManifestSignature + @function createSignature @params {String} manifestPath - temp dir path created to keep the manifest file. @returns {Object} Promise */ -function generateManifestSignature(manifest) { -// return new Promise(function(done, rejected) { - let signature = forge.pkcs7.createSignedData(); +function createSignature(manifest) { + let signature = forge.pkcs7.createSignedData(); - if (typeof manifest === "object") { - signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8") - } else if (typeof manifest === "string") { - signature.content = manifest; - } else { - throw new Error(`Manifest content must be a string or an object. Unable to accept manifest of type ${typeof manifest}`); - } + if (typeof manifest === "object") { + signature.content = forge.util.createBuffer(JSON.stringify(manifest), "utf8") + } else if (typeof manifest === "string") { + signature.content = manifest; + } else { + throw new Error(`Manifest content must be a string or an object. Unable to accept manifest of type ${typeof manifest}`); + } - signature.addCertificate(Certificates.wwdr); - signature.addCertificate(Certificates.signerCert); + signature.addCertificate(Certificates.wwdr); + signature.addCertificate(Certificates.signerCert); - signature.addSigner({ - key: Certificates.signerKey, - certificate: Certificates.signerCert, - digestAlgorithm: forge.pki.oids.sha1, - authenticatedAttributes: [{ - type: forge.pki.oids.contentType, - value: forge.pki.oids.data - }, { - type: forge.pki.oids.messageDigest, - }, { - // the value is autogenerated - type: forge.pki.oids.signingTime, - }] - }); - - signature.sign(); - - /* - * Signing creates in contentInfo a JSON object nested BER/TLV (X.690 standard) structure. - * Each object represents a component of ASN.1 (Abstract Syntax Notation) - * For a more complete reference, refer to: https://en.wikipedia.org/wiki/X.690#BER_encoding - * - * signature.contentInfo.type => SEQUENCE OF (16) - * signature.contentInfo.value[0].type => OBJECT IDENTIFIER (6) - * signature.contantInfo.value[1].type => END OF CONTENT (EOC - 0) - * - * EOC are only present only in constructed indefinite-length methods - * Since `signature.contentInfo.value[1].value` contains an object whose value contains the content we passed, - * we have to pop the whole object away to avoid signature content invalidation. - * - */ - signature.contentInfo.value.pop(); - - // Converting the JSON Structure into a DER (which is a subset of BER), ASN.1 valid structure - // Returning the buffer of the signature -// return done(Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary')); - - return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary'); -// }); -} - - -// function generateManifestSignature(manifestUUID) { -// return new Promise(function(done, rejected) { -// checkSignatureRequirements() -// .then(function() { -// let opensslError = false; -// let opensslBuffer = []; - -// let opensslProcess = spawn("openssl", [ -// "smime", -// "-binary", -// "-sign", -// "-certfile", path.resolve(Certificates.dir, Certificates.files["wwdr_pem"]), -// "-signer", path.resolve(Certificates.dir, Certificates.files["certificate"]), -// "-inkey", path.resolve(Certificates.dir, Certificates.files["key"]), -// "-in", path.resolve(`${os.tmpdir()}/manifest-${manifestUUID}.json`), -// // "-out", path.resolve("passCreator", "event.pass", "./signature"), -// "-outform", "DER", -// "-passin", `pass:${Certificates.credentials["privateKeySecret"]}` -// ]); - -// opensslProcess.stdout.on("data", function(data) { -// opensslBuffer.push(data); -// }); - -// opensslProcess.stderr.on("data", function(data) { -// opensslBuffer.push(data); -// opensslError = true; -// }); - -// opensslProcess.stdout.on("end", function() { -// if (opensslError) { -// return rejected(Buffer.concat(opensslBuffer)); -// } - -// return done(Buffer.concat(opensslBuffer)); -// }); -// }) -// .catch(function(e) { -// return rejected(`Cannot fulfill signature requirements.\n${e}`); -// }); -// }); -// } - -/** - Generates a Buffer of JSON file (manifest) - @function generateManifest - @params {Object} fromObject - Manifest content - @params {String} manifestUUID - @return {Promise} - Promise with the manifest buffer - @see https://apple.co/2IhJr0Q (PassKit Package Structure) - @see https://apple.co/2K2aY3v (Passes Are Cryptographically Signed and Compressed) -*/ - -function generateManifest(fromObject, manifestUUID) { - return new Promise(function(done, failed) { - if (!fromObject || typeof fromObject !== "object" && typeof fromObject !== "string") { - return failed("generateManifest: Argument 0 is required and must be of an object or a string (source object)"); - } - - if (!manifestUUID || typeof manifestUUID !== "string") { - return failed("generateManifest: Argument 1 is required and must be a string (unique uuid)."); - } - - const source = typeof fromObject === "object" ? JSON.stringify(fromObject) : fromObject; - let manifestWS = fs.createWriteStream(`${os.tmpdir()}/manifest-${manifestUUID}.json`); - - manifestWS.write(source); - manifestWS.end(); - - //return done(Buffer.from(source)); - return done(Buffer.from(source).toString()); + signature.addSigner({ + key: Certificates.signerKey, + certificate: Certificates.signerCert, + digestAlgorithm: forge.pki.oids.sha1, + authenticatedAttributes: [{ + type: forge.pki.oids.contentType, + value: forge.pki.oids.data + }, { + type: forge.pki.oids.messageDigest, + }, { + // the value is autogenerated + type: forge.pki.oids.signingTime, + }] }); + + signature.sign(); + + /* + * Signing creates in contentInfo a JSON object nested BER/TLV (X.690 standard) structure. + * Each object represents a component of ASN.1 (Abstract Syntax Notation) + * For a more complete reference, refer to: https://en.wikipedia.org/wiki/X.690#BER_encoding + * + * signature.contentInfo.type => SEQUENCE OF (16) + * signature.contentInfo.value[0].type => OBJECT IDENTIFIER (6) + * signature.contantInfo.value[1].type => END OF CONTENT (EOC - 0) + * + * EOC are only present only in constructed indefinite-length methods + * Since `signature.contentInfo.value[1].value` contains an object whose value contains the content we passed, + * we have to pop the whole object away to avoid signature content invalidation. + * + */ + signature.contentInfo.value.pop(); + + // Converting the JSON Structure into a DER (which is a subset of BER), ASN.1 valid structure + // Returning the buffer of the signature + + return Buffer.from(forge.asn1.toDer(signature.toAsn1()).getBytes(), 'binary'); } /** @@ -373,8 +291,8 @@ function RequestHandler(request, response) { return; } - fs.readdir(`${passModelsDir}/${request.params.type}.pass`, function (err, files) { - /* Invalid path for passModelsDir */ + fs.readdir(`${Configuration.passModelsDir}/${request.params.type}.pass`, function (err, files) { + /* Invalid path for Configuration.passModelsDir */ if (err) { // 😊 response.set("Content-Type", "application/json"); @@ -399,7 +317,7 @@ function RequestHandler(request, response) { } let options = (request.method === "POST" ? request.body : (request.method === "GET" ? request.params : {})); - fileStreamToBuffer(`${passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) { + fileStreamToBuffer(`${Configuration.passModelsDir}/${request.params.type}.pass/pass.json`, function _returnBuffer(bufferResult) { editPassStructure(filterPassOptions(options), bufferResult).then(function _afterJSONParse(passFileBuffer) { // Manifest dictionary let manifest = {}; @@ -416,11 +334,11 @@ function RequestHandler(request, response) { } // adding the files to the zip - i'm not using .directory method because it adds also hidden files like .DS_Store on macOS - archive.file(`${passModelsDir}/${request.params.type}.pass/${file}`, { name: file }); + archive.file(`${Configuration.passModelsDir}/${request.params.type}.pass/${file}`, { name: file }); let hashFlow = forge.md.sha1.create(); - fs.createReadStream(`${passModelsDir}/${request.params.type}.pass/${file}`) + fs.createReadStream(`${Configuration.passModelsDir}/${request.params.type}.pass/${file}`) .on("data", function(data) { hashFlow.update(data.toString("binary")); }) @@ -436,47 +354,42 @@ function RequestHandler(request, response) { throw new Error(`Unable to compile manifest. ${error}`); } - // let uuid = UUIDGen(); + archive.append(Buffer.from(JSON.stringify(manifest), "utf8"), { name: "manifest.json" }); + + let signatureBuffer = createSignature(manifest); - // generateManifest(manifestRaw, uuid) - // .then(function(manifestBuffer) { + if (!fs.existsSync("output")) { + fs.mkdirSync("output"); + } - archive.append(Buffer.from(JSON.stringify(manifest), "utf8"), { name: "manifest.json" }); - - let signatureBuffer = generateManifestSignature(manifest); + archive.append(signatureBuffer, { name: "signature" }); + response.set({ + "Content-type": "application/vnd.apple.pkpass", + "Content-disposition": `attachment; filename=${request.params.type}.pkpass` + }) - console.log(signatureBuffer) - - if (!fs.existsSync("output")) { - fs.mkdirSync("output"); - } - - archive.append(signatureBuffer, { name: "signature" }); - let outputWS = fs.createWriteStream(`${outputDir}/${request.params.type}.pkpass`); + if (Configuration.output.shouldWrite && Configuration.output.dir != null) { + // Memorize and then make it download + let outputWS = fs.createWriteStream(`${Configuration.output.dir}/${request.params.type}.pkpass`); archive.pipe(outputWS); - archive.finalize(); outputWS.on("close", function() { - response.status(201).download(`${outputDir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, { - cacheControl: false, - headers: { - "Content-type": "application/vnd.apple.pkpass", - "Content-length": fs.statSync(`${outputDir}/${request.params.type}.pkpass`).size - } + response.status(201).download(`${Configuration.output.dir}/${request.params.type}.pkpass`, `${request.params.type}.pkpass`, { + cacheControl: false }); }); - // }) - // .catch(function(error) { - // throw error; - // }); + } else { + archive.pipe(response) + response.status(201) + } + + archive.finalize(); }); }) .catch(function(err) { -throw err; - // 😊 response.set("Content-Type", "application/json"); response.status(418).send({ ecode: 418, status: false, message: `Got error while parsing pass.json file: ${err}` }); @@ -493,11 +406,15 @@ function init(configPath) { throw new Error("Initialization must be triggered only once."); } - if (!configPath || fs.accessSync(path.resolve(__dirname, configPath)) !== undefined) { + let configPathResolved = path.resolve(__dirname, configPath); + + if (!configPath || fs.accessSync(configPathResolved) !== undefined) { throw new Error(`Cannot load configuration from 'path' (${configPath}). File not existing or missing path.`); } - loadConfiguration(configPath).then(function(config) { + let setup = require(configPathResolved); + + loadConfiguration(setup).then(function(config) { Certificates.wwdr = config[0]; Certificates.signerCert = config[1]; Certificates.signerKey = config[2]; @@ -506,6 +423,11 @@ function init(configPath) { .catch(function(error) { throw new Error(`Error: ${error}`); }); + + Configuration.passModelsDir = setup.models.dir; + Configuration.output.dir = setup.output.dir; + // for a future implementation + Configuration.output.shouldWrite = false } module.exports = { init, RequestHandler };