🚧 signDocument using node signpdf and custom placeholder insert

This commit is contained in:
Timur Ercan
2023-03-01 19:54:22 +01:00
parent 02e225517c
commit 8387179cbf
7 changed files with 203 additions and 21 deletions

View File

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

View File

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

View File

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

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 { 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) }),
});

36
package-lock.json generated
View File

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

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

@ -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<any> => {
// 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;
};