Skip to content
Snippets Groups Projects
pdfUtilities.js 25.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • /*
     * Based on PDFSign v1.0.0
     * https://github.com/Communication-Systems-Group/pdfsign.js
     *
     * Copyright 2015, Thomas Bocek, University of Zurich
     *
     * Licensed under the MIT license:
     * http://www.opensource.org/licenses/MIT
     */
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    import { ObjectIdentifier, UTCTime, OctetString } from "asn1js";
    
    
    import {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      ContentInfo,
      SignedData,
      Attribute,
      SignerInfo,
      IssuerAndSerialNumber,
      SignedAndUnsignedAttributes,
      EncapsulatedContentInfo,
      getCrypto
    } from "pkijs";
    
    import { parseCertificate, parsePrivateKey } from "./signingUtilities";
    
    
    //keep this import to include the patched version of pdfjs library
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    import { PDFJS } from "../lib/pdfjs.parser.js";
    
    
    function createXrefTable(xrefEntries) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      xrefEntries = sortOnKeys(xrefEntries);
      var retVal = "xref\n";
      var last = -2;
      for (var i in xrefEntries) {
        i = parseInt(i);
        if (typeof xrefEntries[i].offset === "undefined") {
          continue;
        }
        retVal += calcFlow(i, last, xrefEntries);
        var offset = xrefEntries[i].offset;
        retVal +=
          pad10(offset) +
          " " +
          pad5(xrefEntries[i].gen) +
          " " +
          (xrefEntries[i].free ? "f" : "n") +
          " \n";
        last = i;
      }
      return retVal;
    
    }
    
    function calcFlow(i, last, xrefEntries) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (last + 1 === i) {
        return "";
      }
      var count = 1;
      while (
        typeof xrefEntries[i + count] !== "undefined" &&
        typeof xrefEntries[i + count].offset !== "undefined"
      ) {
        count++;
      }
      return i + " " + count + "\n";
    
    }
    
    function createTrailer(topDict, startxref, sha256Hex, size, prev) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var retVal = "trailer <<\n";
      retVal += "  /Size " + size + "\n";
      var refRoot = topDict.getRaw("Root");
      if (typeof refRoot !== "undefined") {
        retVal += "  /Root " + refRoot.num + " " + refRoot.gen + " R\n";
      }
      var refInfo = topDict.getRaw("Info");
      if (typeof refInfo !== "undefined") {
        retVal += "  /Info " + refInfo.num + " " + refInfo.gen + " R\n";
      }
      retVal +=
        "  /ID [<" +
        sha256Hex.substring(0, 32) +
        "><" +
        sha256Hex.substring(32, 64) +
        ">]\n";
      if (typeof prev !== "undefined") {
        retVal += "  /Prev " + prev + "\n";
      }
      retVal += ">>\n";
      retVal += "startxref\n";
      retVal += startxref + "\n";
      retVal += "%%EOF\n";
      return retVal;
    
    }
    
    function createXrefTableAppend(xrefEntries) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      xrefEntries = sortOnKeys(xrefEntries);
    
      var retVal = "xref\n";
      var last = -2;
      for (var i in xrefEntries) {
        i = parseInt(i);
        if (typeof xrefEntries[i].offset === "undefined") {
          continue;
        }
        retVal += calcFlow(i, last, xrefEntries);
        var offset = xrefEntries[i].offset;
        retVal +=
          pad10(offset) +
          " " +
          pad5(xrefEntries[i].gen) +
          " " +
          (xrefEntries[i].free ? "f" : "n") +
          " \n";
        last = i;
      }
      return retVal;
    
    }
    
    //http://stackoverflow.com/questions/10946880/sort-a-dictionary-or-whatever-key-value-data-structure-in-js-on-word-number-ke
    function sortOnKeys(dict) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var sorted = [];
      for (var key in dict) {
        sorted[sorted.length] = key;
      }
      sorted.sort();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var tempDict = {};
      for (var i = 0; i < sorted.length; i++) {
        tempDict[sorted[i]] = dict[sorted[i]];
      }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      return tempDict;
    
    }
    
    function removeFromArray(array, from, to) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var cutlen = to - from;
      var buf = new Uint8Array(array.length - cutlen);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      for (var i = 0; i < from; i++) {
        buf[i] = array[i];
      }
      for (var i = to, len = array.length; i < len; i++) {
        buf[i - cutlen] = array[i];
      }
      return buf;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    
    
    function findXrefBlocks(xrefBlocks) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var num = xrefBlocks.length / 2;
      var retVal = [];
      for (var i = 0; i < num; i++) {
        retVal.push({ start: xrefBlocks[i], end: xrefBlocks[i + num] });
      }
      return retVal;
    
    }
    
    function convertUint8ArrayToBinaryString(u8Array) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var i,
        len = u8Array.length,
        b_str = "";
      for (i = 0; i < len; i++) {
        b_str += String.fromCharCode(u8Array[i]);
      }
      return b_str;
    
    }
    
    function arrayObjectIndexOf(array, start, end, orig) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      for (var i = 0, len = array.length; i < len; i++) {
        if (
          array[i].start === start &&
          array[i].end === end &&
          array[i].orig === orig
        ) {
          return i;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      }
      return -1;
    
    }
    
    function pad10(num) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var s = "000000000" + num;
      return s.substr(s.length - 10);
    
    }
    
    function pad5(num) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var s = "0000" + num;
      return s.substr(s.length - 5);
    
    }
    
    function pad2(num) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var s = "0" + num;
      return s.substr(s.length - 2);
    
    }
    
    function findRootEntry(xref) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var rootNr = xref.root.objId.substring(0, xref.root.objId.length - 1);
      return xref.entries[rootNr];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    
    
    function findSuccessorEntry(xrefEntries, current) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      //find it first
      var currentOffset = current.offset;
      var currentMin = Number.MAX_SAFE_INTEGER;
      var currentMinIndex = -1;
      for (var i in xrefEntries) {
        if (xrefEntries[i].offset > currentOffset) {
          if (xrefEntries[i].offset < currentMin) {
            currentMin = xrefEntries[i].offset;
            currentMinIndex = i;
          }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      }
      if (currentMinIndex === -1) {
        return current;
      }
      return xrefEntries[currentMinIndex];
    
    }
    
    function updateArray(array, pos, str) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var upd = stringToUint8Array(str);
      for (var i = 0, len = upd.length; i < len; i++) {
        array[i + pos] = upd[i];
      }
      return array;
    
    }
    
    function copyToEnd(array, from, to) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var buf = new Uint8Array(array.length + (to - from));
      for (var i = 0, len = array.length; i < len; i++) {
        buf[i] = array[i];
      }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      for (var i = 0, len = to - from; i < len; i++) {
        buf[array.length + i] = array[from + i];
      }
      return buf;
    
    }
    
    function insertIntoArray(array, pos, str) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var ins = stringToUint8Array(str);
      var buf = new Uint8Array(array.length + ins.length);
      for (var i = 0; i < pos; i++) {
        buf[i] = array[i];
      }
      for (var i = 0; i < ins.length; i++) {
        buf[pos + i] = ins[i];
      }
      for (var i = pos; i < array.length; i++) {
        buf[ins.length + i] = array[i];
      }
      return buf;
    
    }
    
    function stringToUint8Array(str) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var buf = new Uint8Array(str.length);
      for (var i = 0, strLen = str.length; i < strLen; i++) {
        buf[i] = str.charCodeAt(i);
      }
      return buf;
    
    }
    
    function uint8ArrayToString(buf, from, to) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (typeof from !== "undefined" && typeof to !== "undefined") {
        var s = "";
        for (var i = from; i < to; i++) {
          s = s + String.fromCharCode(buf[i]);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        return s;
      }
      return String.fromCharCode.apply(null, buf);
    
    }
    
    function findFreeXrefNr(xrefEntries, used) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      used = typeof used !== "undefined" ? used : [];
      var inc = used.length;
    
      for (var i = 1; i < xrefEntries.length; i++) {
        var index = used.indexOf(i);
        var entry = xrefEntries["" + i];
        if (index === -1 && (typeof entry === "undefined" || entry.free)) {
          return i;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        if (index !== -1) {
          inc--;
        }
      }
      return xrefEntries.length + inc;
    
    }
    
    function find(uint8, needle, start, limit) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      start = typeof start !== "undefined" ? start : 0;
      limit = typeof limit !== "undefined" ? limit : Number.MAX_SAFE_INTEGER;
    
      var search = stringToUint8Array(needle);
      var match = 0;
    
      for (var i = start; i < uint8.length && i < limit; i++) {
        if (uint8[i] === search[match]) {
          match++;
        } else {
          match = 0;
          if (uint8[i] === search[match]) {
            match++;
          }
        }
    
        if (match === search.length) {
          return i + 1 - match;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      }
      return -1;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    
    
    function findBackwards(uint8, needle, start, limit) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      start = typeof start !== "undefined" ? start : uint8.length;
      limit = typeof limit !== "undefined" ? limit : Number.MAX_SAFE_INTEGER;
    
      var search = stringToUint8Array(needle);
      var match = search.length - 1;
    
      for (var i = start; i >= 0 && i < limit; i--) {
        if (uint8[i] === search[match]) {
          match--;
        } else {
          match = search.length - 1;
          if (uint8[i] === search[match]) {
            match--;
          }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    
        if (match === 0) {
          return i - 1;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      }
      return -1;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    function strHex(s) {
      var a = "";
      for (var i = 0; i < s.length; i++) {
        a = a + pad2(s.charCodeAt(i).toString(16));
      }
      return a;
    }
    
    
    async function sha256(array) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const cryptoLib = getCrypto();
      const digestTmpBuf = await cryptoLib.digest({ name: "SHA-256" }, array);
      const digestTmpArray = new Uint8Array(digestTmpBuf);
      const digestTmpStr = uint8ArrayToString(digestTmpArray);
      const sha256Hex = strHex(digestTmpStr);
      return sha256Hex;
    
    }
    
    function isSigInRoot(pdf) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (typeof pdf.acroForm === "undefined") {
    
        return false;
      }
    
      if (!pdf.acroForm) {
        return false;
      }
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      return pdf.acroForm.get("SigFlags") === 3;
    
    }
    
    function updateXrefOffset(xref, offset, offsetDelta) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      for (var i in xref.entries) {
        if (xref.entries[i].offset >= offset) {
          xref.entries[i].offset += offsetDelta;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      }
      for (var i in xref.xrefBlocks) {
        if (xref.xrefBlocks[i] >= offset) {
          xref.xrefBlocks[i] += offsetDelta;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      }
    
    }
    
    function updateXrefBlocks(xrefBlocks, offset, offsetDelta) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      for (var i in xrefBlocks) {
        if (xrefBlocks[i].start >= offset) {
          xrefBlocks[i].start += offsetDelta;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        if (xrefBlocks[i].end >= offset) {
          xrefBlocks[i].end += offsetDelta;
        }
      }
    
    }
    
    function updateOffset(pos, offset, offsetDelta) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (pos >= offset) {
        return pos + offsetDelta;
      }
      return pos;
    
    }
    
    function round256(x) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      return Math.ceil(x / 256) * 256 - 1;
    
    }
    
    /**
     * (D:YYYYMMDDHHmmSSOHH'mm)
     * e.g. (D:20151210164400+01'00')
     * where:
     * YYYY shall be the year
     * MM shall be the month (01–12)
     * DD shall be the day (01–31)
     * HH shall be the hour (00–23)
     * mm shall be the minute (00–59)
     * SS shall be the second (00–59)
     * O shall be the relationship of local time to Universal Time (UT), and shall be denoted by one of the characters PLUS SIGN (U+002B) (+), HYPHEN-MINUS (U+002D) (-), or LATIN CAPITAL LETTER Z (U+005A) (Z) (see below)
     * HH followed by APOSTROPHE (U+0027) (') shall be the absolute value of the offset from UT in hours (00–23)
     * mm shall be the absolute value of the offset from UT in minutes (00–59)
     */
    function now(date) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      //date = typeof date !== 'undefined' ? date : new Date();
      var yyyy = date.getFullYear().toString();
      var MM = pad2(date.getMonth() + 1);
      var dd = pad2(date.getDate());
      var hh = pad2(date.getHours());
      var mm = pad2(date.getMinutes());
      var ss = pad2(date.getSeconds());
      return yyyy + MM + dd + hh + mm + ss + createOffset(date);
    
    }
    
    function createOffset(date) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var sign = date.getTimezoneOffset() > 0 ? "-" : "+";
      var offset = Math.abs(date.getTimezoneOffset());
      var hours = pad2(Math.floor(offset / 60));
      var minutes = pad2(offset % 60);
      return sign + hours + "'" + minutes;
    }
    
    async function newSig(
      pdf,
      root,
      rootSuccessor,
      date,
      signingCert,
      certificateChain,
      privateKey
    ) {
      try {
        // {annotEntry} is the ref to the annot widget. If we enlarge the array, make sure all the offsets
        // after the modification will be updated -> xref table and startxref
        var annotEntry = findFreeXrefNr(pdf.xref.entries);
        // we'll store all the modifications we make, as we need to adjust the offset in the PDF
        var offsetForm =
          find(pdf.stream.bytes, "<<", root.offset, rootSuccessor.offset) + 2;
        //first we need to find the root element and add the following:
        //
        // /AcroForm<</Fields[{annotEntry} 0 R] /SigFlags 3>>
        //
        var appendAcroForm =
          "/AcroForm<</Fields[" + annotEntry + " 0 R] /SigFlags 3>>";
        //before we insert the acroform, we find the right place for annotentry
    
        //we need to add Annots [x y R] to the /Type /Page section. We can do that by searching /Contents[
        var pages = pdf.catalog.catDict.get("Pages");
        //get first page, we have hidden sig, so don't bother
        var ref = pages.get("Kids")[0];
        var xref = pdf.xref.fetch(ref);
        var offsetContentEnd = xref.get("#Contents_offset");
        //we now search backwards, this is safe as we don't expect user content here
        var offsetContent = findBackwards(
          pdf.stream.bytes,
          "/Contents",
          offsetContentEnd
        );
        var appendAnnots = "/Annots[" + annotEntry + " 0 R]\n ";
    
        //now insert string into stream
        var array = insertIntoArray(pdf.stream.bytes, offsetForm, appendAcroForm);
        //recalculate the offsets in the xref table, only update those that are affected
        updateXrefOffset(pdf.xref, offsetForm, appendAcroForm.length);
        offsetContent = updateOffset(
          offsetContent,
          offsetForm,
          appendAcroForm.length
        );
    
        var array = insertIntoArray(array, offsetContent, appendAnnots);
        updateXrefOffset(pdf.xref, offsetContent, appendAnnots.length);
        offsetContent = -1; //not needed anymore, don't update when offset changes
    
        //Then add to the next free object (annotEntry)
        //add right before the xref table or stream
        //if its a table, place element before the xref table
        //
        // sigEntry is the ref to the signature content. Next we need the signature object
        var sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]);
    
        //
        // {annotEntry} 0 obj
        // <</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature)/V Y 0 R>>
        // endobj
        //
        var append =
          annotEntry +
          " 0 obj\n<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature" +
          annotEntry +
          ")/V " +
          sigEntry +
          " 0 R>>\nendobj\n\n";
    
        // we want the offset just before the last xref table or entry
        var blocks = findXrefBlocks(pdf.xref.xrefBlocks);
        var offsetAnnot = blocks[0].start;
        array = insertIntoArray(array, offsetAnnot, append);
        //no updateXrefOffset, as the next entry will be following
    
        //
        // {sigEntry} 0 obj
        // <</Contents <0481801e6d931d561563fb254e27c846e08325570847ed63d6f9e35 ... b2c8788a5>
        // /Type/Sig/SubFilter/adbe.pkcs7.detached/Location(Ghent)/M(D:20120928104114+02'00')
        // /ByteRange [A B C D]/Filter/Adobe.PPKLite/Reason(Test)/ContactInfo()>>
        // endobj
        //
    
        //the next entry goes below the above
        var offsetSig = offsetAnnot + append.length;
    
        // Both {annotEntry} and {sigEntry} objects need to be added to the last xref table. The byte range needs
        // to be adjusted. Since the signature will always be in a gap, use first an empty sig
        // to check the size, add ~25% size, then calculate the signature and place in the empty
        // space.
        var start = sigEntry + " 0 obj\n<</Contents <";
        var dummy = await sign_pki(
          signingCert,
          certificateChain,
          privateKey,
          stringToUint8Array("A"),
          date
        );
        //TODO: Adobe thinks its important to have the right size, no idea why this is the case
        var crypto = new Array(round256(dummy.length * 2)).join("0");
        var middle =
          ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:" +
          now(date) +
          "')\n/ByteRange ";
        var byteRange = "[0000000000 0000000000 0000000000 0000000000]";
        var end = "/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n";
        //all together
        var append2 = start + crypto + middle + byteRange + end;
        var offsetByteRange = start.length + crypto.length + middle.length;
    
        array = insertIntoArray(array, offsetSig, append2);
        updateXrefOffset(pdf.xref, offsetAnnot, append2.length + append.length);
    
        //find the xref tables, remove them and also the EOF, as we'll write a new table
        var xrefBlocks = findXrefBlocks(pdf.xref.xrefBlocks);
    
        for (var i in xrefBlocks) {
          var oldSize = array.length;
          array = removeFromArray(array, xrefBlocks[i].start, xrefBlocks[i].end);
          var length = array.length - oldSize;
          updateXrefOffset(pdf.xref, xrefBlocks[i].start, length);
    
          //check for %%EOF and remove it as well
          var offsetEOF = find(
            array,
            "%%EOF",
            xrefBlocks[i].start,
            xrefBlocks[i].start + 20
          );
          if (offsetEOF > 0) {
            var lengthEOF = "%%EOF".length;
            array = removeFromArray(array, offsetEOF, offsetEOF + lengthEOF);
            updateXrefOffset(pdf.xref, offsetEOF, -lengthEOF);
            updateXrefBlocks(xrefBlocks, offsetEOF, -lengthEOF);
            offsetAnnot = updateOffset(offsetAnnot, offsetEOF, -lengthEOF);
            offsetSig = updateOffset(offsetSig, offsetEOF, -lengthEOF);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
          updateXrefBlocks(xrefBlocks, xrefBlocks[i].start, length);
          offsetAnnot = updateOffset(offsetAnnot, xrefBlocks[i].start, length);
          offsetSig = updateOffset(offsetSig, xrefBlocks[i].start, length);
        }
    
        var sha256Hex = await sha256(array);
    
        //add the new entries to the xref
        pdf.xref.entries[annotEntry] = { offset: offsetAnnot, gen: 0, free: false };
        pdf.xref.entries[sigEntry] = { offset: offsetSig, gen: 0, free: false };
    
        var xrefTable = createXrefTable(pdf.xref.entries);
        //also empty entries count as in the PDF spec, page 720 (example)
        xrefTable += createTrailer(
          pdf.xref.topDict,
          array.length,
          sha256Hex,
          pdf.xref.entries.length
        );
        array = insertIntoArray(array, array.length, xrefTable);
    
        //since we consolidate, no prev! [adjust /Prev -> rawparsing + offset]
        var from1 = 0;
        var to1 = offsetSig + start.length;
        var from2 = to1 + crypto.length;
        var to2 = array.length - from2 - 1;
        var byteRange =
          "[" +
          pad10(from1) +
          " " +
          pad10(to1 - 1) +
          " " +
          pad10(from2 + 1) +
          " " +
          pad10(to2) +
          "]";
        array = updateArray(array, offsetSig + offsetByteRange, byteRange);
        var data = removeFromArray(array, to1 - 1, from2 + 1);
        var crypto2 = await sign_pki(
          signingCert,
          certificateChain,
          privateKey,
          data.buffer,
          date
        );
        array = updateArray(array, to1, crypto2);
        return array;
      } catch (err) {
        throw new Error("Error creating new signature in PDF: " + err);
      }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    async function appendSig(
      pdf,
      root,
      rootSuccessor,
      date,
      signingCert,
      certificateChain,
      privateKey
    ) {
      try {
        //copy root and the entry with contents to the end
        var startRoot = pdf.stream.bytes.length + 1;
    
        var array = copyToEnd(
          pdf.stream.bytes,
          root.offset - 1,
          rootSuccessor.offset
        );
    
        //since we signed the first one, we know how the pdf has to look like:
        var offsetAcroForm = find(array, "/AcroForm<</Fields", startRoot);
        var endOffsetAcroForm = find(array, "]", offsetAcroForm);
    
        var annotEntry = findFreeXrefNr(pdf.xref.entries);
        var sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]);
    
        var appendAnnot = " " + annotEntry + " 0 R";
        array = insertIntoArray(array, endOffsetAcroForm, appendAnnot);
    
        //we need to add Annots [x y R] to the /Type /Page section. We can do that by searching /Annots
        var pages = pdf.catalog.catDict.get("Pages");
        //get first page, we have hidden sig, so don't bother
        var contentRef = pages.get("Kids")[0];
        var xref = pdf.xref.fetch(contentRef);
        var offsetAnnotEnd = xref.get("#Annots_offset");
        //we now search ], this is safe as we signed it previously
        var endOffsetAnnot = find(array, "]", offsetAnnotEnd);
        var xrefEntry = pdf.xref.getEntry(contentRef.num);
        var xrefEntrySuccosser = findSuccessorEntry(pdf.xref.entries, xrefEntry);
        var offsetAnnotRelative = endOffsetAnnot - xrefEntrySuccosser.offset;
        var startContent = array.length;
        array = copyToEnd(array, xrefEntry.offset, xrefEntrySuccosser.offset);
        array = insertIntoArray(
          array,
          array.length + offsetAnnotRelative,
          appendAnnot
        );
    
        var startAnnot = array.length;
        var append =
          annotEntry +
          " 0 obj\n<</F 132/Type/Annot/Subtype/Widget/Rect[0 0 0 0]/FT/Sig/DR<<>>/T(signature" +
          annotEntry +
          ")/V " +
          sigEntry +
          " 0 R>>\nendobj\n\n";
        array = insertIntoArray(array, startAnnot, append);
    
        var startSig = array.length;
        var start = sigEntry + " 0 obj\n<</Contents <";
        var dummy = await sign_pki(
          signingCert,
          certificateChain,
          privateKey,
          stringToUint8Array("A"),
          date
        );
        //TODO: Adobe thinks its important to have the right size, no idea why this is the case
        var crypto = new Array(round256(dummy.length * 2)).join("0");
        var middle =
          ">\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:" +
          now(date) +
          "')\n/ByteRange ";
        var byteRange = "[0000000000 0000000000 0000000000 0000000000]";
        var end = "/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n";
        //all together
        var append2 = start + crypto + middle + byteRange + end;
        array = insertIntoArray(array, startSig, append2);
    
        var sha256Hex = await sha256(array);
    
        var prev = pdf.xref.xrefBlocks[0];
        var startxref = array.length;
        var xrefEntries = [];
        xrefEntries[0] = { offset: 0, gen: 65535, free: true };
        xrefEntries[pdf.xref.topDict.getRaw("Root").num] = {
          offset: startRoot,
          gen: 0,
          free: false
        };
        xrefEntries[contentRef.num] = { offset: startContent, gen: 0, free: false };
        xrefEntries[annotEntry] = { offset: startAnnot, gen: 0, free: false };
        xrefEntries[sigEntry] = { offset: startSig, gen: 0, free: false };
        var xrefTable = createXrefTableAppend(xrefEntries);
        xrefTable += createTrailer(
          pdf.xref.topDict,
          startxref,
          sha256Hex,
          xrefEntries.length,
          prev
        );
        array = insertIntoArray(array, array.length, xrefTable);
    
        var from1 = 0;
        var to1 = startSig + start.length;
        var from2 = to1 + crypto.length;
        var to2 = array.length - from2 - 1;
        var byteRange =
          "[" +
          pad10(from1) +
          " " +
          pad10(to1 - 1) +
          " " +
          pad10(from2 + 1) +
          " " +
          pad10(to2) +
          "]";
    
        array = updateArray(array, from2 + middle.length, byteRange);
        //now sign from1-to1 / from2-to2 and update byterange
    
        var data = removeFromArray(array, to1 - 1, from2 + 1);
        var crypto2 = await sign_pki(
          signingCert,
          certificateChain,
          privateKey,
          data.buffer,
          date
        );
        array = updateArray(array, to1, crypto2);
        return array;
      } catch (err) {
        throw new Error("Error appending signature in PDF: " + err);
      }
    
    }
    
    function loadPdf(pdfArray) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      try {
        var pdf = new pdfjsCoreDocument.PDFDocument(false, pdfArray, "");
        pdf.parseStartXRef();
        pdf.parse();
        return pdf;
      } catch (err) {
        throw new Error("Error parsing PDF: " + err);
      }
    
    }
    
    //data must be Uint8Array
    async function sign_pki(signingCert, certificateChain, privateKey, data, date) {
      console.log("Calling sign_pki");
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const crypto = getCrypto();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      //date = typeof date !== 'undefined' ?  date : new Date();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const hashAlg = "SHA-256";
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const digest = await crypto.digest({ name: hashAlg }, data);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const signedAttr = [];
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      signedAttr.push(
        new Attribute({
          type: "1.2.840.113549.1.9.3",
          values: [new ObjectIdentifier({ value: "1.2.840.113549.1.7.1" })]
        })
      ); // contentType
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      signedAttr.push(
        new Attribute({
          type: "1.2.840.113549.1.9.4",
          values: [new OctetString({ valueHex: digest })]
        })
      ); // messageDigest
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      signedAttr.push(
        new Attribute({
          type: "1.2.840.113549.1.9.5",
          values: [new UTCTime({ valueDate: date })]
        })
      ); // signingTime
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const cmsSignedSimpl = new SignedData({
        version: 1,
        encapContentInfo: new EncapsulatedContentInfo({
          eContentType: "1.2.840.113549.1.7.1" // "data" content type
        }),
        signerInfos: [
          new SignerInfo({
    
            version: 1,
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            sid: new IssuerAndSerialNumber({
              issuer: signingCert.issuer,
              serialNumber: signingCert.serialNumber
    
    Alexey Lunin's avatar
    Alexey Lunin committed
            signedAttrs: new SignedAndUnsignedAttributes({
              type: 0,
              attributes: signedAttr
            })
          })
        ],
        certificates: certificateChain
      });
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const signatureBuffer = await cmsSignedSimpl.sign(
        privateKey,
        0,
        hashAlg,
        data.buffer
      );
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const cmsSignedSchema = cmsSignedSimpl.toSchema(true);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const cmsContentSimp = new ContentInfo({
        contentType: "1.2.840.113549.1.7.2",
        content: cmsSignedSchema
      });
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const _cmsSignedSchema = cmsContentSimp.toSchema();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      //region Make length of some elements in "indefinite form"
      _cmsSignedSchema.lenBlock.isIndefiniteForm = true;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const block1 = _cmsSignedSchema.valueBlock.value[1];
      block1.lenBlock.isIndefiniteForm = true;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const block2 = block1.valueBlock.value[0];
      block2.lenBlock.isIndefiniteForm = true;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      //endregion
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      let cmsSignedBuffer = _cmsSignedSchema.toBER(false);
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      const cmsSignedArray = new Uint8Array(cmsSignedBuffer);
      const cmsSignedString = uint8ArrayToString(cmsSignedArray);
      const hex = strHex(cmsSignedString);
      return hex;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    async function signPdfObjects(
      pdfRaw,
      signingCert,
      certificateChain,
      privateKey,
      date
    ) {
    
      console.log("Calling signPdfObjects");
    
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      date = typeof date !== "undefined" && date !== null ? date : new Date();
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (pdfRaw instanceof Buffer) {
        pdfRaw = new Uint8Array(pdfRaw);
      } else if (pdfRaw instanceof ArrayBuffer) {
        pdfRaw = new Uint8Array(pdfRaw);
      }
    
      console.log("Calling loadPdf");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var pdf = loadPdf(pdfRaw);
    
      console.log("Calling findRootEntry");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var root = findRootEntry(pdf.xref);
    
      console.log("Calling findSuccessorEntry");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      var rootSuccessor = findSuccessorEntry(pdf.xref.entries, root);
    
      console.log("Calling isSigInRoot");
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      if (!isSigInRoot(pdf)) {
        console.log("Calling newSig");
        return await newSig(
          pdf,
          root,
          rootSuccessor,
          date,
          signingCert,
          certificateChain,
          privateKey
        );
      } else {
        console.log("Calling appendSig");
        return await appendSig(
          pdf,
          root,
          rootSuccessor,
          date,
          signingCert,
          certificateChain,
          privateKey
        );
      }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
    export async function signPdf(
      pdfRaw,
      signingCert,
      certificateChain,
      privateKey
    ) {
    
      console.log("Calling signPdf");
    
      const signingCertObj = parseCertificate(signingCert);
      const certificateChainObj = [];
      certificateChainObj[0] = parseCertificate(signingCert);
      for (let i = 0; i < certificateChain.length; i++) {
    
    Alexey Lunin's avatar
    Alexey Lunin committed
        certificateChainObj[i + 1] = parseCertificate(certificateChain[i]);
    
      }
    
      let privateKeyDecoded;
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      try {
        console.log("Calling parsePrivateKey");
        privateKeyDecoded = await parsePrivateKey(privateKey);
      } catch (e) {
        console.log("Error decoding private key: ", e);
        throw new Error("Error decoding private key: " + e);
      }
    
    Alexey Lunin's avatar
    Alexey Lunin committed
      return await signPdfObjects(
        pdfRaw,
        signingCertObj,
        certificateChainObj,
        privateKeyDecoded
      );
    }