Skip to content

Latest commit

 

History

History
460 lines (415 loc) · 13.7 KB

README.md

File metadata and controls

460 lines (415 loc) · 13.7 KB

ZgaPdfSigner

version license build status

A javascript tool to sign a pdf or set protection of a pdf in web browser.
And it is more powerful when used in Google Apps Script or nodejs.

PS: ZGA is the abbreviation of my father's name.
And I use this name to hope the merits from this application will be dedicated to my parents.

Main features

  • Sign a pdf with an invisible pkcs#7 signature.
  • Sign a pdf with a visible pkcs#7 signature by drawing an image or a text or both.
  • A visible signature can be placed on multiple pages. (In the same position)
  • Sign a pdf and set DocMDP.
  • Add a new signature to a pdf if it has been signed already. (An incremental update)
  • Add a document timestamp from TSA. ( 🚫Not available in web browser 🌻)
  • Sign a pdf with a timestamp from TSA. ( 🚫Not available in web browser 🌻)
  • Enable signature's LTV. ( 🚫Not available in web browser 🌻)
  • Set password protection to a pdf. Supported algorithms:
    • 40bit RC4 Encryption
    • 128bit RC4 Encryption
    • 128bit AES Encryption
    • 256bit AES Encryption
  • Set public-key certificate protection to a pdf. Supported algorithms are as same as the password protection.

About signing with TSA and LTV

Because of the CORS security restrictions in web browser, signing with a timestamp from TSA or enabling LTV can only be used in Google Apps Script or nodejs.
🌻 However, if you can avoid the CORS security restrictions by creating your own service or providing a reverse proxy server, these features are also available in web browser.

The Dependencies

How to use this tool

❓ For more details please see the wiki.

Web Browser

Just import the dependencies and this tool.

<script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js" type="text/javascript"></script>
<script src="https://unpkg.com/node-forge@1.3.1/dist/forge.min.js" type="text/javascript"></script>
<script src="https://cdn.jsdelivr.net/npm/zgapdfsigner/dist/zgapdfsigner.min.js" type="text/javascript"></script>

When drawing text for signature, importing fontkit and pako library is necessary.

<script src="https://unpkg.com/pdf-fontkit@1.8.9/dist/fontkit.umd.min.js" type="text/javascript"></script>
<script src="https://unpkg.com/pako@1.0.11/dist/pako_inflate.min.js" type="text/javascript"></script>

Thanks to znacloud for fixing the font subsetting issue in @pdf-lib/fontkit.

Load the dependencies and this tool.

// Simulate setTimeout function for pdf-lib
function setTimeout(func, sleep){
  Utilities.sleep(sleep);
  func();
}
// Simulate clearTimeout function for pdf-fontkit
function clearTimeout(timeoutID){
  // Do nothing
}
// Simulate window for node-forge
var window = globalThis;
// Load pdf-lib
eval(UrlFetchApp.fetch("https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js").getContentText());
// It is necessary for drawing text for signature.
eval(UrlFetchApp.fetch("https://unpkg.com/pdf-fontkit@1.8.9/dist/fontkit.umd.min.js").getContentText());
// Load pako, It is necessary for drawing text for signature.
eval(UrlFetchApp.fetch("https://unpkg.com/pako@1.0.11/dist/pako_inflate.min.js").getContentText());
// Load node-forge
eval(UrlFetchApp.fetch("https://unpkg.com/node-forge@1.3.1/dist/forge.min.js").getContentText());
// Load ZgaPdfSigner
eval(UrlFetchApp.fetch("https://cdn.jsdelivr.net/npm/zgapdfsigner/dist/zgapdfsigner.min.js").getContentText());

Or simply import the library of ZgaPdfToolkit

  1. Add the library of ZgaPdfToolkit to your project, and suppose the id of library you defined is "pdfkit".
    Script id: 1T0UPf50gGp2fJ4dR1rZfEFgKYC5VpCwUVooCRNySiL7klvIUVsFBCZ9m
  2. Load the library.
pdfkit.loadZga(globalThis);
  1. Install
npm install zgapdfsigner

If using typescript for development, installation of definitely typed for node-forge is necessary.

npm install --save-dev @types/node-forge
  1. Import
// CommonJS Mode
const Zga = require("zgapdfsigner");
// ES Module Mode
import { default as Zga } from "zgapdfsigner";
// Typescript
import * as Zga from "zgapdfsigner";

Let's sign

Sign with an invisible signature.

