Skip to content
Snippets Groups Projects
pdfUtilities.js 24.7 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
     */
    
    import {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      ObjectIdentifier,
      UTCTime,
      OctetString
    
    } from 'asn1js';
    
    import {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      ContentInfo,
      SignedData,
      Attribute,
      SignerInfo,
      IssuerAndSerialNumber,
      SignedAndUnsignedAttributes,
      EncapsulatedContentInfo,
      getCrypto
    
    } from 'pkijs';
    
    import {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      parseCertificate,
      parsePrivateKey
    
    } from './signingUtilities';
    
    //keep this import to include the patched version of pdfjs library
    import {PDFJS} from "../lib/pdfjs.parser.js";
    
    function createXrefTable(xrefEntries) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      xrefEntries = sortOnKeys(xrefEntries);
      let retVal = 'xref\n';
      let last = -2;
      for (let i in xrefEntries) {
        i = parseInt(i);
        if (typeof xrefEntries[i].offset === 'undefined') { continue; }
        retVal += calcFlow(i, last, xrefEntries);
        const 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) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      if (last + 1 === i) { return ''; }
      let 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) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      let retVal = 'trailer <<\n';
      retVal += '  /Size ' + size + '\n';
      const refRoot = topDict.getRaw('Root');
      if (typeof refRoot !== 'undefined') {
        retVal += '  /Root ' + refRoot.num + ' ' + refRoot.gen + ' R\n';
      }
      const 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) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      xrefEntries = sortOnKeys(xrefEntries);
    
      let retVal = 'xref\n';
      let last = -2;
      for (let i in xrefEntries) {
        i = parseInt(i);
        if (typeof xrefEntries[i].offset === 'undefined') { continue; }
        retVal += calcFlow(i, last, xrefEntries);
        const 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) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const sorted = [];
      for (const key in dict) {
        sorted[sorted.length] = key;
      }
      sorted.sort();
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const tempDict = {};
      for (let i = 0; i < sorted.length; i++) {
        tempDict[sorted[i]] = dict[sorted[i]];
      }
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      return tempDict;
    
    }
    
    function removeFromArray(array, from, to) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const cutlen = to - from;
      const buf = new Uint8Array(array.length - cutlen);
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      for (let i = 0; i < from; i++) {
        buf[i] = array[i];
      }
      for (let i = to, len = array.length; i < len; i++) {
        buf[i - cutlen] = array[i];
      }
      return buf;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    
    
    function findXrefBlocks(xrefBlocks) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const num = xrefBlocks.length / 2;
      const retVal = [];
      for (let i = 0; i < num; i++) {
        retVal.push({start: xrefBlocks[i],
          end: xrefBlocks[i + num]});
      }
      return retVal;
    
    }
    
    function convertUint8ArrayToBinaryString(u8Array) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      let i, len = u8Array.length, bStr = "";
      for (i = 0; i < len; i++) {
        bStr += String.fromCharCode(u8Array[i]);
      }
      return bStr;
    
    }
    
    function arrayObjectIndexOf(array, start, end, orig) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      for (let i = 0, len = array.length; i < len; i++) {
        if ((array[i].start === start) && (array[i].end === end) && (array[i].orig === orig)) {
          return i;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
      return -1;
    
    }
    
    function pad10(num) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const s = "000000000" + num;
      return s.substr(s.length - 10);
    
    }
    
    function pad5(num) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const s = "0000" + num;
      return s.substr(s.length - 5);
    
    }
    
    function pad2(num) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const s = "0" + num;
      return s.substr(s.length - 2);
    
    }
    
    function findRootEntry(xref) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const rootNr = xref.root.objId.substring(0, xref.root.objId.length - 1);
      return xref.entries[rootNr];
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    
    
    function findSuccessorEntry(xrefEntries, current) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      //find it first
      const currentOffset = current.offset;
      let currentMin = Number.MAX_SAFE_INTEGER;
      let currentMinIndex = -1;
      for (const i in xrefEntries) {
        if (xrefEntries[i].offset > currentOffset) {
          if (xrefEntries[i].offset < currentMin) {
            currentMin = xrefEntries[i].offset;
            currentMinIndex = i;
          }
        }
      }
      if (currentMinIndex === -1) {
        return current;
      }
      return xrefEntries[currentMinIndex];
    
    }
    
    function updateArray(array, pos, str) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const upd = stringToUint8Array(str);
      for (let i = 0, len = upd.length; i < len; i++) {
        array[i + pos] = upd[i];
      }
      return array;
    
    }
    
    function copyToEnd(array, from, to) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const buf = new Uint8Array(array.length + (to - from));
      for (let i = 0, len = array.length; i < len; i++) {
        buf[i] = array[i];
      }
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      for (let i = 0, len = to - from; i < len; i++) {
        buf[array.length + i] = array[from + i];
      }
      return buf;
    
    }
    
    function insertIntoArray(array, pos, str) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const ins = stringToUint8Array(str);
      const buf = new Uint8Array(array.length + ins.length);
      for (let i = 0; i < pos; i++) {
        buf[i] = array[i];
      }
      for (let i = 0; i < ins.length; i++) {
        buf[pos + i] = ins[i];
      }
      for (let i = pos; i < array.length; i++) {
        buf[ins.length + i] = array[i];
      }
      return buf;
    
    }
    
    function stringToUint8Array(str) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const buf = new Uint8Array(str.length);
      for (let i = 0, strLen = str.length; i < strLen; i++) {
        buf[i] = str.charCodeAt(i);
      }
      return buf;
    
    }
    
    function uint8ArrayToString(buf, from, to) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      if (typeof from !== 'undefined' && typeof to !== 'undefined') {
        let s = '';
        for (let i = from; i < to; i++) {
          s = s + String.fromCharCode(buf[i]);
    
    Damyan Mitev's avatar
    Damyan Mitev committed
        return s;
      }
      return String.fromCharCode.apply(null, buf);
    
    }
    
    function findFreeXrefNr(xrefEntries, used) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      used = typeof used !== 'undefined' ?  used : [];
      let inc = used.length;
    
      for (let i = 1; i < xrefEntries.length; i++) {
        const index = used.indexOf(i);
        const entry = xrefEntries["" + i];
        if (index === -1 && (typeof entry === 'undefined' || entry.free)) {
          return i;
        }
        if (index !== -1) {
          inc--;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
      return xrefEntries.length + inc;
    
    }
    
    function find(uint8, needle, start, limit) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      start = typeof start !== 'undefined' ? start : 0;
      limit = typeof limit !== 'undefined' ? limit : Number.MAX_SAFE_INTEGER;
    
      const search = stringToUint8Array(needle);
      let match = 0;
    
      for (let 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;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
      return -1;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    
    
    function findBackwards(uint8, needle, start, limit) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      start = typeof start !== 'undefined' ? start : uint8.length;
      limit = typeof limit !== 'undefined' ? limit : Number.MAX_SAFE_INTEGER;
    
      const search = stringToUint8Array(needle);
      let match = search.length - 1;
    
      for (let i = start; i >= 0 && i < limit; i--) {
        if (uint8[i] === search[match]) {
          match--;
        } else {
          match = search.length - 1;
          if (uint8[i] === search[match]) {
            match--;
          }
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    
        if (match === 0) {
          return i - 1;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
      return -1;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    function strHex(s) {
      let a = "";
      for (let i = 0; i < s.length; i++) {
        a = a + pad2(s.charCodeAt(i).toString(16));
      }
      return a;
    }
    
    
    async function sha256(array) {
    
    Damyan Mitev's avatar
    Damyan Mitev 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) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      if (typeof pdf.acroForm === 'undefined') {
        return false;
      }
      return pdf.acroForm.get('SigFlags') === 3;
    
    }
    
    function updateXrefOffset(xref, offset, offsetDelta) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      for (const i in xref.entries) {
        if (xref.entries[i].offset >= offset) {
          xref.entries[i].offset += offsetDelta;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
      for (const i in xref.xrefBlocks) {
        if (xref.xrefBlocks[i] >= offset) {
          xref.xrefBlocks[i]  += offsetDelta;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
    
    }
    
    function updateXrefBlocks(xrefBlocks, offset, offsetDelta) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      for (const i in xrefBlocks) {
        if (xrefBlocks[i].start >= offset) {
          xrefBlocks[i].start += offsetDelta;
        }
        if (xrefBlocks[i].end >= offset) {
          xrefBlocks[i].end += offsetDelta;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      }
    
    }
    
    function updateOffset(pos, offset, offsetDelta) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      if (pos >= offset) {
        return pos + offsetDelta;
      }
      return pos;
    
    }
    
    function round256(x) {
    
    Damyan Mitev's avatar
    Damyan Mitev 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) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      //date = typeof date !== 'undefined' ? date : new Date();
      const yyyy = date.getFullYear().toString();
      const MM = pad2(date.getMonth() + 1);
      const dd = pad2(date.getDate());
      const hh = pad2(date.getHours());
      const mm = pad2(date.getMinutes());
      const ss = pad2(date.getSeconds());
      return yyyy + MM + dd +  hh + mm + ss + createOffset(date);
    
    }
    
    function createOffset(date) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const sign = date.getTimezoneOffset() > 0 ? "-" : "+";
      const offset = Math.abs(date.getTimezoneOffset());
      const hours = pad2(Math.floor(offset / 60));
      const minutes = pad2(offset % 60);
      return sign + hours + "'" + minutes;
    
    }
    
    async function newSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      // {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
      const annotEntry = findFreeXrefNr(pdf.xref.entries);
      // we'll store all the modifications we make, as we need to adjust the offset in the PDF
      const 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>>
      //
      const 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[
      const pages = pdf.catalog.catDict.get('Pages');
      //get first page, we have hidden sig, so don't bother
      const ref = pages.get('Kids')[0];
      const xref = pdf.xref.fetch(ref);
      const offsetContentEnd = xref.get('#Contents_offset');
      //we now search backwards, this is safe as we don't expect user content here
      let offsetContent = findBackwards(pdf.stream.bytes, '/Contents', offsetContentEnd);
      const appendAnnots = '/Annots[' + annotEntry + ' 0 R]\n ';
    
      //now insert string into stream
      let 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);
    
      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
      const 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
      //
      const 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
      const blocks = findXrefBlocks(pdf.xref.xrefBlocks);
      let 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
      let 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.
      const start = sigEntry + ' 0 obj\n<</Contents <';
      const dummy = await signPki(signingCert, certificateChain, privateKey, stringToUint8Array('A'), date);
      //TODO: Adobe thinks its important to have the right size, no idea why this is the case
      const crypto = new Array(round256(dummy.length * 2)).join('0');
      const middle = '>\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:' + now(date) + '\')\n/ByteRange ';
      let byteRange = '[0000000000 0000000000 0000000000 0000000000]';
      const end = '/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n';
      //all together
      const append2 = start + crypto + middle + byteRange + end;
      const 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
      const xrefBlocks = findXrefBlocks(pdf.xref.xrefBlocks);
    
      for (const i in xrefBlocks) {
        const oldSize = array.length;
        array = removeFromArray(array, xrefBlocks[i].start, xrefBlocks[i].end);
        const length = array.length - oldSize;
        updateXrefOffset(pdf.xref, xrefBlocks[i].start, length);
    
        //check for %%EOF and remove it as well
        const offsetEOF = find(array, '%%EOF', xrefBlocks[i].start, xrefBlocks[i].start + 20);
        if (offsetEOF > 0) {
          const 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);
        }
        updateXrefBlocks(xrefBlocks, xrefBlocks[i].start, length);
        offsetAnnot = updateOffset(offsetAnnot, xrefBlocks[i].start, length);
        offsetSig = updateOffset(offsetSig, xrefBlocks[i].start, length);
      }
    
      const 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};
    
      let 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]
      const from1 = 0;
      const to1 = offsetSig + start.length;
      const from2 = to1 + crypto.length;
      const to2 = (array.length - from2) - 1;
      byteRange = '[' + pad10(from1) + ' ' + pad10(to1 - 1) + ' ' + pad10(from2 + 1) + ' ' + pad10(to2) + ']';
      array = updateArray(array, offsetSig + offsetByteRange, byteRange);
      const data = removeFromArray(array, to1 - 1, from2 + 1);
      const crypto2 = await signPki(signingCert, certificateChain, privateKey, data.buffer, date);
      array = updateArray(array, to1, crypto2);
      return array;
    
    async function appendSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      //copy root and the entry with contents to the end
      const startRoot = pdf.stream.bytes.length + 1;
    
      let 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:
      const offsetAcroForm = find(array, '/AcroForm<</Fields', startRoot);
      const endOffsetAcroForm = find(array, ']', offsetAcroForm);
    
      const annotEntry = findFreeXrefNr(pdf.xref.entries);
      const sigEntry = findFreeXrefNr(pdf.xref.entries, [annotEntry]);
    
      const 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
      const pages = pdf.catalog.catDict.get('Pages');
      //get first page, we have hidden sig, so don't bother
      const contentRef = pages.get('Kids')[0];
      const xref = pdf.xref.fetch(contentRef);
      const offsetAnnotEnd = xref.get('#Annots_offset');
      //we now search ], this is safe as we signed it previously
      const endOffsetAnnot = find(array, ']', offsetAnnotEnd);
      const xrefEntry = pdf.xref.getEntry(contentRef.num);
      const xrefEntrySuccosser = findSuccessorEntry(pdf.xref.entries, xrefEntry);
      const offsetAnnotRelative = endOffsetAnnot - xrefEntrySuccosser.offset;
      const startContent = array.length;
      array = copyToEnd(array, xrefEntry.offset, xrefEntrySuccosser.offset);
      array = insertIntoArray(array, array.length + offsetAnnotRelative, appendAnnot);
    
      const startAnnot = array.length;
      const 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);
    
      const startSig = array.length;
      const start = sigEntry + ' 0 obj\n<</Contents <';
      const dummy = await signPki(signingCert, certificateChain, privateKey, stringToUint8Array('A'), date);
      //TODO: Adobe thinks its important to have the right size, no idea why this is the case
      const crypto = new Array(round256(dummy.length * 2)).join('0');
      const middle = '>\n/Type/Sig/SubFilter/adbe.pkcs7.detached/Location()/M(D:' + now(date) + '\')\n/ByteRange ';
      let byteRange = '[0000000000 0000000000 0000000000 0000000000]';
      const end = '/Filter/Adobe.PPKLite/Reason()/ContactInfo()>>\nendobj\n\n';
      //all together
      const append2 = start + crypto + middle + byteRange + end;
      array = insertIntoArray(array, startSig, append2);
    
      const sha256Hex = await sha256(array);
    
      const prev = pdf.xref.xrefBlocks[0];
      const startxref = array.length;
      const 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};
      let xrefTable = createXrefTableAppend(xrefEntries);
      xrefTable += createTrailer(pdf.xref.topDict, startxref, sha256Hex, xrefEntries.length, prev);
      array = insertIntoArray(array, array.length, xrefTable);
    
      const from1 = 0;
      const to1 = startSig + start.length;
      const from2 = to1 + crypto.length;
      const to2 = (array.length - from2) - 1;
      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
    
      const data = removeFromArray(array, to1 - 1, from2 + 1);
      const crypto2 = await signPki(signingCert, certificateChain, privateKey, data.buffer, date);
      array = updateArray(array, to1, crypto2);
      return array;
    
    }
    
    function loadPdf(pdfArray) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const pdf = new pdfjsCoreDocument.PDFDocument(false, pdfArray, '');
      pdf.parseStartXRef();
      pdf.parse();
      return pdf;
    
    }
    
    //data must be Uint8Array
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    async function signPki(signingCert, certificateChain, privateKey, data, date) {
      const crypto = getCrypto();
    
      //date = typeof date !== 'undefined' ?  date : new Date();
    
      const hashAlg = "SHA-256";
    
      const digest = await crypto.digest({ name: hashAlg }, data);
    
      const signedAttr = [];
    
      signedAttr.push(new Attribute({
        type: "1.2.840.113549.1.9.3",
        values: [
          new ObjectIdentifier({ value: "1.2.840.113549.1.7.1" })
        ]
      })); // contentType
    
      signedAttr.push(new Attribute({
        type: "1.2.840.113549.1.9.4",
        values: [
          new OctetString({ valueHex: digest })
        ]
      })); // messageDigest
    
      signedAttr.push(new Attribute({
        type: "1.2.840.113549.1.9.5",
        values: [
          new UTCTime({ valueDate: date })
        ]
      })); // signingTime
    
      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,
    
    Damyan Mitev's avatar
    Damyan Mitev committed
            sid: new IssuerAndSerialNumber({
              issuer: signingCert.issuer,
              serialNumber: signingCert.serialNumber
    
    Damyan Mitev's avatar
    Damyan Mitev committed
            signedAttrs: new SignedAndUnsignedAttributes({
              type: 0,
              attributes: signedAttr
            })
          })
        ],
        certificates: certificateChain
      });
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      await cmsSignedSimpl.sign(privateKey, 0, hashAlg, data.buffer);
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const cmsSignedSchema = cmsSignedSimpl.toSchema(true);
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const cmsContentSimp = new ContentInfo({
        contentType: "1.2.840.113549.1.7.2",
        content: cmsSignedSchema
      });
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const _cmsSignedSchema = cmsContentSimp.toSchema();
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      //region Make length of some elements in "indefinite form"
      _cmsSignedSchema.lenBlock.isIndefiniteForm = true;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const block1 = _cmsSignedSchema.valueBlock.value[1];
      block1.lenBlock.isIndefiniteForm = true;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const block2 = block1.valueBlock.value[0];
      block2.lenBlock.isIndefiniteForm = true;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      //endregion
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const cmsSignedBuffer = _cmsSignedSchema.toBER(false);
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const cmsSignedArray = new Uint8Array(cmsSignedBuffer);
      const cmsSignedString = uint8ArrayToString(cmsSignedArray);
      const hex = strHex(cmsSignedString);
      return hex;
    
    Damyan Mitev's avatar
    Damyan Mitev committed
    async function signPdfObjects(pdfRaw, signingCert, certificateChain, privateKey, date) {
      date = typeof date !== 'undefined' ? date : new Date();
      if (pdfRaw instanceof ArrayBuffer) {
        pdfRaw = new Uint8Array(pdfRaw);
      }
      const pdf = loadPdf(pdfRaw);
      const root = findRootEntry(pdf.xref);
      const rootSuccessor = findSuccessorEntry(pdf.xref.entries, root);
      if (!isSigInRoot(pdf)) {
        return await newSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey);
      } else {
        return await appendSig(pdf, root, rootSuccessor, date, signingCert, certificateChain, privateKey);
      }
    
    }
    
    export function signPdf(pdfRaw, signingCert, certificateChain, privateKey) {
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      const signingCertObj = parseCertificate(signingCert);
      const certificateChainObj = [];
      certificateChainObj[0] = parseCertificate(signingCert);
      for (let i = 0; i < certificateChain.length; i++) {
        certificateChainObj[i + 1] = parseCertificate(certificateChain[i]);
      }
    
    Damyan Mitev's avatar
    Damyan Mitev committed
      return parsePrivateKey(privateKey).then(privateKeyDecoded => {
        return signPdfObjects(pdfRaw, signingCertObj, certificateChainObj, privateKeyDecoded);
      });
    }