From 894266de2857ace1e501586b347b824680bb9d3e Mon Sep 17 00:00:00 2001 From: Alexander Cerutti Date: Thu, 20 Jun 2019 00:30:12 +0200 Subject: [PATCH] Improved security checks; Added back the formatted messages and added new ones; --- src/factory.ts | 183 +++++++++++++++++++++++++++++------------------- src/messages.ts | 8 ++- src/pass.ts | 28 ++++++-- 3 files changed, 141 insertions(+), 78 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 8d4a261..881f3b4 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -13,14 +13,14 @@ const readFile = promisify(_readFile); export async function createPass(options: FactoryOptions): Promise { if (!(options && Object.keys(options).length)) { - throw new Error("Unable to create Pass: no options were passed"); + throw new Error(formatMessage("CP_NO_OPTS")); } try { const [bundle, certificates] = await Promise.all([ getModelContents(options.model), - readCertificatesFromOptions(options.certificates) - ]); + readCertificatesFromOptions(options.certificates) + ]); return new Pass({ model: bundle, @@ -28,13 +28,23 @@ export async function createPass(options: FactoryOptions): Promise { overrides: options.overrides }); } catch (err) { - // @TODO: analyze the error and stop the execution somehow + console.log(err); + throw new Error(formatMessage("CP_INIT_ERROR")); } } async function getModelContents(model: FactoryOptions["model"]) { - if (!(model && (typeof model === "string" || (typeof model === "object" && Object.keys(model).length)))) { - throw new Error("Unable to create Pass: invalid model provided"); + const isModelValid = ( + model && ( + typeof model === "string" || ( + typeof model === "object" && + Object.keys(model).length + ) + ) + ); + + if (!isModelValid) { + throw new Error(formatMessage("MODEL_NOT_VALID")); } let modelContents: PartitionedBundle; @@ -61,71 +71,96 @@ async function getModelContents(model: FactoryOptions["model"]) { */ async function getModelFolderContents(model: string): Promise { - const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); - const modelFilesList = await readDir(modelPath); + try { + const modelPath = path.resolve(model) + (!!model && !path.extname(model) ? ".pass" : ""); + const modelFilesList = await readDir(modelPath); - // No dot-starting files, manifest and signature - const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); + // No dot-starting files, manifest and signature + const filteredFiles = removeHidden(modelFilesList).filter(f => !/(manifest|signature)/i.test(f)); - // Icon is required to proceed - if (!(filteredFiles.length && filteredFiles.some(file => file.toLowerCase().includes("icon")))) { - const eMessage = formatMessage("MODEL_UNINITIALIZED", path.parse(this.model).name); - throw new Error(eMessage); + const isModelInitialized = ( + filteredFiles.length && + filteredFiles.some(file => file.toLowerCase().includes("icon")) + ); + + // Icon is required to proceed + if (!isModelInitialized) { + throw new Error(formatMessage( + "MODEL_UNINITIALIZED", + path.parse(this.model).name + )); + } + + // Splitting files from localization folders + const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); + const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); + + const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); + const buffers = await Promise.all(bundleBuffers); + + const bundle: BundleUnit = Object.assign({}, + ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) + ); + + // Reading concurrently localizations folder + // and their files and their buffers + const L10N_FilesListByFolder: Array = await Promise.all( + l10nFolders.map(folderPath => { + // Reading current folder + const currentLangPath = path.join(modelPath, folderPath); + return readDir(currentLangPath) + .then(files => { + // Transforming files path to a model-relative path + const validFiles = removeHidden(files) + .map(file => path.join(currentLangPath, file)); + + // Getting all the buffers from file paths + return Promise.all([ + ...validFiles.map(file => + readFile(file).catch(() => Buffer.alloc(0)) + ) + ]).then(buffers => + // Assigning each file path to its buffer + // and discarding the empty ones + validFiles.reduce((acc, file, index) => { + if (!buffers[index].length) { + return acc; + } + + return { ...acc, [file]: buffers[index] }; + }, {}) + ); + }); + }) + ); + + const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( + {}, + ...L10N_FilesListByFolder + .map((folder, index) => ({ [l10nFolders[index]]: folder })) + ); + + return { + bundle, + l10nBundle + }; + } catch (err) { + if (err.code && err.code === "ENOENT") { + if (err.syscall === "open") { + // file opening failed + throw new Error(formatMessage("MODELF_NOT_FOUND", err.path)) + } else if (err.syscall === "scandir") { + // directory reading failed + const pathContents = (err.path as string).split(/(\/|\\\?)/); + throw new Error(formatMessage( + "MODELF_FILE_NOT_FOUND", + pathContents[pathContents.length-1] + )) + } + } + + throw err; } - - // Splitting files from localization folders - const rawBundle = filteredFiles.filter(entry => !entry.includes(".lproj")); - const l10nFolders = filteredFiles.filter(entry => entry.includes(".lproj")); - - const bundleBuffers = rawBundle.map(file => readFile(path.resolve(modelPath, file))); - const buffers = await Promise.all(bundleBuffers); - - const bundle: BundleUnit = Object.assign({}, - ...rawBundle.map((fileName, index) => ({ [fileName]: buffers[index] })) - ); - - // Reading concurrently localizations folder - // and their files and their buffers - const L10N_FilesListByFolder: Array = await Promise.all( - l10nFolders.map(folderPath => { - // Reading current folder - const currentLangPath = path.join(modelPath, folderPath); - return readDir(currentLangPath) - .then(files => { - // Transforming files path to a model-relative path - const validFiles = removeHidden(files) - .map(file => path.join(currentLangPath, file)); - - // Getting all the buffers from file paths - return Promise.all([ - ...validFiles.map(file => - readFile(file).catch(() => Buffer.alloc(0)) - ) - ]).then(buffers => - // Assigning each file path to its buffer - // and discarding the empty ones - validFiles.reduce((acc, file, index) => { - if (!buffers[index].length) { - return acc; - } - - return { ...acc, [file]: buffers[index] }; - }, {}) - ); - }); - }) - ); - - const l10nBundle: PartitionedBundle["l10nBundle"] = Object.assign( - {}, - ...L10N_FilesListByFolder - .map((folder, index) => ({ [l10nFolders[index]]: folder })) - ); - - return { - bundle, - l10nBundle - }; } /** @@ -147,8 +182,14 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { const bundleKeys = Object.keys(rawBundle); - if (!bundleKeys.length) { - throw new Error("Cannot proceed with pass creation: bundle not initialized") + const isModelInitialized = ( + bundleKeys.length && + bundleKeys.some(file => file.toLowerCase().includes("icon")) + ); + + // Icon is required to proceed + if (!isModelInitialized) { + throw new Error(formatMessage("MODEL_UNINITIALIZED", "Buffers")) } // separing localization folders @@ -179,7 +220,7 @@ function getModelBufferContents(model: BundleUnit): PartitionedBundle { async function readCertificatesFromOptions(options: Certificates): Promise { if (!(options && Object.keys(options).length && isValid(options, "certificatesSchema"))) { - throw new Error("Unable to create Pass: certificates schema validation failed."); + throw new Error(formatMessage("CP_NO_CERTS")); } // if the signerKey is an object, we want to get diff --git a/src/messages.ts b/src/messages.ts index 05e67ce..3ef035f 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -3,11 +3,15 @@ interface MessageGroup { } const errors: MessageGroup = { + CP_INIT_ERROR: "Something went really bad in the initialization, dude! Please look at the log above this message. It should contain all the infos about the problem.", + CP_NO_OPTS: "Cannot initialize the pass creation: no options were passed.", + CP_NO_CERTS: "Cannot initialize the pass creation: no valid certificates were passed.", PASSFILE_VALIDATION_FAILED: "Validation of pass type failed. Pass file is not a valid buffer or (more probably) does not respect the schema.\nRefer to https://apple.co/2Nvshvn to build a correct pass.", REQUIR_VALID_FAILED: "The options passed to Pass constructor does not meet the requirements.\nRefer to the documentation to compile them correctly.", MODEL_UNINITIALIZED: "Provided model ( %s ) matched but unitialized or may not contain icon.\nRefer to https://apple.co/2IhJr0Q, https://apple.co/2Nvshvn and documentation to fill the model correctly.", - MODEL_NOT_STRING: "A string model name must be provided in order to continue.", - MODEL_NOT_FOUND: "Model %s not found. Provide a valid one to continue.", + MODEL_NOT_VALID: "A model must be provided in form of path (string) or object { 'fileName': Buffer } in order to continue.", + MODELF_NOT_FOUND: "Model %s not found. Provide a valid one to continue.", + MODELF_FILE_NOT_FOUND: "File %s not found.", INVALID_CERTS: "Invalid certificate(s) loaded: %s. Please provide valid WWDR certificates and developer signer certificate and key (with passphrase).\nRefer to docs to obtain them.", INVALID_CERT_PATH: "Invalid certificate loaded. %s does not exist.", TRSTYPE_REQUIRED: "Cannot proceed with pass creation. transitType field is required for boardingPasses.", diff --git a/src/pass.ts b/src/pass.ts index 2271a23..34294e4 100644 --- a/src/pass.ts +++ b/src/pass.ts @@ -53,20 +53,38 @@ export class Pass implements PassIndexSignature { [transitType]: string = ""; constructor(options: schema.PassInstance) { + if (!schema.isValid(options, "instance")) { + throw new Error(formatMessage("REQUIR_VALID_FAILED")); + } + this.Certificates = options.certificates; this.l10nBundles = options.model.l10nBundle; this.bundle = { ...options.model.bundle }; - options.overrides = options.overrides || {}; + // Parsing the options and extracting only the valid ones. + const validOvverrides = schema.getValidated(options.overrides || {}, "supportedOptions") as schema.OverridesSupportedOptions; - // getting pass.json - this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); + if (validOvverrides === null) { + throw new Error(formatMessage("OVV_KEYS_BADFORMAT")) + } + + if (Object.keys(validOvverrides).length) { + this._props = { ...validOvverrides }; + } + + try { + // getting pass.json + this.passCore = JSON.parse(this.bundle["pass.json"].toString("utf8")); + } catch (err) { + throw new Error(formatMessage("PASSFILE_VALIDATION_FAILED")); + } this.type = Object.keys(this.passCore) .find(key => /(boardingPass|eventTicket|coupon|generic|storeCard)/.test(key)) as keyof schema.ValidPassType; if (!this.type) { - throw new Error("Missing type in model"); + // @TODO: change error message to say it is invalid or missing + throw new Error(formatMessage("NO_PASS_TYPE")); } if (this.type === "boardingPass" && this.passCore[this.type]["transitType"]) { @@ -432,7 +450,7 @@ export class Pass implements PassIndexSignature { } if (typeof format !== "string") { - barcodeDebug(formatMessage("BRC_FORMAT_UNMATCH")); + barcodeDebug(formatMessage("BRC_FORMATTYPE_UNMATCH")); return this; }