From cfb0d94e84c85cd73c39d2ec6822b331e28bbfe7 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 1 Mar 2023 18:27:19 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A=20=E2=9E=95=20=20add=20node-signpd?= =?UTF-8?q?f=20copy=20to=20signing=20packgage=20until=20npm=20includes=20f?= =?UTF-8?q?ix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../signing/node-signpdf/dist/SignPdfError.js | 30 +++ .../node-signpdf/dist/helpers/const.js | 18 ++ .../dist/helpers/extractSignature.js | 71 +++++++ .../dist/helpers/findByteRange.js | 41 ++++ .../node-signpdf/dist/helpers/index.js | 49 +++++ .../dist/helpers/pdfkit/abstract_reference.js | 26 +++ .../dist/helpers/pdfkit/pdfobject.js | 149 ++++++++++++++ .../dist/helpers/pdfkitAddPlaceholder.js | 133 ++++++++++++ .../dist/helpers/pdfkitReferenceMock.js | 29 +++ .../createBufferPageWithAnnotation.js | 47 +++++ .../createBufferRootWithAcroform.js | 18 ++ .../createBufferTrailer.js | 21 ++ .../helpers/plainAddPlaceholder/findObject.js | 29 +++ .../plainAddPlaceholder/getIndexFromRef.js | 29 +++ .../helpers/plainAddPlaceholder/getPageRef.js | 29 +++ .../getPagesDictionaryRef.js | 24 +++ .../dist/helpers/plainAddPlaceholder/index.js | 109 ++++++++++ .../helpers/plainAddPlaceholder/readPdf.js | 63 ++++++ .../plainAddPlaceholder/readRefTable.js | 119 +++++++++++ .../plainAddPlaceholder/xrefToRefMap.js | 54 +++++ .../dist/helpers/removeTrailingNewLine.js | 48 +++++ packages/signing/node-signpdf/dist/signpdf.js | 193 ++++++++++++++++++ packages/signing/signpdf.js | 193 ++++++++++++++++++ 23 files changed, 1522 insertions(+) create mode 100644 packages/signing/node-signpdf/dist/SignPdfError.js create mode 100644 packages/signing/node-signpdf/dist/helpers/const.js create mode 100644 packages/signing/node-signpdf/dist/helpers/extractSignature.js create mode 100644 packages/signing/node-signpdf/dist/helpers/findByteRange.js create mode 100644 packages/signing/node-signpdf/dist/helpers/index.js create mode 100644 packages/signing/node-signpdf/dist/helpers/pdfkit/abstract_reference.js create mode 100644 packages/signing/node-signpdf/dist/helpers/pdfkit/pdfobject.js create mode 100644 packages/signing/node-signpdf/dist/helpers/pdfkitAddPlaceholder.js create mode 100644 packages/signing/node-signpdf/dist/helpers/pdfkitReferenceMock.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferPageWithAnnotation.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferRootWithAcroform.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferTrailer.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/findObject.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getIndexFromRef.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPageRef.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPagesDictionaryRef.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/index.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readPdf.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readRefTable.js create mode 100644 packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/xrefToRefMap.js create mode 100644 packages/signing/node-signpdf/dist/helpers/removeTrailingNewLine.js create mode 100644 packages/signing/node-signpdf/dist/signpdf.js create mode 100644 packages/signing/signpdf.js diff --git a/packages/signing/node-signpdf/dist/SignPdfError.js b/packages/signing/node-signpdf/dist/SignPdfError.js new file mode 100644 index 000000000..a46e7f97e --- /dev/null +++ b/packages/signing/node-signpdf/dist/SignPdfError.js @@ -0,0 +1,30 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = exports.ERROR_VERIFY_SIGNATURE = exports.ERROR_TYPE_UNKNOWN = exports.ERROR_TYPE_PARSE = exports.ERROR_TYPE_INPUT = void 0; +const ERROR_TYPE_UNKNOWN = 1; +exports.ERROR_TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN; +const ERROR_TYPE_INPUT = 2; +exports.ERROR_TYPE_INPUT = ERROR_TYPE_INPUT; +const ERROR_TYPE_PARSE = 3; +exports.ERROR_TYPE_PARSE = ERROR_TYPE_PARSE; +const ERROR_VERIFY_SIGNATURE = 4; +exports.ERROR_VERIFY_SIGNATURE = ERROR_VERIFY_SIGNATURE; + +class SignPdfError extends Error { + constructor(msg, type = ERROR_TYPE_UNKNOWN) { + super(msg); + this.type = type; + } + +} // Shorthand + + +SignPdfError.TYPE_UNKNOWN = ERROR_TYPE_UNKNOWN; +SignPdfError.TYPE_INPUT = ERROR_TYPE_INPUT; +SignPdfError.TYPE_PARSE = ERROR_TYPE_PARSE; +SignPdfError.VERIFY_SIGNATURE = ERROR_VERIFY_SIGNATURE; +var _default = SignPdfError; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/const.js b/packages/signing/node-signpdf/dist/helpers/const.js new file mode 100644 index 000000000..7b2d86721 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/const.js @@ -0,0 +1,18 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SUBFILTER_ETSI_CADES_DETACHED = exports.SUBFILTER_ADOBE_X509_SHA1 = exports.SUBFILTER_ADOBE_PKCS7_SHA1 = exports.SUBFILTER_ADOBE_PKCS7_DETACHED = exports.DEFAULT_SIGNATURE_LENGTH = exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = void 0; +const DEFAULT_SIGNATURE_LENGTH = 8192; +exports.DEFAULT_SIGNATURE_LENGTH = DEFAULT_SIGNATURE_LENGTH; +const DEFAULT_BYTE_RANGE_PLACEHOLDER = '**********'; +exports.DEFAULT_BYTE_RANGE_PLACEHOLDER = DEFAULT_BYTE_RANGE_PLACEHOLDER; +const SUBFILTER_ADOBE_PKCS7_DETACHED = 'adbe.pkcs7.detached'; +exports.SUBFILTER_ADOBE_PKCS7_DETACHED = SUBFILTER_ADOBE_PKCS7_DETACHED; +const SUBFILTER_ADOBE_PKCS7_SHA1 = 'adbe.pkcs7.sha1'; +exports.SUBFILTER_ADOBE_PKCS7_SHA1 = SUBFILTER_ADOBE_PKCS7_SHA1; +const SUBFILTER_ADOBE_X509_SHA1 = 'adbe.x509.rsa.sha1'; +exports.SUBFILTER_ADOBE_X509_SHA1 = SUBFILTER_ADOBE_X509_SHA1; +const SUBFILTER_ETSI_CADES_DETACHED = 'ETSI.CAdES.detached'; +exports.SUBFILTER_ETSI_CADES_DETACHED = SUBFILTER_ETSI_CADES_DETACHED; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/extractSignature.js b/packages/signing/node-signpdf/dist/helpers/extractSignature.js new file mode 100644 index 000000000..0d3002401 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/extractSignature.js @@ -0,0 +1,71 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _SignPdfError = _interopRequireDefault(require("../SignPdfError")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const getSubstringIndex = (str, substring, n) => { + let times = 0; + let index = null; + + while (times < n && index !== -1) { + index = str.indexOf(substring, index + 1); + times += 1; + } + + return index; +}; +/** + * Basic implementation of signature extraction. + * + * Really basic. Would work in the simplest of cases where there is only one signature + * in a document and ByteRange is only used once in it. + * + * @param {Buffer} pdf + * @returns {Object} {ByteRange: Number[], signature: Buffer, signedData: Buffer} + */ + + +const extractSignature = (pdf, signatureCount = 1) => { + if (!(pdf instanceof Buffer)) { + throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } // const byteRangePos = pdf.indexOf('/ByteRange ['); + + + const byteRangePos = getSubstringIndex(pdf, '/ByteRange [', signatureCount); + + if (byteRangePos === -1) { + throw new _SignPdfError.default('Failed to locate ByteRange.', _SignPdfError.default.TYPE_PARSE); + } + + const byteRangeEnd = pdf.indexOf(']', byteRangePos); + + if (byteRangeEnd === -1) { + throw new _SignPdfError.default('Failed to locate the end of the ByteRange.', _SignPdfError.default.TYPE_PARSE); + } + + const byteRange = pdf.slice(byteRangePos, byteRangeEnd + 1).toString(); + const matches = /\/ByteRange \[(\d+) +(\d+) +(\d+) +(\d+) *\]/.exec(byteRange); + + if (matches === null) { + throw new _SignPdfError.default('Failed to parse the ByteRange.', _SignPdfError.default.TYPE_PARSE); + } + + const ByteRange = matches.slice(1).map(Number); + const signedData = Buffer.concat([pdf.slice(ByteRange[0], ByteRange[0] + ByteRange[1]), pdf.slice(ByteRange[2], ByteRange[2] + ByteRange[3])]); + const signatureHex = pdf.slice(ByteRange[0] + ByteRange[1] + 1, ByteRange[2]).toString('binary').replace(/(?:00|>)+$/, ''); + const signature = Buffer.from(signatureHex, 'hex').toString('binary'); + return { + ByteRange: matches.slice(1, 5).map(Number), + signature, + signedData + }; +}; + +var _default = extractSignature; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/findByteRange.js b/packages/signing/node-signpdf/dist/helpers/findByteRange.js new file mode 100644 index 000000000..c3853bf07 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/findByteRange.js @@ -0,0 +1,41 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _SignPdfError = _interopRequireDefault(require("../SignPdfError")); + +var _const = require("./const"); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Finds ByteRange information within a given PDF Buffer if one exists + * + * @param {Buffer} pdf + * @returns {Object} {byteRangePlaceholder: String, byteRangeStrings: String[], byteRange: String[]} + */ +const findByteRange = pdf => { + if (!(pdf instanceof Buffer)) { + throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } + + const byteRangeStrings = pdf.toString().match(/\/ByteRange\s*\[{1}\s*(?:(?:\d*|\/\*{10})\s+){3}(?:\d+|\/\*{10}){1}\s*]{1}/g); + + if (!byteRangeStrings) { + throw new _SignPdfError.default('No ByteRangeStrings found within PDF buffer', _SignPdfError.default.TYPE_PARSE); + } + + const byteRangePlaceholder = byteRangeStrings.find(s => s.includes(`/${_const.DEFAULT_BYTE_RANGE_PLACEHOLDER}`)); + const byteRanges = byteRangeStrings.map(brs => brs.match(/[^[\s]*(?:\d|\/\*{10})/g)); + return { + byteRangePlaceholder, + byteRangeStrings, + byteRanges + }; +}; + +var _default = findByteRange; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/index.js b/packages/signing/node-signpdf/dist/helpers/index.js new file mode 100644 index 000000000..3c44df91b --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/index.js @@ -0,0 +1,49 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +Object.defineProperty(exports, "extractSignature", { + enumerable: true, + get: function () { + return _extractSignature.default; + } +}); +Object.defineProperty(exports, "findByteRange", { + enumerable: true, + get: function () { + return _findByteRange.default; + } +}); +Object.defineProperty(exports, "pdfkitAddPlaceholder", { + enumerable: true, + get: function () { + return _pdfkitAddPlaceholder.default; + } +}); +Object.defineProperty(exports, "plainAddPlaceholder", { + enumerable: true, + get: function () { + return _plainAddPlaceholder.default; + } +}); +Object.defineProperty(exports, "removeTrailingNewLine", { + enumerable: true, + get: function () { + return _removeTrailingNewLine.default; + } +}); + +var _extractSignature = _interopRequireDefault(require("./extractSignature")); + +var _pdfkitAddPlaceholder = _interopRequireDefault(require("./pdfkitAddPlaceholder")); + +var _plainAddPlaceholder = _interopRequireDefault(require("./plainAddPlaceholder")); + +var _removeTrailingNewLine = _interopRequireDefault(require("./removeTrailingNewLine")); + +var _findByteRange = _interopRequireDefault(require("./findByteRange")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +'This string is added so that jest collects coverage for this file'; // eslint-disable-line \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/pdfkit/abstract_reference.js b/packages/signing/node-signpdf/dist/helpers/pdfkit/abstract_reference.js new file mode 100644 index 000000000..d68dbff2e --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/pdfkit/abstract_reference.js @@ -0,0 +1,26 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +/* +PDFAbstractReference by Devon Govett used below. +The class is part of pdfkit. See https://github.com/foliojs/pdfkit +LICENSE: MIT. Included in this folder. +Modifications may have been applied for the purposes of node-signpdf. +*/ + +/* +PDFAbstractReference - abstract class for PDF reference +*/ +class PDFAbstractReference { + toString() { + throw new Error('Must be implemented by subclasses'); + } + +} + +var _default = PDFAbstractReference; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/pdfkit/pdfobject.js b/packages/signing/node-signpdf/dist/helpers/pdfkit/pdfobject.js new file mode 100644 index 000000000..498ab5323 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/pdfkit/pdfobject.js @@ -0,0 +1,149 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _abstract_reference = _interopRequireDefault(require("./abstract_reference")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* +PDFObject by Devon Govett used below. +The class is part of pdfkit. See https://github.com/foliojs/pdfkit +LICENSE: MIT. Included in this folder. +Modifications may have been applied for the purposes of node-signpdf. +*/ + +/* +PDFObject - converts JavaScript types into their corresponding PDF types. +By Devon Govett +*/ +const pad = (str, length) => (Array(length + 1).join('0') + str).slice(-length); + +const escapableRe = /[\n\r\t\b\f()\\]/g; +const escapable = { + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\b': '\\b', + '\f': '\\f', + '\\': '\\\\', + '(': '\\(', + ')': '\\)' +}; // Convert little endian UTF-16 to big endian + +const swapBytes = buff => buff.swap16(); + +class PDFObject { + static convert(object, encryptFn = null) { + // String literals are converted to the PDF name type + if (typeof object === 'string') { + return `/${object}`; // String objects are converted to PDF strings (UTF-16) + } + + if (object instanceof String) { + let string = object; // Detect if this is a unicode string + + let isUnicode = false; + + for (let i = 0, end = string.length; i < end; i += 1) { + if (string.charCodeAt(i) > 0x7f) { + isUnicode = true; + break; + } + } // If so, encode it as big endian UTF-16 + + + let stringBuffer; + + if (isUnicode) { + stringBuffer = swapBytes(Buffer.from(`\ufeff${string}`, 'utf16le')); + } else { + stringBuffer = Buffer.from(string, 'ascii'); + } // Encrypt the string when necessary + + + if (encryptFn) { + string = encryptFn(stringBuffer).toString('binary'); + } else { + string = stringBuffer.toString('binary'); + } // Escape characters as required by the spec + + + string = string.replace(escapableRe, c => escapable[c]); + return `(${string})`; // Buffers are converted to PDF hex strings + } + + if (Buffer.isBuffer(object)) { + return `<${object.toString('hex')}>`; + } + + if (object instanceof _abstract_reference.default) { + return object.toString(); + } + + if (object instanceof Date) { + let string = `D:${pad(object.getUTCFullYear(), 4)}${pad(object.getUTCMonth() + 1, 2)}${pad(object.getUTCDate(), 2)}${pad(object.getUTCHours(), 2)}${pad(object.getUTCMinutes(), 2)}${pad(object.getUTCSeconds(), 2)}Z`; // Encrypt the string when necessary + + if (encryptFn) { + string = encryptFn(Buffer.from(string, 'ascii')).toString('binary'); // Escape characters as required by the spec + + string = string.replace(escapableRe, c => escapable[c]); + } + + return `(${string})`; + } + + if (Array.isArray(object)) { + const items = object.map(e => PDFObject.convert(e, encryptFn)).join(' '); + return `[${items}]`; + } + + if ({}.toString.call(object) === '[object Object]') { + const out = ['<<']; + let streamData; // @todo this can probably be refactored into a reduce + + Object.entries(object).forEach(([key, val]) => { + let checkedValue = ''; + + if (val.toString().indexOf('<<') !== -1) { + checkedValue = val; + } else { + checkedValue = PDFObject.convert(val, encryptFn); + } + + if (key === 'stream') { + streamData = `${key}\n${val}\nendstream`; + } else { + out.push(`/${key} ${checkedValue}`); + } + }); + out.push('>>'); + + if (streamData) { + out.push(streamData); + } + + return out.join('\n'); + } + + if (typeof object === 'number') { + return PDFObject.number(object); + } + + return `${object}`; + } + + static number(n) { + if (n > -1e21 && n < 1e21) { + return Math.round(n * 1e6) / 1e6; + } + + throw new Error(`unsupported number: ${n}`); + } + +} + +exports.default = PDFObject; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/pdfkitAddPlaceholder.js b/packages/signing/node-signpdf/dist/helpers/pdfkitAddPlaceholder.js new file mode 100644 index 000000000..fe266ec10 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/pdfkitAddPlaceholder.js @@ -0,0 +1,133 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _const = require("./const"); + +var _pdfkitReferenceMock = _interopRequireDefault(require("./pdfkitReferenceMock")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// eslint-disable-next-line import/no-unresolved + +/** + * Adds the objects that are needed for Adobe.PPKLite to read the signature. + * Also includes a placeholder for the actual signature. + * Returns an Object with all the added PDFReferences. + * @param {PDFDocument} pdf + * @param {string} reason + * @returns {object} + */ +const pdfkitAddPlaceholder = ({ + pdf, + pdfBuffer, + reason, + contactInfo = 'emailfromp1289@gmail.com', + name = 'Name from p12', + location = 'Location from p12', + signatureLength = _const.DEFAULT_SIGNATURE_LENGTH, + byteRangePlaceholder = _const.DEFAULT_BYTE_RANGE_PLACEHOLDER, + subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED +}) => { + /* eslint-disable no-underscore-dangle,no-param-reassign */ + // Generate the signature placeholder + const signature = pdf.ref({ + Type: 'Sig', + Filter: 'Adobe.PPKLite', + SubFilter: subFilter, + ByteRange: [0, byteRangePlaceholder, byteRangePlaceholder, byteRangePlaceholder], + Contents: Buffer.from(String.fromCharCode(0).repeat(signatureLength)), + Reason: new String(reason), + // eslint-disable-line no-new-wrappers + M: new Date(), + ContactInfo: new String(contactInfo), + // eslint-disable-line no-new-wrappers + Name: new String(name), + // eslint-disable-line no-new-wrappers + Location: new String(location) // eslint-disable-line no-new-wrappers + + }); // Check if pdf already contains acroform field + + const acroFormPosition = pdfBuffer.lastIndexOf('/Type /AcroForm'); + const isAcroFormExists = acroFormPosition !== -1; + let fieldIds = []; + let acroFormId; + + if (isAcroFormExists) { + let acroFormStart = acroFormPosition; // 10 is the distance between "/Type /AcroForm" and AcroFrom ID + + const charsUntilIdEnd = 10; + const acroFormIdEnd = acroFormPosition - charsUntilIdEnd; // Let's find AcroForm ID by trying to find the "\n" before the ID + // 12 is a enough space to find the "\n" + // (generally it's 2 or 3, but I'm giving a big space though) + + const maxAcroFormIdLength = 12; + let foundAcroFormId = ''; + let index = charsUntilIdEnd + 1; + + for (index; index < charsUntilIdEnd + maxAcroFormIdLength; index += 1) { + const acroFormIdString = pdfBuffer.slice(acroFormPosition - index, acroFormIdEnd).toString(); + + if (acroFormIdString[0] === '\n') { + break; + } + + foundAcroFormId = acroFormIdString; + acroFormStart = acroFormPosition - index; + } + + const pdfSlice = pdfBuffer.slice(acroFormStart); + const acroForm = pdfSlice.slice(0, pdfSlice.indexOf('endobj')).toString(); + acroFormId = parseInt(foundAcroFormId); + const acroFormFields = acroForm.slice(acroForm.indexOf('/Fields [') + 9, acroForm.indexOf(']')); + fieldIds = acroFormFields.split(' ').filter((element, i) => i % 3 === 0).map(fieldId => new _pdfkitReferenceMock.default(fieldId)); + } + + const signatureName = 'Signature'; // Generate signature annotation widget + + const widget = pdf.ref({ + Type: 'Annot', + Subtype: 'Widget', + FT: 'Sig', + Rect: [0, 0, 0, 0], + V: signature, + T: new String(signatureName + (fieldIds.length + 1)), + // eslint-disable-line no-new-wrappers + F: 4, + P: pdf.page.dictionary // eslint-disable-line no-underscore-dangle + + }); + pdf.page.dictionary.data.Annots = [widget]; // Include the widget in a page + + let form; + + if (!isAcroFormExists) { + // Create a form (with the widget) and link in the _root + form = pdf.ref({ + Type: 'AcroForm', + SigFlags: 3, + Fields: [...fieldIds, widget] + }); + } else { + // Use existing acroform and extend the fields with newly created widgets + form = pdf.ref({ + Type: 'AcroForm', + SigFlags: 3, + Fields: [...fieldIds, widget] + }, acroFormId); + } + + pdf._root.data.AcroForm = form; + return { + signature, + form, + widget + }; + /* eslint-enable no-underscore-dangle,no-param-reassign */ +}; + +var _default = pdfkitAddPlaceholder; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/pdfkitReferenceMock.js b/packages/signing/node-signpdf/dist/helpers/pdfkitReferenceMock.js new file mode 100644 index 000000000..0930df58d --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/pdfkitReferenceMock.js @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _abstract_reference = _interopRequireDefault(require("./pdfkit/abstract_reference")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +class PDFKitReferenceMock extends _abstract_reference.default { + constructor(index, additionalData = undefined) { + super(); + this.index = index; + + if (typeof additionalData !== 'undefined') { + Object.assign(this, additionalData); + } + } + + toString() { + return `${this.index} 0 R`; + } + +} + +var _default = PDFKitReferenceMock; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferPageWithAnnotation.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferPageWithAnnotation.js new file mode 100644 index 000000000..1a046096c --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferPageWithAnnotation.js @@ -0,0 +1,47 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _findObject = _interopRequireDefault(require("./findObject")); + +var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const createBufferPageWithAnnotation = (pdf, info, pagesRef, widget) => { + const pagesDictionary = (0, _findObject.default)(pdf, info.xref, pagesRef).toString(); // Extend page dictionary with newly created annotations + + let annotsStart; + let annotsEnd; + let annots; + annotsStart = pagesDictionary.indexOf('/Annots'); + + if (annotsStart > -1) { + annotsEnd = pagesDictionary.indexOf(']', annotsStart); + annots = pagesDictionary.substr(annotsStart, annotsEnd + 1 - annotsStart); + annots = annots.substr(0, annots.length - 1); // remove the trailing ] + } else { + annotsStart = pagesDictionary.length; + annotsEnd = pagesDictionary.length; + annots = '/Annots ['; + } + + const pagesDictionaryIndex = (0, _getIndexFromRef.default)(info.xref, pagesRef); + const widgetValue = widget.toString(); + annots = `${annots} ${widgetValue}]`; // add the trailing ] back + + const preAnnots = pagesDictionary.substr(0, annotsStart); + let postAnnots = ''; + + if (pagesDictionary.length > annotsEnd) { + postAnnots = pagesDictionary.substr(annotsEnd + 1); + } + + return Buffer.concat([Buffer.from(`${pagesDictionaryIndex} 0 obj\n`), Buffer.from('<<\n'), Buffer.from(`${preAnnots + annots + postAnnots}\n`), Buffer.from('\n>>\nendobj\n')]); +}; + +var _default = createBufferPageWithAnnotation; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferRootWithAcroform.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferRootWithAcroform.js new file mode 100644 index 000000000..4a34369c1 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferRootWithAcroform.js @@ -0,0 +1,18 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const createBufferRootWithAcroform = (pdf, info, form) => { + const rootIndex = (0, _getIndexFromRef.default)(info.xref, info.rootRef); + return Buffer.concat([Buffer.from(`${rootIndex} 0 obj\n`), Buffer.from('<<\n'), Buffer.from(`${info.root}\n`), Buffer.from(`/AcroForm ${form}`), Buffer.from('\n>>\nendobj\n')]); +}; + +var _default = createBufferRootWithAcroform; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferTrailer.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferTrailer.js new file mode 100644 index 000000000..c982767ba --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/createBufferTrailer.js @@ -0,0 +1,21 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +const createBufferTrailer = (pdf, info, addedReferences) => { + let rows = []; + rows[0] = '0000000000 65535 f '; // info.xref.tableRows[0]; + + addedReferences.forEach((offset, index) => { + const paddedOffset = `0000000000${offset}`.slice(-10); + rows[index + 1] = `${index} 1\n${paddedOffset} 00000 n `; + }); + rows = rows.filter(row => row !== undefined); + return Buffer.concat([Buffer.from('xref\n'), Buffer.from(`${info.xref.startingIndex} 1\n`), Buffer.from(rows.join('\n')), Buffer.from('\ntrailer\n'), Buffer.from('<<\n'), Buffer.from(`/Size ${info.xref.maxIndex + 1}\n`), Buffer.from(`/Root ${info.rootRef}\n`), Buffer.from(info.infoRef ? `/Info ${info.infoRef}\n` : ''), Buffer.from(`/Prev ${info.xRefPosition}\n`), Buffer.from('>>\n'), Buffer.from('startxref\n'), Buffer.from(`${pdf.length}\n`), Buffer.from('%%EOF')]); +}; + +var _default = createBufferTrailer; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/findObject.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/findObject.js new file mode 100644 index 000000000..db6c7b3e4 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/findObject.js @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * @param {Buffer} pdf + * @param {Map} refTable + * @returns {object} + */ +const findObject = (pdf, refTable, ref) => { + const index = (0, _getIndexFromRef.default)(refTable, ref); + const offset = refTable.offsets.get(index); + let slice = pdf.slice(offset); + slice = slice.slice(0, slice.indexOf('endobj', 'utf8')); // FIXME: What if it is a stream? + + slice = slice.slice(slice.indexOf('<<', 'utf8') + 2); + slice = slice.slice(0, slice.lastIndexOf('>>', 'utf8')); + return slice; +}; + +var _default = findObject; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getIndexFromRef.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getIndexFromRef.js new file mode 100644 index 000000000..1db2ed468 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getIndexFromRef.js @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _SignPdfError = _interopRequireDefault(require("../../SignPdfError")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * @param {object} refTable + * @param {string} ref + * @returns {number} + */ +const getIndexFromRef = (refTable, ref) => { + let [index] = ref.split(' '); + index = parseInt(index); + + if (!refTable.offsets.has(index)) { + throw new _SignPdfError.default(`Failed to locate object "${ref}".`, _SignPdfError.default.TYPE_PARSE); + } + + return index; +}; + +var _default = getIndexFromRef; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPageRef.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPageRef.js new file mode 100644 index 000000000..f7dc95b85 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPageRef.js @@ -0,0 +1,29 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = getPageRef; + +var _getPagesDictionaryRef = _interopRequireDefault(require("./getPagesDictionaryRef")); + +var _findObject = _interopRequireDefault(require("./findObject")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * Finds the reference to a page. + * + * @param {Buffer} pdfBuffer + * @param {Object} info As extracted from readRef() + */ +function getPageRef(pdfBuffer, info) { + const pagesRef = (0, _getPagesDictionaryRef.default)(info); + const pagesDictionary = (0, _findObject.default)(pdfBuffer, info.xref, pagesRef); + const kidsPosition = pagesDictionary.indexOf('/Kids'); + const kidsStart = pagesDictionary.indexOf('[', kidsPosition) + 1; + const kidsEnd = pagesDictionary.indexOf(']', kidsPosition); + const pages = pagesDictionary.slice(kidsStart, kidsEnd).toString(); + const split = pages.trim().split(' ', 3); + return `${split[0]} ${split[1]} ${split[2]}`; +} \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPagesDictionaryRef.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPagesDictionaryRef.js new file mode 100644 index 000000000..375de63b0 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/getPagesDictionaryRef.js @@ -0,0 +1,24 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = getPagesDictionaryRef; + +var _SignPdfError = _interopRequireDefault(require("../../SignPdfError")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/** + * @param {Object} info As extracted from readRef() + */ +function getPagesDictionaryRef(info) { + const pagesRefRegex = /\/Pages\s+(\d+\s+\d+\s+R)/g; + const match = pagesRefRegex.exec(info.root); + + if (match === null) { + throw new _SignPdfError.default('Failed to find the pages descriptor. This is probably a problem in node-signpdf.', _SignPdfError.default.TYPE_PARSE); + } + + return match[1]; +} \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/index.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/index.js new file mode 100644 index 000000000..764dbfe24 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/index.js @@ -0,0 +1,109 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _pdfobject = _interopRequireDefault(require("../pdfkit/pdfobject")); + +var _pdfkitReferenceMock = _interopRequireDefault(require("../pdfkitReferenceMock")); + +var _removeTrailingNewLine = _interopRequireDefault(require("../removeTrailingNewLine")); + +var _const = require("../const"); + +var _pdfkitAddPlaceholder = _interopRequireDefault(require("../pdfkitAddPlaceholder")); + +var _getIndexFromRef = _interopRequireDefault(require("./getIndexFromRef")); + +var _readPdf = _interopRequireDefault(require("./readPdf")); + +var _getPageRef = _interopRequireDefault(require("./getPageRef")); + +var _createBufferRootWithAcroform = _interopRequireDefault(require("./createBufferRootWithAcroform")); + +var _createBufferPageWithAnnotation = _interopRequireDefault(require("./createBufferPageWithAnnotation")); + +var _createBufferTrailer = _interopRequireDefault(require("./createBufferTrailer")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const isContainBufferRootWithAcroform = pdf => { + const bufferRootWithAcroformRefRegex = /\/AcroForm\s+(\d+\s\d+\sR)/g; + const match = bufferRootWithAcroformRefRegex.exec(pdf.toString()); + return match != null && match[1] != null && match[1] !== ''; +}; +/** + * Adds a signature placeholder to a PDF Buffer. + * + * This contrasts with the default pdfkit-based implementation. + * Parsing is done using simple string operations. + * Adding is done with `Buffer.concat`. + * This allows node-signpdf to be used on any PDF and + * not only on a freshly created through PDFKit one. + */ + + +const plainAddPlaceholder = ({ + pdfBuffer, + reason, + contactInfo = 'emailfromp1289@gmail.com', + name = 'Name from p12', + location = 'Location from p12', + signatureLength = _const.DEFAULT_SIGNATURE_LENGTH, + subFilter = _const.SUBFILTER_ADOBE_PKCS7_DETACHED +}) => { + let pdf = (0, _removeTrailingNewLine.default)(pdfBuffer); + const info = (0, _readPdf.default)(pdf); + const pageRef = (0, _getPageRef.default)(pdf, info); + const pageIndex = (0, _getIndexFromRef.default)(info.xref, pageRef); + const addedReferences = new Map(); + const pdfKitMock = { + ref: (input, additionalIndex) => { + info.xref.maxIndex += 1; + const index = additionalIndex != null ? additionalIndex : info.xref.maxIndex; + addedReferences.set(index, pdf.length + 1); // + 1 new line + + pdf = Buffer.concat([pdf, Buffer.from('\n'), Buffer.from(`${index} 0 obj\n`), Buffer.from(_pdfobject.default.convert(input)), Buffer.from('\nendobj\n')]); + return new _pdfkitReferenceMock.default(info.xref.maxIndex); + }, + page: { + dictionary: new _pdfkitReferenceMock.default(pageIndex, { + data: { + Annots: [] + } + }) + }, + _root: { + data: {} + } + }; + const { + form, + widget + } = (0, _pdfkitAddPlaceholder.default)({ + pdf: pdfKitMock, + pdfBuffer, + reason, + contactInfo, + name, + location, + signatureLength, + subFilter + }); + + if (!isContainBufferRootWithAcroform(pdf)) { + const rootIndex = (0, _getIndexFromRef.default)(info.xref, info.rootRef); + addedReferences.set(rootIndex, pdf.length + 1); + pdf = Buffer.concat([pdf, Buffer.from('\n'), (0, _createBufferRootWithAcroform.default)(pdf, info, form)]); + } + + addedReferences.set(pageIndex, pdf.length + 1); + pdf = Buffer.concat([pdf, Buffer.from('\n'), (0, _createBufferPageWithAnnotation.default)(pdf, info, pageRef, widget)]); + pdf = Buffer.concat([pdf, Buffer.from('\n'), (0, _createBufferTrailer.default)(pdf, info, addedReferences)]); + return pdf; +}; + +var _default = plainAddPlaceholder; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readPdf.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readPdf.js new file mode 100644 index 000000000..1bba22555 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readPdf.js @@ -0,0 +1,63 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _readRefTable = _interopRequireDefault(require("./readRefTable")); + +var _findObject = _interopRequireDefault(require("./findObject")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const getValue = (trailer, key) => { + let index = trailer.indexOf(key); + + if (index === -1) { + return undefined; + } + + const slice = trailer.slice(index); + index = slice.indexOf('/', 1); + + if (index === -1) { + index = slice.indexOf('>', 1); + } + + return slice.slice(key.length + 1, index).toString().trim(); // key + at least one space +}; +/** + * Simplified parsing of a PDF Buffer. + * Extracts reference table, root info and trailer start. + * + * See section 7.5.5 (File Trailer) of the PDF specs. + * + * @param {Buffer} pdfBuffer + */ + + +const readPdf = pdfBuffer => { + // Extract the trailer dictionary. + const trailerStart = pdfBuffer.lastIndexOf('trailer'); // The trailer is followed by xref. Then an EOF. EOF's length is 6 characters. + + const trailer = pdfBuffer.slice(trailerStart, pdfBuffer.length - 6); + let xRefPosition = trailer.slice(trailer.lastIndexOf('startxref') + 10).toString(); + xRefPosition = parseInt(xRefPosition); + const refTable = (0, _readRefTable.default)(pdfBuffer); + const rootRef = getValue(trailer, '/Root'); + const root = (0, _findObject.default)(pdfBuffer, refTable, rootRef).toString(); + const infoRef = getValue(trailer, '/Info'); + return { + xref: refTable, + rootRef, + root, + infoRef, + trailerStart, + previousXrefs: [], + xRefPosition + }; +}; + +var _default = readPdf; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readRefTable.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readRefTable.js new file mode 100644 index 000000000..8c8a75c27 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/readRefTable.js @@ -0,0 +1,119 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.getXref = exports.getLastTrailerPosition = exports.getFullXrefTable = exports.default = void 0; + +var _SignPdfError = _interopRequireDefault(require("../../SignPdfError")); + +var _xrefToRefMap = _interopRequireDefault(require("./xrefToRefMap")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const getLastTrailerPosition = pdf => { + const trailerStart = pdf.lastIndexOf(Buffer.from('trailer', 'utf8')); + const trailer = pdf.slice(trailerStart, pdf.length - 6); + const xRefPosition = trailer.slice(trailer.lastIndexOf(Buffer.from('startxref', 'utf8')) + 10).toString(); + return parseInt(xRefPosition); +}; + +exports.getLastTrailerPosition = getLastTrailerPosition; + +const getXref = (pdf, position) => { + let refTable = pdf.slice(position); // slice starting from where xref starts + + const realPosition = refTable.indexOf(Buffer.from('xref', 'utf8')); + + if (realPosition === -1) { + throw new _SignPdfError.default(`Could not find xref anywhere at or after ${position}.`, _SignPdfError.default.TYPE_PARSE); + } + + if (realPosition > 0) { + const prefix = refTable.slice(0, realPosition); + + if (prefix.toString().replace(/\s*/g, '') !== '') { + throw new _SignPdfError.default(`Expected xref at ${position} but found other content.`, _SignPdfError.default.TYPE_PARSE); + } + } + + const nextEofPosition = refTable.indexOf(Buffer.from('%%EOF', 'utf8')); + + if (nextEofPosition === -1) { + throw new _SignPdfError.default('Expected EOF after xref and trailer but could not find one.', _SignPdfError.default.TYPE_PARSE); + } + + refTable = refTable.slice(0, nextEofPosition); + refTable = refTable.slice(realPosition + 4); // move ahead with the "xref" + + refTable = refTable.slice(refTable.indexOf('\n') + 1); // move after the next new line + // extract the size + + let size = refTable.toString().split('/Size')[1]; + + if (!size) { + throw new _SignPdfError.default('Size not found in xref table.', _SignPdfError.default.TYPE_PARSE); + } + + size = /^\s*(\d+)/.exec(size); + + if (size === null) { + throw new _SignPdfError.default('Failed to parse size of xref table.', _SignPdfError.default.TYPE_PARSE); + } + + size = parseInt(size[1]); + const [objects, infos] = refTable.toString().split('trailer'); + const isContainingPrev = infos.split('/Prev')[1] != null; + let prev; + + if (isContainingPrev) { + const pagesRefRegex = /Prev (\d+)/g; + const match = pagesRefRegex.exec(infos); + const [, prevPosition] = match; + prev = prevPosition; + } + + const xRefContent = (0, _xrefToRefMap.default)(objects); + return { + size, + prev, + xRefContent + }; +}; + +exports.getXref = getXref; + +const getFullXrefTable = pdf => { + const lastTrailerPosition = getLastTrailerPosition(pdf); + const lastXrefTable = getXref(pdf, lastTrailerPosition); + + if (lastXrefTable.prev === undefined) { + return lastXrefTable.xRefContent; + } + + const pdfWithoutLastTrailer = pdf.slice(0, lastTrailerPosition); + const partOfXrefTable = getFullXrefTable(pdfWithoutLastTrailer); + const mergedXrefTable = new Map([...partOfXrefTable, ...lastXrefTable.xRefContent]); + return mergedXrefTable; +}; +/** + * @param {Buffer} pdfBuffer + * @returns {object} + */ + + +exports.getFullXrefTable = getFullXrefTable; + +const readRefTable = pdf => { + const fullXrefTable = getFullXrefTable(pdf); + const startingIndex = 0; + const maxIndex = Math.max(...fullXrefTable.keys()); + return { + startingIndex, + maxIndex, + offsets: fullXrefTable + }; +}; + +var _default = readRefTable; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/xrefToRefMap.js b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/xrefToRefMap.js new file mode 100644 index 000000000..5c78727b5 --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/plainAddPlaceholder/xrefToRefMap.js @@ -0,0 +1,54 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _SignPdfError = _interopRequireDefault(require("../../SignPdfError")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const xrefToRefMap = xrefString => { + const lines = xrefString.split('\n').filter(l => l !== ''); + let index = 0; + let expectedLines = 0; + const xref = new Map(); + lines.forEach(line => { + const split = line.split(' '); + + if (split.length === 2) { + index = parseInt(split[0]); + expectedLines = parseInt(split[1]); + return; + } + + if (expectedLines <= 0) { + throw new _SignPdfError.default('Too many lines in xref table.', _SignPdfError.default.TYPE_PARSE); + } + + expectedLines -= 1; + const [offset,, inUse] = split; + + if (inUse.trim() === 'f') { + index += 1; + return; + } + + if (inUse.trim() !== 'n') { + throw new _SignPdfError.default(`Unknown in-use flag "${inUse}". Expected "n" or "f".`, _SignPdfError.default.TYPE_PARSE); + } + + if (!/^\d+$/.test(offset.trim())) { + throw new _SignPdfError.default(`Expected integer offset. Got "${offset}".`, _SignPdfError.default.TYPE_PARSE); + } + + const storeOffset = parseInt(offset.trim()); + xref.set(index, storeOffset); + index += 1; + }); + return xref; +}; + +var _default = xrefToRefMap; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/helpers/removeTrailingNewLine.js b/packages/signing/node-signpdf/dist/helpers/removeTrailingNewLine.js new file mode 100644 index 000000000..74f19c47a --- /dev/null +++ b/packages/signing/node-signpdf/dist/helpers/removeTrailingNewLine.js @@ -0,0 +1,48 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.default = void 0; + +var _SignPdfError = _interopRequireDefault(require("../SignPdfError")); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +const sliceLastChar = (pdf, character) => { + const lastChar = pdf.slice(pdf.length - 1).toString(); + + if (lastChar === character) { + return pdf.slice(0, pdf.length - 1); + } + + return pdf; +}; +/** + * Removes a trailing new line if there is such. + * + * Also makes sure the file ends with an EOF line as per spec. + * @param {Buffer} pdf + * @returns {Buffer} + */ + + +const removeTrailingNewLine = pdf => { + if (!(pdf instanceof Buffer)) { + throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } + + let output = pdf; + output = sliceLastChar(output, '\n'); + output = sliceLastChar(output, '\r'); + const lastLine = output.slice(output.length - 6).toString(); + + if (lastLine !== '\n%%EOF') { + throw new _SignPdfError.default('A PDF file must end with an EOF line.', _SignPdfError.default.TYPE_PARSE); + } + + return output; +}; + +var _default = removeTrailingNewLine; +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/node-signpdf/dist/signpdf.js b/packages/signing/node-signpdf/dist/signpdf.js new file mode 100644 index 000000000..936275e1e --- /dev/null +++ b/packages/signing/node-signpdf/dist/signpdf.js @@ -0,0 +1,193 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = { + SignPdf: true, + SignPdfError: true +}; +exports.SignPdf = void 0; +Object.defineProperty(exports, "SignPdfError", { + enumerable: true, + get: function () { + return _SignPdfError.default; + } +}); +exports.default = void 0; + +var _nodeForge = _interopRequireDefault(require("node-forge")); + +var _SignPdfError = _interopRequireDefault(require("./SignPdfError")); + +var _helpers = require("./helpers"); + +Object.keys(_helpers).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _helpers[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _helpers[key]; + } + }); +}); + +var _const = require("./helpers/const"); + +Object.keys(_const).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _const[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _const[key]; + } + }); +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +class SignPdf { + constructor() { + this.byteRangePlaceholder = _const.DEFAULT_BYTE_RANGE_PLACEHOLDER; + this.lastSignature = null; + } + + sign(pdfBuffer, p12Buffer, additionalOptions = {}) { + const options = { + asn1StrictParsing: false, + passphrase: '', + ...additionalOptions + }; + + if (!(pdfBuffer instanceof Buffer)) { + throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } + + if (!(p12Buffer instanceof Buffer)) { + throw new _SignPdfError.default('p12 certificate expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } + + let pdf = (0, _helpers.removeTrailingNewLine)(pdfBuffer); // Find the ByteRange placeholder. + + const { + byteRangePlaceholder + } = (0, _helpers.findByteRange)(pdf); + + if (!byteRangePlaceholder) { + throw new _SignPdfError.default(`Could not find empty ByteRange placeholder: ${byteRangePlaceholder}`, _SignPdfError.default.TYPE_PARSE); + } + + const byteRangePos = pdf.indexOf(byteRangePlaceholder); // Calculate the actual ByteRange that needs to replace the placeholder. + + const byteRangeEnd = byteRangePos + byteRangePlaceholder.length; + const contentsTagPos = pdf.indexOf('/Contents ', byteRangeEnd); + const placeholderPos = pdf.indexOf('<', contentsTagPos); + const placeholderEnd = pdf.indexOf('>', placeholderPos); + const placeholderLengthWithBrackets = placeholderEnd + 1 - placeholderPos; + const placeholderLength = placeholderLengthWithBrackets - 2; + const byteRange = [0, 0, 0, 0]; + byteRange[1] = placeholderPos; + byteRange[2] = byteRange[1] + placeholderLengthWithBrackets; + byteRange[3] = pdf.length - byteRange[2]; + let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`; + actualByteRange += ' '.repeat(byteRangePlaceholder.length - actualByteRange.length); // Replace the /ByteRange placeholder with the actual ByteRange + + pdf = Buffer.concat([pdf.slice(0, byteRangePos), Buffer.from(actualByteRange), pdf.slice(byteRangeEnd)]); // Remove the placeholder signature + + pdf = Buffer.concat([pdf.slice(0, byteRange[1]), pdf.slice(byteRange[2], byteRange[2] + byteRange[3])]); // Convert Buffer P12 to a forge implementation. + + const forgeCert = _nodeForge.default.util.createBuffer(p12Buffer.toString('binary')); + + const p12Asn1 = _nodeForge.default.asn1.fromDer(forgeCert); + + const p12 = _nodeForge.default.pkcs12.pkcs12FromAsn1(p12Asn1, options.asn1StrictParsing, options.passphrase); // Extract safe bags by type. + // We will need all the certificates and the private key. + + + const certBags = p12.getBags({ + bagType: _nodeForge.default.pki.oids.certBag + })[_nodeForge.default.pki.oids.certBag]; + + const keyBags = p12.getBags({ + bagType: _nodeForge.default.pki.oids.pkcs8ShroudedKeyBag + })[_nodeForge.default.pki.oids.pkcs8ShroudedKeyBag]; + + const privateKey = keyBags[0].key; // Here comes the actual PKCS#7 signing. + + const p7 = _nodeForge.default.pkcs7.createSignedData(); // Start off by setting the content. + + + p7.content = _nodeForge.default.util.createBuffer(pdf.toString('binary')); // Then add all the certificates (-cacerts & -clcerts) + // Keep track of the last found client certificate. + // This will be the public key that will be bundled in the signature. + + let certificate; + Object.keys(certBags).forEach(i => { + const { + publicKey + } = certBags[i].cert; + p7.addCertificate(certBags[i].cert); // Try to find the certificate that matches the private key. + + if (privateKey.n.compareTo(publicKey.n) === 0 && privateKey.e.compareTo(publicKey.e) === 0) { + certificate = certBags[i].cert; + } + }); + + if (typeof certificate === 'undefined') { + throw new _SignPdfError.default('Failed to find a certificate that matches the private key.', _SignPdfError.default.TYPE_INPUT); + } // Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects. + + + p7.addSigner({ + key: privateKey, + certificate, + digestAlgorithm: _nodeForge.default.pki.oids.sha256, + authenticatedAttributes: [{ + type: _nodeForge.default.pki.oids.contentType, + value: _nodeForge.default.pki.oids.data + }, { + type: _nodeForge.default.pki.oids.messageDigest // value will be auto-populated at signing time + + }, { + type: _nodeForge.default.pki.oids.signingTime, + // value can also be auto-populated at signing time + // We may also support passing this as an option to sign(). + // Would be useful to match the creation time of the document for example. + value: new Date() + }] + }); // Sign in detached mode. + + p7.sign({ + detached: true + }); // Check if the PDF has a good enough placeholder to fit the signature. + + const raw = _nodeForge.default.asn1.toDer(p7.toAsn1()).getBytes(); // placeholderLength represents the length of the HEXified symbols but we're + // checking the actual lengths. + + + if (raw.length * 2 > placeholderLength) { + throw new _SignPdfError.default(`Signature exceeds placeholder length: ${raw.length * 2} > ${placeholderLength}`, _SignPdfError.default.TYPE_INPUT); + } + + let signature = Buffer.from(raw, 'binary').toString('hex'); // Store the HEXified signature. At least useful in tests. + + this.lastSignature = signature; // Pad the signature with zeroes so the it is the same length as the placeholder + + signature += Buffer.from(String.fromCharCode(0).repeat(placeholderLength / 2 - raw.length)).toString('hex'); // Place it in the document. + + pdf = Buffer.concat([pdf.slice(0, byteRange[1]), Buffer.from(`<${signature}>`), pdf.slice(byteRange[1])]); // Magic. Done. + + return pdf; + } + +} + +exports.SignPdf = SignPdf; + +var _default = new SignPdf(); + +exports.default = _default; \ No newline at end of file diff --git a/packages/signing/signpdf.js b/packages/signing/signpdf.js new file mode 100644 index 000000000..9e06b6028 --- /dev/null +++ b/packages/signing/signpdf.js @@ -0,0 +1,193 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var _exportNames = { + SignPdf: true, + SignPdfError: true +}; +exports.SignPdf = void 0; +Object.defineProperty(exports, "SignPdfError", { + enumerable: true, + get: function () { + return _SignPdfError.default; + } +}); +exports.default = void 0; + +var _nodeForge = _interopRequireDefault(require("node-forge")); + +var _SignPdfError = _interopRequireDefault(require("./SignPdfError")); + +var _helpers = require("./helpers"); + +Object.keys(_helpers).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _helpers[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _helpers[key]; + } + }); +}); + +var _const = require("./helpers/const"); + +Object.keys(_const).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; + if (key in exports && exports[key] === _const[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _const[key]; + } + }); +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +class SignPdf { + constructor() { + this.byteRangePlaceholder = _const.DEFAULT_BYTE_RANGE_PLACEHOLDER; + this.lastSignature = null; + } + + sign(pdfBuffer, p12Buffer, additionalOptions = {}) { + const options = { + asn1StrictParsing: false, + passphrase: '', + ...additionalOptions + }; + + if (!(pdfBuffer instanceof Buffer)) { + throw new _SignPdfError.default('PDF expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } + + if (!(p12Buffer instanceof Buffer)) { + throw new _SignPdfError.default('p12 certificate expected as Buffer.', _SignPdfError.default.TYPE_INPUT); + } + + let pdf = (0, _helpers.removeTrailingNewLine)(pdfBuffer); // Find the ByteRange placeholder. + + const { + byteRangePlaceholder + } = (0, _helpers.findByteRange)(pdf); + + if (!byteRangePlaceholder) { + throw new _SignPdfError.default(`Could not find empty ByteRange placeholder: ${byteRangePlaceholder}`, _SignPdfError.default.TYPE_PARSE); + } + + const byteRangePos = pdf.indexOf(byteRangePlaceholder); // Calculate the actual ByteRange that needs to replace the placeholder. + + const byteRangeEnd = byteRangePos + byteRangePlaceholder.length; + const contentsTagPos = pdf.indexOf('/Contents ', byteRangeEnd); + const placeholderPos = pdf.indexOf('<', contentsTagPos); + const placeholderEnd = pdf.indexOf('>', placeholderPos); + const placeholderLengthWithBrackets = placeholderEnd + 1 - placeholderPos; + const placeholderLength = placeholderLengthWithBrackets - 2; + const byteRange = [0, 0, 0, 0]; + byteRange[1] = placeholderPos; + byteRange[2] = byteRange[1] + placeholderLengthWithBrackets; + byteRange[3] = pdf.length - byteRange[2]; + let actualByteRange = `/ByteRange [${byteRange.join(' ')}]`; + actualByteRange += ' '.repeat(byteRangePlaceholder.length - actualByteRange.length); // Replace the /ByteRange placeholder with the actual ByteRange + + pdf = Buffer.concat([pdf.slice(0, byteRangePos), Buffer.from(actualByteRange), pdf.slice(byteRangeEnd)]); // Remove the placeholder signature + + pdf = Buffer.concat([pdf.slice(0, byteRange[1]), pdf.slice(byteRange[2], byteRange[2] + byteRange[3])]); // Convert Buffer P12 to a forge implementation. + + const forgeCert = _nodeForge.default.util.createBuffer(p12Buffer.toString('binary')); + + const p12Asn1 = _nodeForge.default.asn1.fromDer(forgeCert); + + const p12 = _nodeForge.default.pkcs12.pkcs12FromAsn1(p12Asn1, options.asn1StrictParsing, options.passphrase); // Extract safe bags by type. + // We will need all the certificates and the private key. + + + const certBags = p12.getBags({ + bagType: _nodeForge.default.pki.oids.certBag + })[_nodeForge.default.pki.oids.certBag]; + + const keyBags = p12.getBags({ + bagType: _nodeForge.default.pki.oids.pkcs8ShroudedKeyBag + })[_nodeForge.default.pki.oids.pkcs8ShroudedKeyBag]; + + const privateKey = keyBags[0].key; // Here comes the actual PKCS#7 signing. + + const p7 = _nodeForge.default.pkcs7.createSignedData(); // Start off by setting the content. + + + p7.content = _nodeForge.default.util.createBuffer(pdf.toString('binary')); // Then add all the certificates (-cacerts & -clcerts) + // Keep track of the last found client certificate. + // This will be the public key that will be bundled in the signature. + + let certificate; + Object.keys(certBags).forEach(i => { + const { + publicKey + } = certBags[i].cert; + p7.addCertificate(certBags[i].cert); // Try to find the certificate that matches the private key. + + if (privateKey.n.compareTo(publicKey.n) === 0 && privateKey.e.compareTo(publicKey.e) === 0) { + certificate = certBags[i].cert; + } + }); + + if (typeof certificate === 'undefined') { + throw new _SignPdfError.default('Failed to find a certificate that matches the private key.', _SignPdfError.default.TYPE_INPUT); + } // Add a sha256 signer. That's what Adobe.PPKLite adbe.pkcs7.detached expects. + + + p7.addSigner({ + key: privateKey, + certificate, + digestAlgorithm: _nodeForge.default.pki.oids.sha256, + authenticatedAttributes: [{ + type: _nodeForge.default.pki.oids.contentType, + value: _nodeForge.default.pki.oids.data + }, { + type: _nodeForge.default.pki.oids.signingTime, + // value can also be auto-populated at signing time + // We may also support passing this as an option to sign(). + // Would be useful to match the creation time of the document for example. + value: new Date() + }, { + type: _nodeForge.default.pki.oids.messageDigest // value will be auto-populated at signing time + + }] + }); // Sign in detached mode. + + p7.sign({ + detached: true + }); // Check if the PDF has a good enough placeholder to fit the signature. + + const raw = _nodeForge.default.asn1.toDer(p7.toAsn1()).getBytes(); // placeholderLength represents the length of the HEXified symbols but we're + // checking the actual lengths. + + + if (raw.length * 2 > placeholderLength) { + throw new _SignPdfError.default(`Signature exceeds placeholder length: ${raw.length * 2} > ${placeholderLength}`, _SignPdfError.default.TYPE_INPUT); + } + + let signature = Buffer.from(raw, 'binary').toString('hex'); // Store the HEXified signature. At least useful in tests. + + this.lastSignature = signature; // Pad the signature with zeroes so the it is the same length as the placeholder + + signature += Buffer.from(String.fromCharCode(0).repeat(placeholderLength / 2 - raw.length)).toString('hex'); // Place it in the document. + + pdf = Buffer.concat([pdf.slice(0, byteRange[1]), Buffer.from(`<${signature}>`), pdf.slice(byteRange[1])]); // Magic. Done. + + return pdf; + } + +} + +exports.SignPdf = SignPdf; + +var _default = new SignPdf(); + +exports.default = _default; \ No newline at end of file