/**
 * @param {ArrayBuffer} pdf
 * @param {ArrayBuffer} cert
 * @param {string} pwd
 * @return {Promise<Blob>}
 */
async function sign1(pdf, cert, pwd){
  /** @type {SignOption} */
  var sopt = {
    p12cert: cert,
    pwd: pwd,
    permission: 1,
  };
  var signer = new Zga.PdfSigner(sopt);
  var u8arr = await signer.sign(pdf);
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Sign with a visible signature of an image.

/**
 * @param {ArrayBuffer} pdf
 * @param {ArrayBuffer} cert
 * @param {string} pwd
 * @param {ArrayBuffer} imgdat
 * @param {string} imgtyp
 * @return {Promise<Blob>}
 */
async function sign2(pdf, cert, pwd, imgdat, imgtyp){
  /** @type {SignOption} */
  var sopt = {
    p12cert: cert,
    pwd: pwd,
    drawinf: {
      area: {
        x: 25,  // left
        y: 150, // top
        w: 60,  // width
        h: 60,  // height
      },
      imgInfo: {
        imgData: imgdat,
        imgType: imgtyp,
      },
    },
  };
  var signer = new Zga.PdfSigner(sopt);
  var u8arr = await signer.sign(pdf);
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Sign with a visible signature by drawing a text.

/**
 * @param {ArrayBuffer} pdf
 * @param {ArrayBuffer} cert
 * @param {string} pwd
 * @param {string} txt
 * @param {ArrayBuffer} fontdat
 * @return {Promise<Blob>}
 */
async function sign3(pdf, cert, pwd, txt, fontdat){
  /** @type {SignOption} */
  var sopt = {
    p12cert: cert,
    pwd: pwd,
    drawinf: {
      area: {
        x: 25,  // left
        y: 150, // top
        w: 60,  // width
        h: 60,  // height
      },
      textInfo: {
        text: txt,
        fontData: fontdat,
        color: "#00f0f1",
        size: 16,
      },
    },
  };
  var signer = new Zga.PdfSigner(sopt);
  var u8arr = await signer.sign(pdf);
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Use it in Google Apps Script

/**
 * @param {string} pwd Passphrase of certificate
 * @return {Promise}
 */
async function createPdf(pwd){
  // Load pdf, certificate
  var pdfBlob = DriveApp.getFilesByName("_test.pdf").next().getBlob();
  var certBlob = DriveApp.getFilesByName("_test.pfx").next().getBlob();
  // Sign the pdf
  /** @type {SignOption} */
  var sopt = {
    p12cert: certBlob.getBytes(),
    pwd,
    signdate: "1",
    ltv: 1,
  };
  var signer = new Zga.PdfSigner(sopt);
  var u8arr = await signer.sign(pdfBlob.getBytes());
  // Save the result pdf to some folder
  var fld = DriveApp.getFolderById("a folder's id");
  fld.createFile(Utilities.newBlob(u8arr, "application/pdf").setName("signed_test.pdf"));
}

Use queryPassword function in ZgaPdfToolkit.

function myfunction(){
  var spd = SpreadsheetApp.getActiveSpreadsheet();
  pdfkit.queryPassword("createPdf", "Please input the passphrase", spd.getName());
}

Use it in nodejs

const m_fs = require("fs");
const m_path = require("path");
async function main(){
  /** @type {string} */
  var pdfPath = m_path.join(__dirname, "_test.pdf");
  /** @type {string} */
  var pfxPath = m_path.join(__dirname, "_test.pfx");
  /** @type {string} */
  var ps = "";
  /** @type {string} */
  var imgPath = m_path.join(__dirname, "_test.png");
  /** @type {string} */
  var txt = "I am a test string!";
  /** @type {string} */
  var fontPath = m_path.join(__dirname, "_test.ttf");

  if(process.argv.length > 3){
    pfxPath = process.argv[2];
    ps = process.argv[3];
  }else if(process.argv[2]){
    ps = process.argv[2];
  }

  if(!ps){
    // throw new Error("The passphrase is not specified.");
    pfxPath = "";
  }

  /** @type {Buffer} */
  var pdf = m_fs.readFileSync(pdfPath);
  /** @type {Buffer} */
  var pfx = null;
  if(pfxPath){
    pfx = m_fs.readFileSync(pfxPath);
  }
  /** @type {Buffer} */
  var img = null;
  /** @type {string} */
  var imgType = "";
  if(imgPath){
    img = m_fs.readFileSync(imgPath);
    imgType = m_path.extname(imgPath).slice(1);
  }
  /** @type {Buffer} */
  var font = null;
  if(fontPath){
    font = m_fs.readFileSync(fontPath);
  }

  /** @type {SignOption} */
  var sopt = {
    p12cert: pfx,
    pwd: ps,
    permission: pfx ? 2 : 0,
    signdate: "1",
    reason: "I have a test reason.",
    location: "I am on the earth.",
    contact: "zga@zga.com",
    ltv: 1,
    debug: true,
  };
  if(img || txt){
    sopt.drawinf = {
      area: {
        x: 25, // left
        y: 50, // top
        w: txt ? undefined : 60,
        h: txt ? undefined : 100,
      },
      pageidx: "2-3", // Placed the signature on the 3rd page and the 4th page. (Indexes of pages start from 0)
      imgInfo: img ? {
        imgData: img,
        imgType: imgType,
      } : undefined,
      textInfo: txt ? {
        text: txt,
        fontData: font,
        size: 16,
      } : undefined,
    };
  }

  /** @type {Zga.PdfSigner} */
  var ser = new Zga.PdfSigner(sopt);
  /** @type {Uint8Array} */
  var u8dat = await ser.sign(pdf);

  if(u8dat){
    /** @type {string} */
    var outPath = m_path.join(__dirname, "test_signed.pdf");
    m_fs.writeFileSync(outPath, u8dat);
    console.log("Output file: " + outPath);
  }

  console.log("Done");
}

Let's protect the pdf

Set password protection to the pdf.

/**
 * @param {ArrayBuffer} pdf
 * @param {string} upwd
 * @param {string} opwd
 * @return {Promise<Blob>}
 */
async function protect1(pdf, upwd, opwd){
  /** @type {EncryptOption} */
  var eopt = {
    mode: Zga.Crypto.Mode.RC4_40,
    permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
    userpwd: upwd,
    ownerpwd: opwd,
  };
  var cyptor = new Zga.PdfCryptor(eopt);
  var pdfdoc = await cyptor.encryptPdf(pdf);
  u8arr = await pdfdoc.save({"useObjectStreams": false});
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Set public-key certificate protection to the pdf.

/**
 * @param {ArrayBuffer} pdf
 * @param {ArrayBuffer} cert
 * @return {Promise<Blob>}
 */
async function protect2(pdf, cert){
  /** @type {EncryptOption} */
  var eopt = {
    mode: Zga.Crypto.Mode.AES_128,
    pubkeys: [{
      c: cert,
      p: ["copy", "modify", "copy-extract", "annot-forms", "fill-forms", "extract", "assemble"],
    }],
  };
  var cyptor = new Zga.PdfCryptor(eopt);
  var pdfdoc = await cyptor.encryptPdf(pdf);
  u8arr = await pdfdoc.save({"useObjectStreams": false});
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Sign and set protection.

/**
 * @param {ArrayBuffer} pdf
 * @param {ArrayBuffer} cert
 * @param {string} pwd
 * @param {string} opwd
 * @return {Promise<Blob>}
 */
async function signAndProtect1(pdf, cert, pwd, opwd){
  /** @type {SignOption} */
  var sopt = {
    p12cert: cert,
    pwd: pwd,
  };
  /** @type {EncryptOption} */
  var eopt = {
    mode: Zga.Crypto.Mode.RC4_128,
    permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
    ownerpwd: opwd,
  };
  var signer = new Zga.PdfSigner(sopt);
  var u8arr = await signer.sign(pdf, eopt);
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Sign and set protection by the same certificate.

/**
 * @param {ArrayBuffer} pdf
 * @param {ArrayBuffer} cert
 * @param {string} pwd
 * @return {Promise<Blob>}
 */
async function signAndProtect2(pdf, cert, pwd){
  /** @type {SignOption} */
  var sopt = {
    p12cert: cert,
    pwd: pwd,
  };
  /** @type {EncryptOption} */
  var eopt = {
    mode: Zga.Crypto.Mode.AES_256,
    permissions: ["modify", "annot-forms", "fill-forms", "extract", "assemble"],
    pubkeys: [],
  };
  var signer = new Zga.PdfSigner(sopt);
  var u8arr = await signer.sign(pdf, eopt);
  return new Blob([u8arr], {"type" : "application/pdf"});
}

Thanks

  • The module of setting protection was almost migrated from TCPDF.

License

This tool is available under the MIT license.