From 474152be0cf015b7dcc36befaee511e43637cea2 Mon Sep 17 00:00:00 2001 From: dcbr <15089458+dcbr@users.noreply.github.com> Date: Fri, 29 Dec 2023 16:42:51 +0000 Subject: [PATCH] Web and pdflib enhancements --- packages/examples/src/pdf-lib.js | 10 +- .../dist/pdflibAddPlaceholder.d.ts | 8 +- .../dist/pdflibAddPlaceholder.d.ts.map | 2 +- .../dist/pdflibAddPlaceholder.js | 75 ++++++--- .../src/pdflibAddPlaceholder.js | 65 +++++--- .../src/pdflibAddPlaceholder.test.js | 148 +++++++++++++++++- packages/signer-p12/dist/P12Signer.d.ts | 7 +- packages/signer-p12/dist/P12Signer.d.ts.map | 2 +- packages/signer-p12/dist/P12Signer.js | 15 +- packages/signer-p12/src/P12Signer.js | 20 +-- packages/signer-p12/src/P12Signer.test.js | 8 +- packages/signpdf/dist/signpdf.d.ts | 6 +- packages/signpdf/dist/signpdf.d.ts.map | 2 +- packages/signpdf/dist/signpdf.js | 13 +- packages/signpdf/src/signpdf.js | 16 +- packages/signpdf/src/signpdf.test.js | 4 +- packages/utils/dist/Signer.d.ts | 5 +- packages/utils/dist/Signer.d.ts.map | 2 +- packages/utils/dist/Signer.js | 5 +- packages/utils/dist/convertBuffer.d.ts | 7 + packages/utils/dist/convertBuffer.d.ts.map | 1 + packages/utils/dist/convertBuffer.js | 21 +++ packages/utils/dist/index.d.ts | 1 + packages/utils/dist/index.js | 11 ++ packages/utils/src/Signer.js | 5 +- packages/utils/src/convertBuffer.js | 19 +++ packages/utils/src/convertBuffer.test.js | 37 +++++ packages/utils/src/index.js | 1 + 28 files changed, 403 insertions(+), 113 deletions(-) create mode 100644 packages/utils/dist/convertBuffer.d.ts create mode 100644 packages/utils/dist/convertBuffer.d.ts.map create mode 100644 packages/utils/dist/convertBuffer.js create mode 100644 packages/utils/src/convertBuffer.js create mode 100644 packages/utils/src/convertBuffer.test.js diff --git a/packages/examples/src/pdf-lib.js b/packages/examples/src/pdf-lib.js index 7b0e0d24..a212c237 100644 --- a/packages/examples/src/pdf-lib.js +++ b/packages/examples/src/pdf-lib.js @@ -20,19 +20,17 @@ function work() { // Add a placeholder for a signature. pdflibAddPlaceholder({ pdfDoc: pdfDoc, - reason: 'The user is decalaring consent through JavaScript.', + reason: 'The user is declaring consent through JavaScript.', contactInfo: 'signpdf@example.com', name: 'John Doe', location: 'Free Text Str., Free World', }); - // Convert the PDF-LIB PDFDocument to Buffer - pdfDoc.save({useObjectStreams: false}).then(function (pdfBytes) { - var pdfWithPlaceholder = Buffer.from(pdfBytes); - + // Get the modified PDFDocument bytes + pdfDoc.save().then(function (pdfWithPlaceholderBytes) { // And finally sign the document. signpdf - .sign(pdfWithPlaceholder, signer) + .sign(pdfWithPlaceholderBytes, signer) .then(function (signedPdf) { // signedPdf is a Buffer of an electronically signed PDF. Store it. var targetPath = path.join(__dirname, '/../output/pdf-lib.pdf'); diff --git a/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts b/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts index 3d3332b1..f8fe6593 100644 --- a/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts +++ b/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts @@ -1,11 +1,13 @@ -export function pdflibAddPlaceholder({ pdfDoc, reason, contactInfo, name, location, signatureLength, byteRangePlaceholder, subFilter, widgetRect, }: InputType): void; +export function pdflibAddPlaceholder({ pdfDoc, pdfPage, reason, contactInfo, name, location, signingTime, signatureLength, byteRangePlaceholder, subFilter, widgetRect, appName, }: InputType): void; export type PDFDocument = import('pdf-lib').PDFDocument; export type InputType = { pdfDoc: PDFDocument; + pdfPage: PDFPage; reason: string; contactInfo: string; name: string; location: string; + signingTime?: Date; signatureLength?: number; byteRangePlaceholder?: string; /** @@ -16,5 +18,9 @@ export type InputType = { * [x1, y1, x2, y2] widget rectangle */ widgetRect?: number[]; + /** + * Name of the application generating the signature + */ + appName?: string; }; //# sourceMappingURL=pdflibAddPlaceholder.d.ts.map \ No newline at end of file diff --git a/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts.map b/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts.map index 1aac768f..bc86ec5a 100644 --- a/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts.map +++ b/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"pdflibAddPlaceholder.d.ts","sourceRoot":"","sources":["../src/pdflibAddPlaceholder.js"],"names":[],"mappings":"AAoCO,qJAHI,SAAS,GACP,IAAI,CA0FhB;0BAhHY,OAAO,SAAS,EAAE,WAAW;;YAK7B,WAAW;YACX,MAAM;iBACN,MAAM;UACN,MAAM;cACN,MAAM;sBACN,MAAM;2BACN,MAAM;;;;gBACN,MAAM;;;;iBACN,MAAM,EAAE"} \ No newline at end of file +{"version":3,"file":"pdflibAddPlaceholder.d.ts","sourceRoot":"","sources":["../src/pdflibAddPlaceholder.js"],"names":[],"mappings":"AAwCO,oLAHI,SAAS,GACP,IAAI,CAmHhB;0BA5IY,OAAO,SAAS,EAAE,WAAW;;YAK7B,WAAW;;YAEX,MAAM;iBACN,MAAM;UACN,MAAM;cACN,MAAM;kBACN,IAAI;sBACJ,MAAM;2BACN,MAAM;;;;gBACN,MAAM;;;;iBACN,MAAM,EAAE;;;;cACR,MAAM"} \ No newline at end of file diff --git a/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.js b/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.js index f1bad163..e2c86a08 100644 --- a/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.js +++ b/packages/placeholder-pdf-lib/dist/pdflibAddPlaceholder.js @@ -13,14 +13,17 @@ var _pdfLib = require("pdf-lib"); /** * @typedef {object} InputType * @property {PDFDocument} pdfDoc +* @property {PDFPage} pdfPage * @property {string} reason * @property {string} contactInfo * @property {string} name * @property {string} location +* @property {Date} [signingTime] * @property {number} [signatureLength] * @property {string} [byteRangePlaceholder] * @property {string} [subFilter] One of SUBFILTER_* from \@signpdf/utils * @property {number[]} [widgetRect] [x1, y1, x2, y2] widget rectangle +* @property {string} [appName] Name of the application generating the signature */ /** @@ -32,21 +35,28 @@ var _pdfLib = require("pdf-lib"); * @returns {void} */ const pdflibAddPlaceholder = ({ - pdfDoc, + pdfDoc = undefined, + pdfPage = undefined, reason, contactInfo, name, location, + signingTime = undefined, signatureLength = _utils.DEFAULT_SIGNATURE_LENGTH, byteRangePlaceholder = _utils.DEFAULT_BYTE_RANGE_PLACEHOLDER, subFilter = _utils.SUBFILTER_ADOBE_PKCS7_DETACHED, - widgetRect = [0, 0, 0, 0] + widgetRect = [0, 0, 0, 0], + appName = undefined }) => { - const page = pdfDoc.getPage(0); + if (pdfDoc === undefined && pdfPage === undefined) { + throw new _utils.SignPdfError('PDFDoc or PDFPage must be set.', _utils.SignPdfError.TYPE_INPUT); + } + const doc = pdfDoc !== null && pdfDoc !== void 0 ? pdfDoc : pdfPage.doc; + const page = pdfPage !== null && pdfPage !== void 0 ? pdfPage : doc.getPages()[0]; // Create a placeholder where the the last 3 parameters of the // actual range will be replaced when signing is done. - const byteRange = _pdfLib.PDFArray.withContext(pdfDoc.context); + const byteRange = _pdfLib.PDFArray.withContext(doc.context); byteRange.push(_pdfLib.PDFNumber.of(0)); byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder)); byteRange.push(_pdfLib.PDFName.of(byteRangePlaceholder)); @@ -56,24 +66,45 @@ const pdflibAddPlaceholder = ({ const placeholder = _pdfLib.PDFHexString.of(String.fromCharCode(0).repeat(signatureLength)); // Create a signature dictionary to be referenced in the signature widget. - const signatureDict = pdfDoc.context.obj({ + const appBuild = appName ? { + App: { + Name: appName + } + } : {}; + const signatureDict = doc.context.obj({ Type: 'Sig', Filter: 'Adobe.PPKLite', SubFilter: subFilter, ByteRange: byteRange, Contents: placeholder, Reason: _pdfLib.PDFString.of(reason), - M: _pdfLib.PDFString.fromDate(new Date()), + M: _pdfLib.PDFString.fromDate(signingTime !== null && signingTime !== void 0 ? signingTime : new Date()), ContactInfo: _pdfLib.PDFString.of(contactInfo), Name: _pdfLib.PDFString.of(name), - Location: _pdfLib.PDFString.of(location) - }, pdfDoc.index); - const signatureDictRef = pdfDoc.context.register(signatureDict); + Location: _pdfLib.PDFString.of(location), + Prop_Build: { + Filter: { + Name: 'Adobe.PPKLite' + }, + ...appBuild + } + }); + // Register signatureDict as a PDFInvalidObject to prevent PDFLib from serializing it + // in an object stream. + const signatureBuffer = new Uint8Array(signatureDict.sizeInBytes()); + signatureDict.copyBytesInto(signatureBuffer, 0); + const signatureObj = _pdfLib.PDFInvalidObject.of(signatureBuffer); + const signatureDictRef = doc.context.register(signatureObj); // Create the signature widget - const rect = _pdfLib.PDFArray.withContext(pdfDoc.context); + const rect = _pdfLib.PDFArray.withContext(doc.context); widgetRect.forEach(c => rect.push(_pdfLib.PDFNumber.of(c))); - const widgetDict = pdfDoc.context.obj({ + const apStream = doc.context.formXObject([], { + BBox: widgetRect, + Resources: {} // Necessary to avoid Acrobat bug (see https://stackoverflow.com/a/73011571) + }); + + const widgetDict = doc.context.obj({ Type: 'Annot', Subtype: 'Widget', FT: 'Sig', @@ -81,27 +112,31 @@ const pdflibAddPlaceholder = ({ V: signatureDictRef, T: _pdfLib.PDFString.of('Signature1'), F: _utils.ANNOTATION_FLAGS.PRINT, - P: page.ref - }, pdfDoc.index); - const widgetDictRef = pdfDoc.context.register(widgetDict); + P: page.ref, + AP: { + N: doc.context.register(apStream) + } // Required for PDF/A compliance + }); + + const widgetDictRef = doc.context.register(widgetDict); - // Annotate the widget on the first page + // Annotate the widget on the given page let annotations = page.node.lookupMaybe(_pdfLib.PDFName.of('Annots'), _pdfLib.PDFArray); if (typeof annotations === 'undefined') { - annotations = pdfDoc.context.obj([]); + annotations = doc.context.obj([]); } annotations.push(widgetDictRef); page.node.set(_pdfLib.PDFName.of('Annots'), annotations); // Add an AcroForm or update the existing one - let acroForm = pdfDoc.catalog.lookupMaybe(_pdfLib.PDFName.of('AcroForm'), _pdfLib.PDFDict); + let acroForm = doc.catalog.lookupMaybe(_pdfLib.PDFName.of('AcroForm'), _pdfLib.PDFDict); if (typeof acroForm === 'undefined') { // Need to create a new AcroForm - acroForm = pdfDoc.context.obj({ + acroForm = doc.context.obj({ Fields: [] }); - const acroFormRef = pdfDoc.context.register(acroForm); - pdfDoc.catalog.set(_pdfLib.PDFName.of('AcroForm'), acroFormRef); + const acroFormRef = doc.context.register(acroForm); + doc.catalog.set(_pdfLib.PDFName.of('AcroForm'), acroFormRef); } /** diff --git a/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js b/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js index 67954aaf..8d5efd2c 100644 --- a/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js +++ b/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.js @@ -4,9 +4,10 @@ import { DEFAULT_SIGNATURE_LENGTH, SIG_FLAGS, SUBFILTER_ADOBE_PKCS7_DETACHED, + SignPdfError, } from '@signpdf/utils'; import { - PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFString, + PDFArray, PDFDict, PDFHexString, PDFName, PDFNumber, PDFInvalidObject, PDFString, } from 'pdf-lib'; /** @@ -16,14 +17,17 @@ import { /** * @typedef {object} InputType * @property {PDFDocument} pdfDoc +* @property {PDFPage} pdfPage * @property {string} reason * @property {string} contactInfo * @property {string} name * @property {string} location +* @property {Date} [signingTime] * @property {number} [signatureLength] * @property {string} [byteRangePlaceholder] * @property {string} [subFilter] One of SUBFILTER_* from \@signpdf/utils * @property {number[]} [widgetRect] [x1, y1, x2, y2] widget rectangle +* @property {string} [appName] Name of the application generating the signature */ /** @@ -35,21 +39,31 @@ import { * @returns {void} */ export const pdflibAddPlaceholder = ({ - pdfDoc, + pdfDoc = undefined, + pdfPage = undefined, reason, contactInfo, name, location, + signingTime = undefined, signatureLength = DEFAULT_SIGNATURE_LENGTH, byteRangePlaceholder = DEFAULT_BYTE_RANGE_PLACEHOLDER, subFilter = SUBFILTER_ADOBE_PKCS7_DETACHED, widgetRect = [0, 0, 0, 0], + appName = undefined, }) => { - const page = pdfDoc.getPage(0); + if (pdfDoc === undefined && pdfPage === undefined) { + throw new SignPdfError( + 'PDFDoc or PDFPage must be set.', + SignPdfError.TYPE_INPUT, + ); + } + const doc = pdfDoc ?? pdfPage.doc; + const page = pdfPage ?? doc.getPages()[0]; // Create a placeholder where the the last 3 parameters of the // actual range will be replaced when signing is done. - const byteRange = PDFArray.withContext(pdfDoc.context); + const byteRange = PDFArray.withContext(doc.context); byteRange.push(PDFNumber.of(0)); byteRange.push(PDFName.of(byteRangePlaceholder)); byteRange.push(PDFName.of(byteRangePlaceholder)); @@ -59,24 +73,38 @@ export const pdflibAddPlaceholder = ({ const placeholder = PDFHexString.of(String.fromCharCode(0).repeat(signatureLength)); // Create a signature dictionary to be referenced in the signature widget. - const signatureDict = pdfDoc.context.obj({ + const appBuild = appName ? {App: {Name: appName}} : {}; + const signatureDict = doc.context.obj({ Type: 'Sig', Filter: 'Adobe.PPKLite', SubFilter: subFilter, ByteRange: byteRange, Contents: placeholder, Reason: PDFString.of(reason), - M: PDFString.fromDate(new Date()), + M: PDFString.fromDate(signingTime ?? new Date()), ContactInfo: PDFString.of(contactInfo), Name: PDFString.of(name), Location: PDFString.of(location), - }, pdfDoc.index); - const signatureDictRef = pdfDoc.context.register(signatureDict); + Prop_Build: { + Filter: {Name: 'Adobe.PPKLite'}, + ...appBuild, + }, + }); + // Register signatureDict as a PDFInvalidObject to prevent PDFLib from serializing it + // in an object stream. + const signatureBuffer = new Uint8Array(signatureDict.sizeInBytes()); + signatureDict.copyBytesInto(signatureBuffer, 0); + const signatureObj = PDFInvalidObject.of(signatureBuffer); + const signatureDictRef = doc.context.register(signatureObj); // Create the signature widget - const rect = PDFArray.withContext(pdfDoc.context); + const rect = PDFArray.withContext(doc.context); widgetRect.forEach((c) => rect.push(PDFNumber.of(c))); - const widgetDict = pdfDoc.context.obj({ + const apStream = doc.context.formXObject([], { + BBox: widgetRect, + Resources: {}, // Necessary to avoid Acrobat bug (see https://stackoverflow.com/a/73011571) + }); + const widgetDict = doc.context.obj({ Type: 'Annot', Subtype: 'Widget', FT: 'Sig', @@ -85,24 +113,25 @@ export const pdflibAddPlaceholder = ({ T: PDFString.of('Signature1'), F: ANNOTATION_FLAGS.PRINT, P: page.ref, - }, pdfDoc.index); - const widgetDictRef = pdfDoc.context.register(widgetDict); + AP: {N: doc.context.register(apStream)}, // Required for PDF/A compliance + }); + const widgetDictRef = doc.context.register(widgetDict); - // Annotate the widget on the first page + // Annotate the widget on the given page let annotations = page.node.lookupMaybe(PDFName.of('Annots'), PDFArray); if (typeof annotations === 'undefined') { - annotations = pdfDoc.context.obj([]); + annotations = doc.context.obj([]); } annotations.push(widgetDictRef); page.node.set(PDFName.of('Annots'), annotations); // Add an AcroForm or update the existing one - let acroForm = pdfDoc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict); + let acroForm = doc.catalog.lookupMaybe(PDFName.of('AcroForm'), PDFDict); if (typeof acroForm === 'undefined') { // Need to create a new AcroForm - acroForm = pdfDoc.context.obj({Fields: []}); - const acroFormRef = pdfDoc.context.register(acroForm); - pdfDoc.catalog.set(PDFName.of('AcroForm'), acroFormRef); + acroForm = doc.context.obj({Fields: []}); + const acroFormRef = doc.context.register(acroForm); + doc.catalog.set(PDFName.of('AcroForm'), acroFormRef); } /** diff --git a/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.test.js b/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.test.js index e686cda5..8dcf80cc 100644 --- a/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.test.js +++ b/packages/placeholder-pdf-lib/src/pdflibAddPlaceholder.test.js @@ -1,10 +1,19 @@ import { - PDFArray, PDFDict, PDFDocument, PDFName, PDFString, + PDFArray, PDFDict, PDFDocument, PDFName, PDFObjectParser, PDFStream, PDFString, } from 'pdf-lib'; import {readTestResource} from '@signpdf/internal-utils'; -import {DEFAULT_BYTE_RANGE_PLACEHOLDER, SUBFILTER_ETSI_CADES_DETACHED} from '@signpdf/utils'; +import {DEFAULT_BYTE_RANGE_PLACEHOLDER, SUBFILTER_ETSI_CADES_DETACHED, SignPdfError} from '@signpdf/utils'; import {pdflibAddPlaceholder} from './pdflibAddPlaceholder'; +// Helper function to convert the added signatureDict (as a PDFInvalidObject) +// back to a PDFDict. +function parseObject(doc, obj) { + const bytes = new Uint8Array(obj.sizeInBytes()); + obj.copyBytesInto(bytes, 0); + const parser = PDFObjectParser.forBytes(bytes, doc.context); + return parser.parseObject(); +} + describe(pdflibAddPlaceholder, () => { const defaults = { reason: 'Because I can', @@ -13,6 +22,15 @@ describe(pdflibAddPlaceholder, () => { location: 'test Location', }; + it('expects a pdf document or page', async () => { + try { + pdflibAddPlaceholder(defaults); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_INPUT); + expect(e.message).toMatchInlineSnapshot('"PDFDoc or PDFPage must be set."'); + } + }); it('adds placeholder to a prepared document', async () => { const input = readTestResource('w3dummy.pdf'); expect(input.indexOf('/ByteRange')).toBe(-1); @@ -33,6 +51,26 @@ describe(pdflibAddPlaceholder, () => { expect(buffer.indexOf('/Filter /Adobe.PPKLite')).not.toBe(-1); }); + it('adds placeholder to a prepared page', async () => { + const input = readTestResource('w3dummy.pdf'); + expect(input.indexOf('/ByteRange')).toBe(-1); + const pdfDoc = await PDFDocument.load(input); + const pdfPage = pdfDoc.getPages()[0]; + + pdflibAddPlaceholder({ + pdfPage, + ...defaults, + }); + // Convert the PDFDocument to bytes + const pdfBytes = await pdfDoc.save(); + // and then to buffer + const buffer = Buffer.from(pdfBytes); + + expect(buffer).toBeInstanceOf(Buffer); + expect(buffer.indexOf('/ByteRange')).not.toBe(-1); + expect(buffer.indexOf('/Filter /Adobe.PPKLite')).not.toBe(-1); + }); + it('allows defining signature SubFilter', async () => { const input = readTestResource('w3dummy.pdf'); expect(input.indexOf('/ByteRange')).toBe(-1); @@ -44,7 +82,7 @@ describe(pdflibAddPlaceholder, () => { subFilter: SUBFILTER_ETSI_CADES_DETACHED, }); // Convert the PDFDocument to bytes - const pdfBytes = await pdfDoc.save({useObjectStreams: false}); + const pdfBytes = await pdfDoc.save(); // and then to buffer const buffer = Buffer.from(pdfBytes); @@ -75,7 +113,7 @@ describe(pdflibAddPlaceholder, () => { /** * @type {PDFDict} */ - const widgetData = widget.lookup(PDFName.of('V')); + const widgetData = parseObject(pdfDoc, widget.lookup(PDFName.of('V'))); expect(widget.get(PDFName.of('Subtype'))).toEqual(PDFName.of('Widget')); expect(widgetData.get(PDFName.of('Reason'))).toEqual(PDFString.of(defaults.reason)); @@ -84,6 +122,37 @@ describe(pdflibAddPlaceholder, () => { expect(widgetData.get(PDFName.of('Name'))).toEqual(PDFString.of(defaults.name)); }); + it('allows defining signing time', async () => { + const input = readTestResource('w3dummy.pdf'); + expect(input.indexOf('/ByteRange')).toBe(-1); + const pdfDoc = await PDFDocument.load(input); + const signingTime = new Date(2023, 11, 0, 11, 0, 0); + + pdflibAddPlaceholder({ + pdfDoc, + ...defaults, + signingTime, + }); + + /** + * @type {PDFArray} + */ + const annots = pdfDoc.getPage(0).node.lookup(PDFName.of('Annots')); + + /** + * @type {PDFDict} + */ + const widget = annots.lookup(annots.size() - 1, PDFDict); + + /** + * @type {PDFDict} + */ + const widgetData = parseObject(pdfDoc, widget.lookup(PDFName.of('V'))); + + expect(widget.get(PDFName.of('Subtype'))).toEqual(PDFName.of('Widget')); + expect(widgetData.get(PDFName.of('M'))).toEqual(PDFString.fromDate(signingTime)); + }); + it('sets the widget rectange to invisible by default', async () => { const input = readTestResource('w3dummy.pdf'); expect(input.indexOf('/ByteRange')).toBe(-1); @@ -142,6 +211,73 @@ describe(pdflibAddPlaceholder, () => { expect(rect.toString()).toEqual('[ 100 100 200 200 ]'); }); + it('sets an appearance stream for the signature widget', async () => { + const input = readTestResource('w3dummy.pdf'); + expect(input.indexOf('/ByteRange')).toBe(-1); + const pdfDoc = await PDFDocument.load(input); + const widgetRect = [100, 100, 200, 200]; + + pdflibAddPlaceholder({ + pdfDoc, + ...defaults, + widgetRect, + }); + + /** + * @type {PDFArray} + */ + const annots = pdfDoc.getPage(0).node.lookup(PDFName.of('Annots')); + + /** + * @type {PDFDict} + */ + const widget = annots.lookup(annots.size() - 1, PDFDict); + + /** + * @type {PDFStream} + */ + const apStream = widget.get(PDFName.of('AP')).lookup(PDFName.of('N'), PDFStream); + + expect(widget.get(PDFName.of('Subtype'))).toEqual(PDFName.of('Widget')); + expect(apStream.dict.get(PDFName.of('BBox')).toString()).toEqual('[ 100 100 200 200 ]'); + }); + + it('sets the Prop_Build dictionary for the signature', async () => { + const input = readTestResource('w3dummy.pdf'); + expect(input.indexOf('/ByteRange')).toBe(-1); + const pdfDoc = await PDFDocument.load(input); + + pdflibAddPlaceholder({ + pdfDoc, + ...defaults, + appName: 'signpdf', + }); + + /** + * @type {PDFArray} + */ + const annots = pdfDoc.getPage(0).node.lookup(PDFName.of('Annots')); + + /** + * @type {PDFDict} + */ + const widget = annots.lookup(annots.size() - 1, PDFDict); + + /** + * @type {PDFDict} + */ + const widgetData = parseObject(pdfDoc, widget.lookup(PDFName.of('V'))); + + /** + * @type {PDFDict} + */ + const propBuild = widgetData.get(PDFName.of('Prop_Build')); + + expect(widget.get(PDFName.of('Subtype'))).toEqual(PDFName.of('Widget')); + expect(propBuild.get(PDFName.of('Filter')).get(PDFName.of('Name'))).toEqual(PDFName.of('Adobe.PPKLite')); + expect(propBuild.get(PDFName.of('App')).get(PDFName.of('Name'))).toEqual(PDFName.of('signpdf')); + }); + it('does not overwrite the AcroForm when it was already there', async () => { const input = readTestResource('signed-once.pdf'); expect(input.indexOf('/ByteRange')).not.toBe(-1); @@ -195,8 +331,8 @@ describe(pdflibAddPlaceholder, () => { const widgetDict = annotations.lookup(annotations.size() - 1, PDFDict); expect(widgetDict.get(PDFName.of('Subtype'))).toEqual(PDFName.of('Widget')); - const byteRange = widgetDict - .lookup(PDFName.of('V'), PDFDict) + const signatureDict = parseObject(pdfDoc, widgetDict.lookup(PDFName.of('V'))); + const byteRange = signatureDict .lookup(PDFName.of('ByteRange'), PDFArray) .asArray() .map((v) => v.toString()); diff --git a/packages/signer-p12/dist/P12Signer.d.ts b/packages/signer-p12/dist/P12Signer.d.ts index fd1f471a..a1b4e0c8 100644 --- a/packages/signer-p12/dist/P12Signer.d.ts +++ b/packages/signer-p12/dist/P12Signer.d.ts @@ -5,10 +5,10 @@ */ export class P12Signer extends Signer { /** - * @param {Buffer} p12Buffer + * @param {Buffer | Uint8Array | string} p12Buffer * @param {SignerOptions} additionalOptions */ - constructor(p12Buffer: Buffer, additionalOptions?: SignerOptions); + constructor(p12Buffer: Buffer | Uint8Array | string, additionalOptions?: SignerOptions); options: { passphrase: string; asn1StrictParsing: boolean; @@ -16,9 +16,10 @@ export class P12Signer extends Signer { cert: any; /** * @param {Buffer} pdfBuffer + * @param {Date | undefined} signingTime * @returns {Buffer} */ - sign(pdfBuffer: Buffer): Buffer; + sign(pdfBuffer: Buffer, signingTime?: Date | undefined): Buffer; } export type SignerOptions = { passphrase?: string; diff --git a/packages/signer-p12/dist/P12Signer.d.ts.map b/packages/signer-p12/dist/P12Signer.d.ts.map index 6ffc58ad..b8382fde 100644 --- a/packages/signer-p12/dist/P12Signer.d.ts.map +++ b/packages/signer-p12/dist/P12Signer.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"P12Signer.d.ts","sourceRoot":"","sources":["../src/P12Signer.js"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH;IACI;;;OAGG;IACH,uBAHW,MAAM,sBACN,aAAa,EAkBvB;IANG;oBAnBE,MAAM;2BACN,OAAO;MAsBR;IACD,UAAiE;IAGrE;;;OAGG;IACH,gBAHW,MAAM,GACJ,MAAM,CAsFlB;CACJ;;iBApHS,MAAM;wBACN,OAAO;;uBALkB,gBAAgB"} \ No newline at end of file +{"version":3,"file":"P12Signer.d.ts","sourceRoot":"","sources":["../src/P12Signer.js"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH;IACI;;;OAGG;IACH,uBAHW,MAAM,GAAG,UAAU,GAAG,MAAM,sBAC5B,aAAa,EAavB;IANG;oBAdE,MAAM;2BACN,OAAO;MAiBR;IACD,UAA8D;IAGlE;;;;OAIG;IACH,gBAJW,MAAM,gBACN,IAAI,GAAG,SAAS,GACd,MAAM,CAoFlB;CACJ;;iBA9GS,MAAM;wBACN,OAAO;;uBALiC,gBAAgB"} \ No newline at end of file diff --git a/packages/signer-p12/dist/P12Signer.js b/packages/signer-p12/dist/P12Signer.js index 5391d8df..2a32b884 100644 --- a/packages/signer-p12/dist/P12Signer.js +++ b/packages/signer-p12/dist/P12Signer.js @@ -15,27 +15,26 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de class P12Signer extends _utils.Signer { /** - * @param {Buffer} p12Buffer + * @param {Buffer | Uint8Array | string} p12Buffer * @param {SignerOptions} additionalOptions */ constructor(p12Buffer, additionalOptions = {}) { super(); - if (!(p12Buffer instanceof Buffer)) { - throw new _utils.SignPdfError('p12 certificate expected as Buffer.', _utils.SignPdfError.TYPE_INPUT); - } + const buffer = (0, _utils.convertBuffer)(p12Buffer, 'p12 certificate'); this.options = { asn1StrictParsing: false, passphrase: '', ...additionalOptions }; - this.cert = _nodeForge.default.util.createBuffer(p12Buffer.toString('binary')); + this.cert = _nodeForge.default.util.createBuffer(buffer.toString('binary')); } /** * @param {Buffer} pdfBuffer + * @param {Date | undefined} signingTime * @returns {Buffer} */ - sign(pdfBuffer) { + async sign(pdfBuffer, signingTime = undefined) { if (!(pdfBuffer instanceof Buffer)) { throw new _utils.SignPdfError('PDF expected as Buffer.', _utils.SignPdfError.TYPE_INPUT); } @@ -91,9 +90,7 @@ class P12Signer extends _utils.Signer { }, { 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() + value: signingTime !== null && signingTime !== void 0 ? signingTime : new Date() }, { type: _nodeForge.default.pki.oids.messageDigest // value will be auto-populated at signing time diff --git a/packages/signer-p12/src/P12Signer.js b/packages/signer-p12/src/P12Signer.js index 6a0a2cda..74a98729 100644 --- a/packages/signer-p12/src/P12Signer.js +++ b/packages/signer-p12/src/P12Signer.js @@ -1,5 +1,5 @@ import forge from 'node-forge'; -import {SignPdfError, Signer} from '@signpdf/utils'; +import {convertBuffer, SignPdfError, Signer} from '@signpdf/utils'; /** * @typedef {object} SignerOptions @@ -9,32 +9,28 @@ import {SignPdfError, Signer} from '@signpdf/utils'; export class P12Signer extends Signer { /** - * @param {Buffer} p12Buffer + * @param {Buffer | Uint8Array | string} p12Buffer * @param {SignerOptions} additionalOptions */ constructor(p12Buffer, additionalOptions = {}) { super(); - if (!(p12Buffer instanceof Buffer)) { - throw new SignPdfError( - 'p12 certificate expected as Buffer.', - SignPdfError.TYPE_INPUT, - ); - } + const buffer = convertBuffer(p12Buffer, 'p12 certificate'); this.options = { asn1StrictParsing: false, passphrase: '', ...additionalOptions, }; - this.cert = forge.util.createBuffer(p12Buffer.toString('binary')); + this.cert = forge.util.createBuffer(buffer.toString('binary')); } /** * @param {Buffer} pdfBuffer + * @param {Date | undefined} signingTime * @returns {Buffer} */ - sign(pdfBuffer) { + async sign(pdfBuffer, signingTime = undefined) { if (!(pdfBuffer instanceof Buffer)) { throw new SignPdfError( 'PDF expected as Buffer.', @@ -104,9 +100,7 @@ export class P12Signer extends Signer { }, { type: forge.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(), + value: signingTime ?? new Date(), }, { type: forge.pki.oids.messageDigest, // value will be auto-populated at signing time diff --git a/packages/signer-p12/src/P12Signer.test.js b/packages/signer-p12/src/P12Signer.test.js index 1416bb2a..c04df1b7 100644 --- a/packages/signer-p12/src/P12Signer.test.js +++ b/packages/signer-p12/src/P12Signer.test.js @@ -7,12 +7,12 @@ describe(P12Signer, () => { it('expects P12 certificate to be Buffer', () => { try { // eslint-disable-next-line no-new - new P12Signer('non-buffer'); + new P12Signer(['non-buffer']); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); - expect(e.message).toMatchInlineSnapshot('"p12 certificate expected as Buffer."'); + expect(e.message).toMatchInlineSnapshot('"p12 certificate expected as Buffer, Uint8Array or base64-encoded string."'); } }); it('expects pdf to be Buffer', async () => { @@ -90,7 +90,7 @@ describe(P12Signer, () => { const signer = new P12Signer(p12Buffer, {passphrase: 'Wrong passphrase'}); try { - signer.sign(Buffer.from('')); + await signer.sign(Buffer.from('')); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof Error).toBe(true); @@ -101,7 +101,7 @@ describe(P12Signer, () => { const p12Buffer = readTestResource('withpass.p12'); const signer = new P12Signer(p12Buffer, {passphrase: 'node-signpdf'}); - const signature = await signer.sign(Buffer.from(''), signer); + const signature = await signer.sign(Buffer.from('')); expect(signature instanceof Buffer).toBe(true); }); }); diff --git a/packages/signpdf/dist/signpdf.d.ts b/packages/signpdf/dist/signpdf.d.ts index 6c54c1f6..2219f3d0 100644 --- a/packages/signpdf/dist/signpdf.d.ts +++ b/packages/signpdf/dist/signpdf.d.ts @@ -6,12 +6,12 @@ export class SignPdf { lastSignature: string; /** - * @param {Buffer} pdfBuffer + * @param {Buffer | Uint8Array | string} pdfBuffer * @param {Signer} signer - * @param {SignerOptions} additionalOptions + * @param {Date | undefined} signingTime * @returns {Promise} */ - sign(pdfBuffer: Buffer, signer: Signer): Promise; + sign(pdfBuffer: Buffer | Uint8Array | string, signer: Signer, signingTime?: Date | undefined): Promise; } declare const _default: SignPdf; export default _default; diff --git a/packages/signpdf/dist/signpdf.d.ts.map b/packages/signpdf/dist/signpdf.d.ts.map index 1eada40c..15c61c34 100644 --- a/packages/signpdf/dist/signpdf.d.ts.map +++ b/packages/signpdf/dist/signpdf.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"signpdf.d.ts","sourceRoot":"","sources":["../src/signpdf.js"],"names":[],"mappings":"AASA;;;;GAIG;AAEH;IAEQ,sBAAyB;IAG7B;;;;;OAKG;IACH,gBALW,MAAM,UACN,MAAM,GAEJ,QAAQ,MAAM,CAAC,CAwF3B;CACJ;;;;iBAtGS,MAAM;wBACN,OAAO;;uBAPV,gBAAgB;6BAAhB,gBAAgB"} \ No newline at end of file +{"version":3,"file":"signpdf.d.ts","sourceRoot":"","sources":["../src/signpdf.js"],"names":[],"mappings":"AAUA;;;;GAIG;AAEH;IAEQ,sBAAyB;IAG7B;;;;;OAKG;IACH,gBALW,MAAM,GAAG,UAAU,GAAG,MAAM,UAC5B,MAAM,gBACN,IAAI,GAAG,SAAS,GACd,QAAQ,MAAM,CAAC,CAmF3B;CACJ;;;;iBAjGS,MAAM;wBACN,OAAO;;uBAPV,gBAAgB;6BAAhB,gBAAgB"} \ No newline at end of file diff --git a/packages/signpdf/dist/signpdf.js b/packages/signpdf/dist/signpdf.js index 2afe1b00..27e4d7d5 100644 --- a/packages/signpdf/dist/signpdf.js +++ b/packages/signpdf/dist/signpdf.js @@ -30,19 +30,16 @@ class SignPdf { } /** - * @param {Buffer} pdfBuffer + * @param {Buffer | Uint8Array | string} pdfBuffer * @param {Signer} signer - * @param {SignerOptions} additionalOptions + * @param {Date | undefined} signingTime * @returns {Promise} */ - async sign(pdfBuffer, signer) { - if (!(pdfBuffer instanceof Buffer)) { - throw new _utils.SignPdfError('PDF expected as Buffer.', _utils.SignPdfError.TYPE_INPUT); - } + async sign(pdfBuffer, signer, signingTime = undefined) { if (!(signer instanceof _utils.Signer)) { throw new _utils.SignPdfError('Signer implementation expected.', _utils.SignPdfError.TYPE_INPUT); } - let pdf = (0, _utils.removeTrailingNewLine)(pdfBuffer); + let pdf = (0, _utils.removeTrailingNewLine)((0, _utils.convertBuffer)(pdfBuffer, 'PDF')); // Find the ByteRange placeholder. const { @@ -72,7 +69,7 @@ class SignPdf { // Remove the placeholder signature pdf = Buffer.concat([pdf.slice(0, byteRange[1]), pdf.slice(byteRange[2], byteRange[2] + byteRange[3])]); - const raw = await signer.sign(pdf); + const raw = await signer.sign(pdf, signingTime); // Check if the PDF has a good enough placeholder to fit the signature. // placeholderLength represents the length of the HEXified symbols but we're diff --git a/packages/signpdf/src/signpdf.js b/packages/signpdf/src/signpdf.js index 76092346..b445ad09 100644 --- a/packages/signpdf/src/signpdf.js +++ b/packages/signpdf/src/signpdf.js @@ -1,4 +1,5 @@ import { + convertBuffer, removeTrailingNewLine, findByteRange, SignPdfError, @@ -19,21 +20,16 @@ export class SignPdf { } /** - * @param {Buffer} pdfBuffer + * @param {Buffer | Uint8Array | string} pdfBuffer * @param {Signer} signer - * @param {SignerOptions} additionalOptions + * @param {Date | undefined} signingTime * @returns {Promise} */ async sign( pdfBuffer, signer, + signingTime = undefined, ) { - if (!(pdfBuffer instanceof Buffer)) { - throw new SignPdfError( - 'PDF expected as Buffer.', - SignPdfError.TYPE_INPUT, - ); - } if (!(signer instanceof Signer)) { throw new SignPdfError( 'Signer implementation expected.', @@ -41,7 +37,7 @@ export class SignPdf { ); } - let pdf = removeTrailingNewLine(pdfBuffer); + let pdf = removeTrailingNewLine(convertBuffer(pdfBuffer, 'PDF')); // Find the ByteRange placeholder. const {byteRangePlaceholder, byteRangePlaceholderPosition} = findByteRange(pdf); @@ -80,7 +76,7 @@ export class SignPdf { pdf.slice(byteRange[2], byteRange[2] + byteRange[3]), ]); - const raw = await signer.sign(pdf); + const raw = await signer.sign(pdf, signingTime); // Check if the PDF has a good enough placeholder to fit the signature. // placeholderLength represents the length of the HEXified symbols but we're diff --git a/packages/signpdf/src/signpdf.test.js b/packages/signpdf/src/signpdf.test.js index 3db73ad1..bdc75609 100644 --- a/packages/signpdf/src/signpdf.test.js +++ b/packages/signpdf/src/signpdf.test.js @@ -49,12 +49,12 @@ const createPdf = (params) => { describe('Test signing', () => { it('expects PDF to be Buffer', async () => { try { - await signpdf.sign('non-buffer', Buffer.from('')); + await signpdf.sign(['non-buffer'], new P12Signer(Buffer.from(''))); expect('here').not.toBe('here'); } catch (e) { expect(e instanceof SignPdfError).toBe(true); expect(e.type).toBe(SignPdfError.TYPE_INPUT); - expect(e.message).toMatchInlineSnapshot('"PDF expected as Buffer."'); + expect(e.message).toMatchInlineSnapshot('"PDF expected as Buffer, Uint8Array or base64-encoded string."'); } }); it('expects P12 signer to be Buffer', async () => { diff --git a/packages/utils/dist/Signer.d.ts b/packages/utils/dist/Signer.d.ts index 636ff49a..57b9af95 100644 --- a/packages/utils/dist/Signer.d.ts +++ b/packages/utils/dist/Signer.d.ts @@ -1,8 +1,9 @@ export class Signer { /** * @param {Buffer} pdfBuffer - * @returns {Promise | Buffer} + * @param {Date | undefined} signingTime + * @returns {Promise} */ - sign(pdfBuffer: Buffer): Promise | Buffer; + sign(pdfBuffer: Buffer, signingTime?: Date | undefined): Promise; } //# sourceMappingURL=Signer.d.ts.map \ No newline at end of file diff --git a/packages/utils/dist/Signer.d.ts.map b/packages/utils/dist/Signer.d.ts.map index ccf22883..4836fca9 100644 --- a/packages/utils/dist/Signer.d.ts.map +++ b/packages/utils/dist/Signer.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"Signer.d.ts","sourceRoot":"","sources":["../src/Signer.js"],"names":[],"mappings":"AAGA;IACI;;;OAGG;IACH,gBAHW,MAAM,GACJ,QAAQ,MAAM,CAAC,GAAG,MAAM,CAOpC;CACJ"} \ No newline at end of file +{"version":3,"file":"Signer.d.ts","sourceRoot":"","sources":["../src/Signer.js"],"names":[],"mappings":"AAGA;IACI;;;;OAIG;IACH,gBAJW,MAAM,gBACN,IAAI,GAAG,SAAS,GACd,QAAQ,MAAM,CAAC,CAO3B;CACJ"} \ No newline at end of file diff --git a/packages/utils/dist/Signer.js b/packages/utils/dist/Signer.js index 1f285409..6acebe46 100644 --- a/packages/utils/dist/Signer.js +++ b/packages/utils/dist/Signer.js @@ -10,9 +10,10 @@ var _SignPdfError = require("./SignPdfError"); class Signer { /** * @param {Buffer} pdfBuffer - * @returns {Promise | Buffer} + * @param {Date | undefined} signingTime + * @returns {Promise} */ - sign(pdfBuffer) { + async sign(pdfBuffer, signingTime = undefined) { throw new _SignPdfError.SignPdfError(`sign() is not implemented on ${this.constructor.name}`, _SignPdfError.SignPdfError.TYPE_INPUT); } } diff --git a/packages/utils/dist/convertBuffer.d.ts b/packages/utils/dist/convertBuffer.d.ts new file mode 100644 index 00000000..3facebf1 --- /dev/null +++ b/packages/utils/dist/convertBuffer.d.ts @@ -0,0 +1,7 @@ +/** + * @param {Buffer | Uint8Array | string} input + * @param {string} name + * @returns {Buffer} + */ +export function convertBuffer(input: Buffer | Uint8Array | string, name: string): Buffer; +//# sourceMappingURL=convertBuffer.d.ts.map \ No newline at end of file diff --git a/packages/utils/dist/convertBuffer.d.ts.map b/packages/utils/dist/convertBuffer.d.ts.map new file mode 100644 index 00000000..9aab0201 --- /dev/null +++ b/packages/utils/dist/convertBuffer.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"convertBuffer.d.ts","sourceRoot":"","sources":["../src/convertBuffer.js"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,qCAJW,MAAM,GAAG,UAAU,GAAG,MAAM,QAC5B,MAAM,GACJ,MAAM,CAalB"} \ No newline at end of file diff --git a/packages/utils/dist/convertBuffer.js b/packages/utils/dist/convertBuffer.js new file mode 100644 index 00000000..30b41c66 --- /dev/null +++ b/packages/utils/dist/convertBuffer.js @@ -0,0 +1,21 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.convertBuffer = convertBuffer; +var _SignPdfError = require("./SignPdfError"); +/** + * @param {Buffer | Uint8Array | string} input + * @param {string} name + * @returns {Buffer} + */ +function convertBuffer(input, name) { + if (typeof input === 'string') { + return Buffer.from(input, 'base64'); + } + if (input instanceof Buffer || input instanceof Uint8Array) { + return Buffer.from(input); + } + throw new _SignPdfError.SignPdfError(`${name} expected as Buffer, Uint8Array or base64-encoded string.`, _SignPdfError.SignPdfError.TYPE_INPUT); +} \ No newline at end of file diff --git a/packages/utils/dist/index.d.ts b/packages/utils/dist/index.d.ts index 60a16622..ac1e5be2 100644 --- a/packages/utils/dist/index.d.ts +++ b/packages/utils/dist/index.d.ts @@ -1,4 +1,5 @@ export * from "./const"; +export * from "./convertBuffer"; export * from "./extractSignature"; export * from "./findByteRange"; export * from "./removeTrailingNewLine"; diff --git a/packages/utils/dist/index.js b/packages/utils/dist/index.js index edb853f1..247d5fc9 100644 --- a/packages/utils/dist/index.js +++ b/packages/utils/dist/index.js @@ -14,6 +14,17 @@ Object.keys(_const).forEach(function (key) { } }); }); +var _convertBuffer = require("./convertBuffer"); +Object.keys(_convertBuffer).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (key in exports && exports[key] === _convertBuffer[key]) return; + Object.defineProperty(exports, key, { + enumerable: true, + get: function () { + return _convertBuffer[key]; + } + }); +}); var _extractSignature = require("./extractSignature"); Object.keys(_extractSignature).forEach(function (key) { if (key === "default" || key === "__esModule") return; diff --git a/packages/utils/src/Signer.js b/packages/utils/src/Signer.js index 053f164b..4aea42de 100644 --- a/packages/utils/src/Signer.js +++ b/packages/utils/src/Signer.js @@ -4,9 +4,10 @@ import {SignPdfError} from './SignPdfError'; export class Signer { /** * @param {Buffer} pdfBuffer - * @returns {Promise | Buffer} + * @param {Date | undefined} signingTime + * @returns {Promise} */ - sign(pdfBuffer) { + async sign(pdfBuffer, signingTime = undefined) { throw new SignPdfError( `sign() is not implemented on ${this.constructor.name}`, SignPdfError.TYPE_INPUT, diff --git a/packages/utils/src/convertBuffer.js b/packages/utils/src/convertBuffer.js new file mode 100644 index 00000000..200a9ecf --- /dev/null +++ b/packages/utils/src/convertBuffer.js @@ -0,0 +1,19 @@ +import {SignPdfError} from './SignPdfError'; + +/** + * @param {Buffer | Uint8Array | string} input + * @param {string} name + * @returns {Buffer} + */ +export function convertBuffer(input, name) { + if (typeof input === 'string') { + return Buffer.from(input, 'base64'); + } + if (input instanceof Buffer || input instanceof Uint8Array) { + return Buffer.from(input); + } + throw new SignPdfError( + `${name} expected as Buffer, Uint8Array or base64-encoded string.`, + SignPdfError.TYPE_INPUT, + ); +} diff --git a/packages/utils/src/convertBuffer.test.js b/packages/utils/src/convertBuffer.test.js new file mode 100644 index 00000000..c7669269 --- /dev/null +++ b/packages/utils/src/convertBuffer.test.js @@ -0,0 +1,37 @@ +import {convertBuffer} from './convertBuffer'; +import {SignPdfError} from './SignPdfError'; + +describe(convertBuffer, () => { + it('expects an error if input is not a Buffer, Uint8Array or string', () => { + try { + convertBuffer(['non-buffer'], 'Input'); + expect('here').not.toBe('here'); + } catch (e) { + expect(e instanceof SignPdfError).toBe(true); + expect(e.type).toBe(SignPdfError.TYPE_INPUT); + expect(e.message).toMatchInlineSnapshot('"Input expected as Buffer, Uint8Array or base64-encoded string."'); + } + }); + it('converts Buffer to Buffer', () => { + const data = 'test'; + const input = Buffer.from(data); // Data as Buffer + const buffer = convertBuffer(input, 'Input'); + expect(buffer instanceof Buffer).toBe(true); + expect(buffer.toString()).toBe(data); + }); + it('converts Uint8Array to Buffer', () => { + const data = 'test'; + const encoder = new TextEncoder(); + const input = encoder.encode(data); // Data as Uint8Array + const buffer = convertBuffer(input, 'Input'); + expect(buffer instanceof Buffer).toBe(true); + expect(buffer.toString()).toBe(data); + }); + it('converts string to Buffer', () => { + const data = 'test'; + const input = Buffer.from(data).toString('base64'); // Data as base64 string + const buffer = convertBuffer(input, 'Input'); + expect(buffer instanceof Buffer).toBe(true); + expect(buffer.toString()).toBe(data); + }); +}); diff --git a/packages/utils/src/index.js b/packages/utils/src/index.js index a79f4421..35659068 100644 --- a/packages/utils/src/index.js +++ b/packages/utils/src/index.js @@ -1,4 +1,5 @@ export * from './const'; +export * from './convertBuffer'; export * from './extractSignature'; export * from './findByteRange'; export * from './removeTrailingNewLine';