Merge pull request #20 from ElTimuro/doc-20

Doc-20
This commit is contained in:
Timur Ercan
2023-03-02 18:56:10 +01:00
committed by GitHub
33 changed files with 1554 additions and 16 deletions

View File

@ -12,6 +12,7 @@ const withTM = require("next-transpile-modules")([
"@documenso/ui", "@documenso/ui",
"@documenso/pdf", "@documenso/pdf",
"@documenso/features", "@documenso/features",
"@documenso/signing",
"react-signature-canvas", "react-signature-canvas",
]); ]);
const plugins = []; const plugins = [];

View File

@ -32,9 +32,12 @@
"next": "13.0.3", "next": "13.0.3",
"next-auth": "^4.18.3", "next-auth": "^4.18.3",
"next-transpile-modules": "^10.0.0", "next-transpile-modules": "^10.0.0",
"node-forge": "^1.3.1",
"node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0", "nodemailer": "^6.9.0",
"nodemailer-sendgrid": "^1.0.3", "nodemailer-sendgrid": "^1.0.3",
"npm": "^9.1.3", "npm": "^9.1.3",
"pdf-lib": "^1.17.1",
"placeholder-loading": "^0.6.0", "placeholder-loading": "^0.6.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",

View File

@ -7,6 +7,7 @@ import prisma from "@documenso/prisma";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { Document as PrismaDocument } from "@prisma/client"; import { Document as PrismaDocument } from "@prisma/client";
import { getDocument } from "@documenso/lib/query"; import { getDocument } from "@documenso/lib/query";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
async function getHandler(req: NextApiRequest, res: NextApiResponse) { async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const user = await getUserFromToken(req, res); const user = await getUserFromToken(req, res);
@ -24,16 +25,30 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
if (!document) if (!document)
res.status(404).end(`No document with id ${documentId} found.`); res.status(404).end(`No document with id ${documentId} found.`);
const buffer: Buffer = Buffer.from(document.document.toString(), "base64"); 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"); res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Length", buffer.length);
res.setHeader( res.setHeader(
"Content-Disposition", "Content-Disposition",
`attachment; filename=${document.title}` `attachment; filename=${document.title}`
); );
res.setHeader("Content-Length", buffer.length);
res.status(200).send(buffer); return res.status(200).send(buffer);
return;
} }
async function deleteHandler(req: NextApiRequest, res: NextApiResponse) { async function deleteHandler(req: NextApiRequest, res: NextApiResponse) {

View File

@ -3,15 +3,12 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { defaultHandler, defaultResponder } from "@documenso/lib/server"; import { defaultHandler, defaultResponder } from "@documenso/lib/server";
import prisma from "@documenso/prisma"; import prisma from "@documenso/prisma";
type responseData = {
status: string;
};
// Return a healthy 200 status code for uptime monitoring and render.com zero-downtime-deploy // Return a healthy 200 status code for uptime monitoring and render.com zero-downtime-deploy
async function getHandler(req: NextApiRequest, res: NextApiResponse) { 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(); 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({ export default defaultHandler({

View File

@ -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 { 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);
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}`
);
return res.status(200).send(signedDocument);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
});

Binary file not shown.

36
package-lock.json generated
View File

@ -68,9 +68,12 @@
"next": "13.0.3", "next": "13.0.3",
"next-auth": "^4.18.3", "next-auth": "^4.18.3",
"next-transpile-modules": "^10.0.0", "next-transpile-modules": "^10.0.0",
"node-forge": "^1.3.1",
"node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0", "nodemailer": "^6.9.0",
"nodemailer-sendgrid": "^1.0.3", "nodemailer-sendgrid": "^1.0.3",
"npm": "^9.1.3", "npm": "^9.1.3",
"pdf-lib": "^1.17.1",
"placeholder-loading": "^0.6.0", "placeholder-loading": "^0.6.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -3959,11 +3962,30 @@
"enhanced-resolve": "^5.10.0" "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": { "node_modules/node-releases": {
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" "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": { "node_modules/nodemailer": {
"version": "6.9.0", "version": "6.9.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",
@ -8659,9 +8681,12 @@
"next": "13.0.3", "next": "13.0.3",
"next-auth": "^4.18.3", "next-auth": "^4.18.3",
"next-transpile-modules": "^10.0.0", "next-transpile-modules": "^10.0.0",
"node-forge": "^1.3.1",
"node-signpdf": "^1.5.0",
"nodemailer": "^6.9.0", "nodemailer": "^6.9.0",
"nodemailer-sendgrid": "^1.0.3", "nodemailer-sendgrid": "^1.0.3",
"npm": "^9.1.3", "npm": "^9.1.3",
"pdf-lib": "*",
"placeholder-loading": "^0.6.0", "placeholder-loading": "^0.6.0",
"postcss": "^8.4.19", "postcss": "^8.4.19",
"react": "18.2.0", "react": "18.2.0",
@ -11461,11 +11486,22 @@
"enhanced-resolve": "^5.10.0" "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": { "node-releases": {
"version": "2.0.8", "version": "2.0.8",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz",
"integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==" "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": { "nodemailer": {
"version": "6.9.0", "version": "6.9.0",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.0.tgz",

View File

@ -1,7 +1,7 @@
import { sendMail } from "./sendMail"; import { sendMail } from "./sendMail";
import { NEXT_PUBLIC_WEBAPP_URL } from "@documenso/lib/constants";
import { signingCompleteTemplate } from "@documenso/lib/mail"; import { signingCompleteTemplate } from "@documenso/lib/mail";
import { Document as PrismaDocument } from "@prisma/client"; import { Document as PrismaDocument } from "@prisma/client";
import { addDigitalSignature } from "@documenso/signing/addDigitalSignature";
export const sendSigningDoneMail = async ( export const sendSigningDoneMail = async (
recipient: any, recipient: any,
@ -16,7 +16,10 @@ export const sendSigningDoneMail = async (
[ [
{ {
filename: document.title, filename: document.title,
content: Buffer.from(document.document.toString(), "base64"), content: Buffer.from(
await addDigitalSignature(document.document),
"base64"
),
}, },
] ]
); );

View File

@ -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;

View File

@ -0,0 +1,73 @@
const fs = require("fs");
const signer = require("./node-signpdf/dist/signpdf");
import {
PDFDocument,
PDFName,
PDFNumber,
PDFHexString,
PDFString,
} from "pdf-lib";
export const addDigitalSignature = async (
documentAsBase64: string
): Promise<string> => {
// Custom code to add Byterange to PDF
const PDFArrayCustom = require("./PDFArrayCustom");
const pdfBuffer = Buffer.from(documentAsBase64, "base64");
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 signature widget to the first page
pages[0].node.set(PDFName.of("Annots"), pdfDoc.context.obj([widgetDictRef]));
// Create an AcroForm object containing the 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: Buffer = signObj.sign(modifiedPdfBuffer, p12Buffer, {
passphrase: "",
});
return signedPdfBuffer.toString("base64");
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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]}`;
}

View File

@ -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];
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,4 +0,0 @@
export const signDocument = (documentAsBase64: string): string => {
return documentAsBase64;
};