From eb38024c20942634bf19c5a45206761b5c40d262 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 1 Mar 2023 18:04:59 +0100 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9A=97=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signing/signDocument.ts | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/signing/signDocument.ts b/packages/signing/signDocument.ts index fba752b0a..69280acb8 100644 --- a/packages/signing/signDocument.ts +++ b/packages/signing/signDocument.ts @@ -1,4 +1,26 @@ -export const signDocument = (documentAsBase64: string): string => { - - return documentAsBase64; +const signer = require("../../node_modules/node-signpdf/dist/signpdf"); +const { + pdfkitAddPlaceholder, +} = require("../../node_modules/node-signpdf/dist/helpers/pdfkitAddPlaceholder"); +import * as fs from "fs"; + +export const signDocument = (documentAsBase64: string): any => { + const pdfBuffer = Buffer.from(documentAsBase64, "base64"); + const certBuffer = fs.readFileSync("public/certificate.p12"); + + console.log("adding placeholder.."); + console.log(signer.pdfkitAddPlaceholder); + const inputBuffer = signer.pdfkitAddPlaceholder({ + pdfBuffer, + reason: "Signed Certificate.", + contactInfo: "sign@example.com", + name: "Example", + location: "Jakarta", + signatureLength: certBuffer.length, + }); + + console.log("signing.."); + const signedPdf = new signer.SignPdf().sign(inputBuffer, certBuffer); + + return signedPdf; }; From cfb0d94e84c85cd73c39d2ec6822b331e28bbfe7 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 1 Mar 2023 18:27:19 +0100 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=9A=9A=20=E2=9E=95=20=20add=20node-?= =?UTF-8?q?signpdf=20copy=20to=20signing=20packgage=20until=20npm=20includ?= =?UTF-8?q?es=20fix?= 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 From 2dc6164119749ea193fd4898b92b4c26fcb31d58 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 1 Mar 2023 18:28:05 +0100 Subject: [PATCH 03/11] =?UTF-8?q?=F0=9F=90=9B=20invalid=20pades=20signatur?= =?UTF-8?q?e=20fix=20node-sign=20pdf=20locally?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signing/node-signpdf/dist/signpdf.js | 6 +- packages/signing/signpdf.js | 193 ------------------ 2 files changed, 3 insertions(+), 196 deletions(-) delete mode 100644 packages/signing/signpdf.js diff --git a/packages/signing/node-signpdf/dist/signpdf.js b/packages/signing/node-signpdf/dist/signpdf.js index 936275e1e..9e06b6028 100644 --- a/packages/signing/node-signpdf/dist/signpdf.js +++ b/packages/signing/node-signpdf/dist/signpdf.js @@ -149,15 +149,15 @@ class SignPdf { 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() + }, { + type: _nodeForge.default.pki.oids.messageDigest // value will be auto-populated at signing time + }] }); // Sign in detached mode. diff --git a/packages/signing/signpdf.js b/packages/signing/signpdf.js deleted file mode 100644 index 9e06b6028..000000000 --- a/packages/signing/signpdf.js +++ /dev/null @@ -1,193 +0,0 @@ -"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 From 02e225517cf447f12042bc15edbad5d2ea81c064 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Wed, 1 Mar 2023 18:47:08 +0100 Subject: [PATCH 04/11] Documenso self signed certificate --- apps/web/ressources/certificate.p12 | Bin 0 -> 2637 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 apps/web/ressources/certificate.p12 diff --git a/apps/web/ressources/certificate.p12 b/apps/web/ressources/certificate.p12 new file mode 100644 index 0000000000000000000000000000000000000000..c36ca6ecf08560aebdfab366370b400d7a57282b GIT binary patch literal 2637 zcmV-T3bOSuf(l6j0Ru3C3J(SeDuzgg_YDCD0ic2k00e>w{4jzE_%MP5uLcP!hDe6@ z4FLxRpn?RTFoFb}0s#Opf&`TY2`Yw2hW8Bt2LUh~1_~;MNQUH(gbXp-mdH0I_i$h52)6Y^Yl}&>Xl*nd%YHTbA(=AXrPZk1&6AhKQC`;S z+|%>~MGjgR^d`8IqTJP-lXZ&iy>S}e)I4P2&nu_zRuUY`3PK;eHiM7xTjt17dT%mY z{G5cVpK+p1l+LiZZW_LrHq8PuNMz7J4eC5n{tl573q3T4k->-!?Rob$V#2KFhvJ%_ z)B&fA|9lZY=zRc8JNR=o)36s^iUU8;5EJW*A*z4+IX`dfVlM9qp}CBJ30N8Bm*Pv zF54s8gfKCxTg-@I0V+FG2ZJEq>>xceG`hnN`)_WX7(oNKVoJ(dzfZGmpHL*#dCT2l zl1@qwp>_dTOW2iZqdN-*q6Nj&pooq|IjBq%P~BhZtua0eM6F%#(Qo-JoXJ*?zaKv% z@~Y1ZGX?Cc0ncU1ch5VFg;jEKM*A;QK#)0kK1#^YhRNJ6?tD zvpI4Gu4(#a7*Xh=>*}fGw+@b!#C@qfKwPNMNfzYr>Fq{vaDAHCQwpqL0SARg=y5|Z zg;Ny)o%9VbLpM(|MsTBuQ*WZdt;MRnWCwj`BG(W+jmYa5NW6wPOWzm>aF)>n8Vyqu ztgxWb9>R+4hN{4h?3q+ndw0k{{oxpST=(SFIHnI-?&*6iE^nvAX2*AnQPjc4>8ac% z2aNnauWr68e5#Q1cD27=U!de1(|=OScOIpei-_!D{4A_YHIeGV$4|FgOlf)Q=x(fF zrG_7ySC3!(nl-MbnJntXgg%+dRDS?;6kx@zyDrvlX`pREw&>G{qQ=T>Xck1pXHt)3 z_J+lHe!Aj_-tNsDTu~?YX7aGdy4}1)lk_Lv4oJp}w2e5>Foe{_#+YwDcF&@5SX zz^2p|V`D}1zk+~*ESUn?OX{SPl0IW=f`9cY-IAPk zFhs6`gq6vX^4D+7kavJm&fsDm9}}CLEp271}XiAYmi`W!%w-# z=3GU{92VyBKCu2Wf&LNQURIc+3pO?I21P9sNNl$@%B3 zf0eq*55Z`P5e6hrt7Jscb%En05#JaMrOOqKGD2%x$E<$;qQ!Xj#zqnHg_1wt;j%(~ z^Xb`yT|`)5hZaT%vs`=vW;*>{{`9~X!urhi+XxFm6(Sm#?cdc~-nd?C3*-25rb}VH zbOeV618OV|^jHrrF|Pc3-}Wjpk{+2LWycqN?B?B58Dq_KtcDkoF%B7Abk16o3 zU?bzIYceHM7@-%<2r?Quc>{;XlVu_f%W@;o8k&nTKH3xgW+f;Aq{}yfY~t(PG+-c8 z$!e}Qjvw|pVPxoqZqj3t#8~GKfx@M}QHQ6NV<+|uX#(9xsn_@nUW?;&@JhqN5t@{u z^J|`crojl&0}*wzdl+=17eri>V^1(?@GQpp|B;dEGm~?P%tBC~w_F^_&e;LK^r?AT z?j+6^k-7Q1dYGnmy}3DE=J`6al}ZBtKREaJ>q2+H30dM=Ul|y7+f*=K=CJ($I2ZT2 zGZ>!q=>X5jXsIEECRm?_(v{kRz6qNqdv+(lYNMv&213j-Dj>4)d$X|n&*rIFCYXDv zqXovh;@!tzrT51zRv%P!kr)U&`Z>Rw7e(7C4{rIaRJbjwf{f2L$3i|3)Y1$D@rD<}a+-Nlci?%@J6juUNMYIiV zC)3PWJJ5jmHDjFLRCGw1=y>J**d4H+yP9L4NNo5uPTsrTTewDCj7}sI=~AU;6AVW` z6dv=vyV-3(6yW_sZ#lp;EetQa7WA5AyJZv!MpK(Nvd^|&G6L{dKX0N;w3Kp> z5+DJ|z`V}@LAZ)Du^JJ9LgjsZ&ofgSw{_?4UUX}u!Cf<(c5J;f!l`1@zS02k+X6x> zRYOy-918EPActgnHyrs6M3sOek{i1mx5|t5f=U(?G7A1`pUP#UjjTwP-%WaD~j7 zxg=DwnCU1-H+LHKG&Xja8VK-o{jMw*UhR3Zl z2FM?+)X8~zQ8KN|D)fE7n`r1|*qgopMVw~)cNig$v=Bfn!LN^eAOn+vQd$Ob_mD(t znGYZklX@{FFe3&DDuzgg_YDCF6)_eB6qbVQbZ@H;HlEfYt^}p| Date: Wed, 1 Mar 2023 19:54:22 +0100 Subject: [PATCH 05/11] =?UTF-8?q?=E2=9C=A8=F0=9F=9A=A7=20signDocument=20us?= =?UTF-8?q?ing=20node=20signpdf=20and=20custom=20placeholder=20insert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/next.config.js | 1 + apps/web/package.json | 3 + apps/web/pages/api/documents/[id].ts | 7 ++- apps/web/pages/api/test-sign/[id].ts | 30 +++++++++ package-lock.json | 36 +++++++++++ packages/signing/PDFArrayCustom.js | 55 +++++++++++++++++ packages/signing/signDocument.ts | 92 ++++++++++++++++++++++------ 7 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 apps/web/pages/api/test-sign/[id].ts create mode 100644 packages/signing/PDFArrayCustom.js diff --git a/apps/web/next.config.js b/apps/web/next.config.js index ef2a49e57..3043c0962 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -12,6 +12,7 @@ const withTM = require("next-transpile-modules")([ "@documenso/ui", "@documenso/pdf", "@documenso/features", + "@documenso/signing", "react-signature-canvas", ]); const plugins = []; diff --git a/apps/web/package.json b/apps/web/package.json index b3794df66..28073a93a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -32,9 +32,12 @@ "next": "13.0.3", "next-auth": "^4.18.3", "next-transpile-modules": "^10.0.0", + "node-forge": "^1.3.1", + "node-signpdf": "^1.5.0", "nodemailer": "^6.9.0", "nodemailer-sendgrid": "^1.0.3", "npm": "^9.1.3", + "pdf-lib": "^1.17.1", "placeholder-loading": "^0.6.0", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/apps/web/pages/api/documents/[id].ts b/apps/web/pages/api/documents/[id].ts index e462d6bc7..11a924ad8 100644 --- a/apps/web/pages/api/documents/[id].ts +++ b/apps/web/pages/api/documents/[id].ts @@ -7,6 +7,7 @@ import prisma from "@documenso/prisma"; import { NextApiRequest, NextApiResponse } from "next"; import { Document as PrismaDocument } from "@prisma/client"; import { getDocument } from "@documenso/lib/query"; +import { signDocument } from "@documenso/signing/signDocument"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { const user = await getUserFromToken(req, res); @@ -24,7 +25,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { if (!document) res.status(404).end(`No document with id ${documentId} found.`); - const buffer: Buffer = Buffer.from(document.document.toString(), "base64"); + const signedDocumentAsBase64 = await signDocument( + document.document.toString() + ); + + const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); res.setHeader("Content-Type", "application/pdf"); res.setHeader( "Content-Disposition", diff --git a/apps/web/pages/api/test-sign/[id].ts b/apps/web/pages/api/test-sign/[id].ts new file mode 100644 index 000000000..1a41838a1 --- /dev/null +++ b/apps/web/pages/api/test-sign/[id].ts @@ -0,0 +1,30 @@ +import { + defaultHandler, + defaultResponder, + getUserFromToken, +} from "@documenso/lib/server"; +import prisma from "@documenso/prisma"; +import { NextApiRequest, NextApiResponse } from "next"; +import { Document as PrismaDocument } from "@prisma/client"; +import { getDocument } from "@documenso/lib/query"; +import { signDocument } from "@documenso/signing/signDocument"; + +async function getHandler(req: NextApiRequest, res: NextApiResponse) { + const documentId = req.query.id || 1; + const document: PrismaDocument = await getDocument(+documentId, req, res); + const signedDocumentAsBase64 = await signDocument(document.document.toString()); + const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); + res.setHeader("Content-Type", "application/pdf"); + res.setHeader( + "Content-Disposition", + `attachment; filename=${document.title}` + ); + res.setHeader("Content-Length", buffer.length); + + res.status(200).send(buffer); + return; +} + +export default defaultHandler({ + GET: Promise.resolve({ default: defaultResponder(getHandler) }), +}); diff --git a/package-lock.json b/package-lock.json index c87db62a1..8255b1eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,9 +68,12 @@ "next": "13.0.3", "next-auth": "^4.18.3", "next-transpile-modules": "^10.0.0", + "node-forge": "^1.3.1", + "node-signpdf": "^1.5.0", "nodemailer": "^6.9.0", "nodemailer-sendgrid": "^1.0.3", "npm": "^9.1.3", + "pdf-lib": "^1.17.1", "placeholder-loading": "^0.6.0", "react": "18.2.0", "react-dom": "18.2.0", @@ -3959,11 +3962,30 @@ "enhanced-resolve": "^5.10.0" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-releases": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" }, + "node_modules/node-signpdf": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-1.5.0.tgz", + "integrity": "sha512-zDOduHZGadYUpCGd1JUtOHgVn6QTW6t1feMKNe3NLexsGQXDWFjOr1mGTjqTq1UWXAp47T4ShllLnmhGu9GQsA==", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "node-forge": "^1.2.1" + } + }, "node_modules/nodemailer": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz", @@ -8659,9 +8681,12 @@ "next": "13.0.3", "next-auth": "^4.18.3", "next-transpile-modules": "^10.0.0", + "node-forge": "^1.3.1", + "node-signpdf": "^1.5.0", "nodemailer": "^6.9.0", "nodemailer-sendgrid": "^1.0.3", "npm": "^9.1.3", + "pdf-lib": "*", "placeholder-loading": "^0.6.0", "postcss": "^8.4.19", "react": "18.2.0", @@ -11461,11 +11486,22 @@ "enhanced-resolve": "^5.10.0" } }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" + }, "node-releases": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" }, + "node-signpdf": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/node-signpdf/-/node-signpdf-1.5.0.tgz", + "integrity": "sha512-zDOduHZGadYUpCGd1JUtOHgVn6QTW6t1feMKNe3NLexsGQXDWFjOr1mGTjqTq1UWXAp47T4ShllLnmhGu9GQsA==", + "requires": {} + }, "nodemailer": { "version": "6.9.0", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz", diff --git a/packages/signing/PDFArrayCustom.js b/packages/signing/PDFArrayCustom.js new file mode 100644 index 000000000..7981faad8 --- /dev/null +++ b/packages/signing/PDFArrayCustom.js @@ -0,0 +1,55 @@ +const { PDFArray, CharCodes } = require("pdf-lib"); + +/** + * Extends PDFArray class in order to make ByteRange look like this: + * /ByteRange [0 /********** /********** /**********] + * Not this: + * /ByteRange [ 0 /********** /********** /********** ] + */ +class PDFArrayCustom extends PDFArray { + static withContext(context) { + return new PDFArrayCustom(context); + } + + clone(context) { + const clone = PDFArrayCustom.withContext(context || this.context); + for (let idx = 0, len = this.size(); idx < len; idx++) { + clone.push(this.array[idx]); + } + return clone; + } + + toString() { + let arrayString = "["; + for (let idx = 0, len = this.size(); idx < len; idx++) { + arrayString += this.get(idx).toString(); + if (idx < len - 1) arrayString += " "; + } + arrayString += "]"; + return arrayString; + } + + sizeInBytes() { + let size = 2; + for (let idx = 0, len = this.size(); idx < len; idx++) { + size += this.get(idx).sizeInBytes(); + if (idx < len - 1) size += 1; + } + return size; + } + + copyBytesInto(buffer, offset) { + const initialOffset = offset; + + buffer[offset++] = CharCodes.LeftSquareBracket; + for (let idx = 0, len = this.size(); idx < len; idx++) { + offset += this.get(idx).copyBytesInto(buffer, offset); + if (idx < len - 1) buffer[offset++] = CharCodes.Space; + } + buffer[offset++] = CharCodes.RightSquareBracket; + + return offset - initialOffset; + } +} + +module.exports = PDFArrayCustom; diff --git a/packages/signing/signDocument.ts b/packages/signing/signDocument.ts index 69280acb8..fe9448b9c 100644 --- a/packages/signing/signDocument.ts +++ b/packages/signing/signDocument.ts @@ -1,26 +1,78 @@ -const signer = require("../../node_modules/node-signpdf/dist/signpdf"); -const { - pdfkitAddPlaceholder, -} = require("../../node_modules/node-signpdf/dist/helpers/pdfkitAddPlaceholder"); -import * as fs from "fs"; +const fs = require("fs"); +const signer = require("./node-signpdf/dist/signpdf"); +import { + PDFDocument, + PDFName, + PDFNumber, + PDFHexString, + PDFString, +} from "pdf-lib"; -export const signDocument = (documentAsBase64: string): any => { +export const signDocument = async (documentAsBase64: string): Promise => { + // Custom code to add Byterange to PDF + const PDFArrayCustom = require("./PDFArrayCustom"); + + // The PDF we're going to sign const pdfBuffer = Buffer.from(documentAsBase64, "base64"); - const certBuffer = fs.readFileSync("public/certificate.p12"); - console.log("adding placeholder.."); - console.log(signer.pdfkitAddPlaceholder); - const inputBuffer = signer.pdfkitAddPlaceholder({ - pdfBuffer, - reason: "Signed Certificate.", - contactInfo: "sign@example.com", - name: "Example", - location: "Jakarta", - signatureLength: certBuffer.length, + // The p12 certificate we're going to sign with + const p12Buffer = fs.readFileSync("ressources/certificate.p12"); + + const SIGNATURE_LENGTH = 4540; + + const pdfDoc = await PDFDocument.load(pdfBuffer); + const pages = pdfDoc.getPages(); + + const ByteRange = PDFArrayCustom.withContext(pdfDoc.context); + ByteRange.push(PDFNumber.of(0)); + ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER)); + ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER)); + ByteRange.push(PDFName.of(signer.DEFAULT_BYTE_RANGE_PLACEHOLDER)); + + const signatureDict = pdfDoc.context.obj({ + Type: "Sig", + Filter: "Adobe.PPKLite", + SubFilter: "adbe.pkcs7.detached", + ByteRange, + Contents: PDFHexString.of("A".repeat(SIGNATURE_LENGTH)), + Reason: PDFString.of("Signed by Documenso"), + M: PDFString.fromDate(new Date()), + }); + const signatureDictRef = pdfDoc.context.register(signatureDict); + + const widgetDict = pdfDoc.context.obj({ + Type: "Annot", + Subtype: "Widget", + FT: "Sig", + Rect: [0, 0, 0, 0], + V: signatureDictRef, + T: PDFString.of("Signature1"), + F: 4, + P: pages[0].ref, + }); + const widgetDictRef = pdfDoc.context.register(widgetDict); + + // Add our signature widget to the first page + pages[0].node.set(PDFName.of("Annots"), pdfDoc.context.obj([widgetDictRef])); + + // Create an AcroForm object containing our signature widget + pdfDoc.catalog.set( + PDFName.of("AcroForm"), + pdfDoc.context.obj({ + SigFlags: 3, + Fields: [widgetDictRef], + }) + ); + + const modifiedPdfBytes = await pdfDoc.save({ useObjectStreams: false }); + const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes); + + const signObj = new signer.SignPdf(); + const signedPdfBuffer = signObj.sign(modifiedPdfBuffer, p12Buffer, { + passphrase: "", }); - console.log("signing.."); - const signedPdf = new signer.SignPdf().sign(inputBuffer, certBuffer); - - return signedPdf; + // Write the signed file + // fs.writeFileSync("./signed.pdf", signedPdfBuffer); + return signedPdfBuffer; }; From 7980ed4998e5477a7ba54a211761c5df24ca9dce Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 2 Mar 2023 17:04:51 +0100 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/pages/api/documents/[id].ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/pages/api/documents/[id].ts b/apps/web/pages/api/documents/[id].ts index 11a924ad8..94cf67c5c 100644 --- a/apps/web/pages/api/documents/[id].ts +++ b/apps/web/pages/api/documents/[id].ts @@ -31,11 +31,11 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); res.setHeader("Content-Type", "application/pdf"); + res.setHeader("Content-Length", buffer.length); res.setHeader( "Content-Disposition", `attachment; filename=${document.title}` ); - res.setHeader("Content-Length", buffer.length); res.status(200).send(buffer); return; From c6cdb116e3df9375eb96c908f735659e80e373ab Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 2 Mar 2023 17:54:19 +0100 Subject: [PATCH 07/11] =?UTF-8?q?=F0=9F=9A=B8=20=F0=9F=9A=9A=20rename=20si?= =?UTF-8?q?gnDocument=20to=20addDigitalSignature,=20sign=20only=20when=20s?= =?UTF-8?q?ignatures=20exist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/pages/api/documents/[id].ts | 19 +++++++++++++++---- apps/web/pages/api/test-sign/[id].ts | 6 ++++-- ...signDocument.ts => addDigitalSignature.ts} | 4 +++- 3 files changed, 22 insertions(+), 7 deletions(-) rename packages/signing/{signDocument.ts => addDigitalSignature.ts} (96%) diff --git a/apps/web/pages/api/documents/[id].ts b/apps/web/pages/api/documents/[id].ts index 94cf67c5c..48284a6f4 100644 --- a/apps/web/pages/api/documents/[id].ts +++ b/apps/web/pages/api/documents/[id].ts @@ -7,7 +7,7 @@ import prisma from "@documenso/prisma"; import { NextApiRequest, NextApiResponse } from "next"; import { Document as PrismaDocument } from "@prisma/client"; import { getDocument } from "@documenso/lib/query"; -import { signDocument } from "@documenso/signing/signDocument"; +import { addDigitalSignature } from "@documenso/signing/addDigitalSignature"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { const user = await getUserFromToken(req, res); @@ -25,9 +25,20 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { if (!document) res.status(404).end(`No document with id ${documentId} found.`); - const signedDocumentAsBase64 = await signDocument( - document.document.toString() - ); + const signaturesCount = await prisma.signature.count({ + where: { + Field: { + documentId: document.id, + }, + }, + }); + + let signedDocumentAsBase64 = document.document; + + // No need to add a signature, if no one signed yet. + if (signaturesCount > 0) { + signedDocumentAsBase64 = await addDigitalSignature(document.document); + } const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); res.setHeader("Content-Type", "application/pdf"); diff --git a/apps/web/pages/api/test-sign/[id].ts b/apps/web/pages/api/test-sign/[id].ts index 1a41838a1..7c38f1a24 100644 --- a/apps/web/pages/api/test-sign/[id].ts +++ b/apps/web/pages/api/test-sign/[id].ts @@ -7,12 +7,14 @@ import prisma from "@documenso/prisma"; import { NextApiRequest, NextApiResponse } from "next"; import { Document as PrismaDocument } from "@prisma/client"; import { getDocument } from "@documenso/lib/query"; -import { signDocument } from "@documenso/signing/signDocument"; +import { addDigitalSignature } from "@documenso/signing/addDigitalSignature"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { const documentId = req.query.id || 1; const document: PrismaDocument = await getDocument(+documentId, req, res); - const signedDocumentAsBase64 = await signDocument(document.document.toString()); + const signedDocumentAsBase64 = await addDigitalSignature( + document.document.toString() + ); const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); res.setHeader("Content-Type", "application/pdf"); res.setHeader( diff --git a/packages/signing/signDocument.ts b/packages/signing/addDigitalSignature.ts similarity index 96% rename from packages/signing/signDocument.ts rename to packages/signing/addDigitalSignature.ts index fe9448b9c..e37401202 100644 --- a/packages/signing/signDocument.ts +++ b/packages/signing/addDigitalSignature.ts @@ -8,7 +8,9 @@ import { PDFString, } from "pdf-lib"; -export const signDocument = async (documentAsBase64: string): Promise => { +export const addDigitalSignature = async ( + documentAsBase64: string +): Promise => { // Custom code to add Byterange to PDF const PDFArrayCustom = require("./PDFArrayCustom"); From 07dca8a2db668dc5ed1934703c72480b666dcfa2 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 2 Mar 2023 17:54:59 +0100 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=90=9B=F0=9F=A7=B9=20also=20add=20d?= =?UTF-8?q?igital=20signature=20before=20sending=20out=20completed=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/lib/mail/sendSigningDoneMail.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/lib/mail/sendSigningDoneMail.ts b/packages/lib/mail/sendSigningDoneMail.ts index f2af85c4f..a0a52617f 100644 --- a/packages/lib/mail/sendSigningDoneMail.ts +++ b/packages/lib/mail/sendSigningDoneMail.ts @@ -1,7 +1,7 @@ import { sendMail } from "./sendMail"; -import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants"; import { signingCompleteTemplate } from "@documenso/lib/mail"; import { Document as PrismaDocument } from "@prisma/client"; +import { addDigitalSignature } from "@documenso/signing/addDigitalSignature"; export const sendSigningDoneMail = async ( recipient: any, @@ -16,7 +16,10 @@ export const sendSigningDoneMail = async ( [ { filename: document.title, - content: Buffer.from(document.document.toString(), "base64"), + content: Buffer.from( + await addDigitalSignature(document.document), + "base64" + ), }, ] ); From b178a4ed58f0150bd4e01867cc42e71bee87c972 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 2 Mar 2023 18:42:38 +0100 Subject: [PATCH 09/11] =?UTF-8?q?=F0=9F=8E=A8=20=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/pages/api/documents/[id].ts | 3 +-- apps/web/pages/api/health.ts | 9 +++------ apps/web/pages/api/test-sign/[id].ts | 10 +++------- packages/signing/addDigitalSignature.ts | 13 +++---------- 4 files changed, 10 insertions(+), 25 deletions(-) diff --git a/apps/web/pages/api/documents/[id].ts b/apps/web/pages/api/documents/[id].ts index 48284a6f4..d969ec348 100644 --- a/apps/web/pages/api/documents/[id].ts +++ b/apps/web/pages/api/documents/[id].ts @@ -48,8 +48,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) { `attachment; filename=${document.title}` ); - res.status(200).send(buffer); - return; + return res.status(200).send(buffer); } async function deleteHandler(req: NextApiRequest, res: NextApiResponse) { diff --git a/apps/web/pages/api/health.ts b/apps/web/pages/api/health.ts index 1e8db5c37..16432b87d 100644 --- a/apps/web/pages/api/health.ts +++ b/apps/web/pages/api/health.ts @@ -3,15 +3,12 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { defaultHandler, defaultResponder } from "@documenso/lib/server"; import prisma from "@documenso/prisma"; -type responseData = { - status: string; -}; - // Return a healthy 200 status code for uptime monitoring and render.com zero-downtime-deploy async function getHandler(req: NextApiRequest, res: NextApiResponse) { - // A generic database access to make sure the service is healthy. + // Some generic database access to make sure the service is healthy. const users = await prisma.user.findFirst(); - res.status(200).json({ message: "Api up and running :)" }); + + return res.status(200).json({ message: "Api up and running :)" }); } export default defaultHandler({ diff --git a/apps/web/pages/api/test-sign/[id].ts b/apps/web/pages/api/test-sign/[id].ts index 7c38f1a24..ed3aab2b1 100644 --- a/apps/web/pages/api/test-sign/[id].ts +++ b/apps/web/pages/api/test-sign/[id].ts @@ -12,19 +12,15 @@ import { addDigitalSignature } from "@documenso/signing/addDigitalSignature"; async function getHandler(req: NextApiRequest, res: NextApiResponse) { const documentId = req.query.id || 1; const document: PrismaDocument = await getDocument(+documentId, req, res); - const signedDocumentAsBase64 = await addDigitalSignature( - document.document.toString() - ); - const buffer: Buffer = Buffer.from(signedDocumentAsBase64, "base64"); + const signedDocument = await addDigitalSignature(document.document); res.setHeader("Content-Type", "application/pdf"); + res.setHeader("Content-Length", signedDocument.length); res.setHeader( "Content-Disposition", `attachment; filename=${document.title}` ); - res.setHeader("Content-Length", buffer.length); - res.status(200).send(buffer); - return; + return res.status(200).send(signedDocument); } export default defaultHandler({ diff --git a/packages/signing/addDigitalSignature.ts b/packages/signing/addDigitalSignature.ts index e37401202..e41e1e180 100644 --- a/packages/signing/addDigitalSignature.ts +++ b/packages/signing/addDigitalSignature.ts @@ -10,16 +10,11 @@ import { export const addDigitalSignature = async ( documentAsBase64: string -): Promise => { +): Promise => { // Custom code to add Byterange to PDF const PDFArrayCustom = require("./PDFArrayCustom"); - - // The PDF we're going to sign const pdfBuffer = Buffer.from(documentAsBase64, "base64"); - - // The p12 certificate we're going to sign with const p12Buffer = fs.readFileSync("ressources/certificate.p12"); - const SIGNATURE_LENGTH = 4540; const pdfDoc = await PDFDocument.load(pdfBuffer); @@ -70,11 +65,9 @@ export const addDigitalSignature = async ( const modifiedPdfBuffer = Buffer.from(modifiedPdfBytes); const signObj = new signer.SignPdf(); - const signedPdfBuffer = signObj.sign(modifiedPdfBuffer, p12Buffer, { + const signedPdfBuffer: Buffer = signObj.sign(modifiedPdfBuffer, p12Buffer, { passphrase: "", }); - // Write the signed file - // fs.writeFileSync("./signed.pdf", signedPdfBuffer); - return signedPdfBuffer; + return signedPdfBuffer.toString("base64"); }; From 3afad7a72a8c2c7aa2566afbb3853dd08ab55276 Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 2 Mar 2023 18:44:54 +0100 Subject: [PATCH 10/11] Update [id].ts --- apps/web/pages/api/test-sign/[id].ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/pages/api/test-sign/[id].ts b/apps/web/pages/api/test-sign/[id].ts index ed3aab2b1..55774fa05 100644 --- a/apps/web/pages/api/test-sign/[id].ts +++ b/apps/web/pages/api/test-sign/[id].ts @@ -9,6 +9,8 @@ import { Document as PrismaDocument } from "@prisma/client"; import { getDocument } from "@documenso/lib/query"; import { addDigitalSignature } from "@documenso/signing/addDigitalSignature"; +// todo remove before launch + async function getHandler(req: NextApiRequest, res: NextApiResponse) { const documentId = req.query.id || 1; const document: PrismaDocument = await getDocument(+documentId, req, res); From 3c6faff4016a66ae2e024dee9deb3e8c37db32de Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Thu, 2 Mar 2023 18:46:03 +0100 Subject: [PATCH 11/11] =?UTF-8?q?=F0=9F=A7=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/signing/addDigitalSignature.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signing/addDigitalSignature.ts b/packages/signing/addDigitalSignature.ts index e41e1e180..569fa63db 100644 --- a/packages/signing/addDigitalSignature.ts +++ b/packages/signing/addDigitalSignature.ts @@ -49,10 +49,10 @@ export const addDigitalSignature = async ( }); const widgetDictRef = pdfDoc.context.register(widgetDict); - // Add our signature widget to the first page + // Add signature widget to the first page pages[0].node.set(PDFName.of("Annots"), pdfDoc.context.obj([widgetDictRef])); - // Create an AcroForm object containing our signature widget + // Create an AcroForm object containing the signature widget pdfDoc.catalog.set( PDFName.of("AcroForm"), pdfDoc.context.obj({