mirror of
https://github.com/marcogll/passkit-generator.git
synced 2026-03-15 16:25:21 +00:00
Removed generateManifest, getUUID and old generateManifestSignature functions
Removed passModelsDir and outputDir constants and putted them asconfiguration object keys Removed configuration loading from loadConfiguration and moved into init() Removed generateManifest function Created first implementation of pass saving option inside configuration output directory
This commit is contained in:
268
index.js
268
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 };
|
||||
|
||||
Reference in New Issue
Block a user