/* * Copyright 2021 Netsyms Technologies. * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const OpenTimestamps = require('opentimestamps'); const nwglobal = require('nwglobal'); const {jsPDF} = window.jspdf; const certificateVersion = 1; const pdfPageScale = 3; const pdfAssumedDPI = 72; var pdfDoc = null; var pageNumber = 0; var pdfTitle = ""; function addPDF() { openFileDialog(function (path) { var filedata = getFileAsUint8Array(path); // Get filename // https://stackoverflow.com/a/424006 pdfTitle = path.split('\\').pop().split('/').pop(); pdfjsLib.getDocument(filedata).promise.then(function (pdfDoc_) { pdfDoc = pdfDoc_; pdfDoc.getMetadata().then(function (meta) { if (typeof meta.contentDispositionFilename == "string") { pdfTitle = meta.contentDispositionFilename; } }).catch(function (err) { console.log('Error getting PDF metadata: ', err); }); renderAllPages(pdfDoc); pdfZoom("fitheight"); $(".enable-when-doc-open").removeClass("disabled"); // Initial/first page rendering //renderPage(pageNum); }); }, ".pdf"); } function closePDF(showuserconfirm) { if (showuserconfirm && !confirm("Are you sure you want to close? All unsaved changes will be lost.")) { return; } disableGuideBox(); pageNumber = 0; pdfDoc = null; $("#page-canvas-container .page-canvas").remove(); $(".enable-when-doc-open").addClass("disabled"); } function analyzeSignedPDF() { if ($("#page-canvas-container .page-canvas").length > 0 && !confirm("Opening a PDF to analyze will close the open document. Are you sure?")) { return; } closePDF(false); openFileDialog(function (path, html5file) { var analyze = function (pdf) { var splitindex = pdf.indexOf("-----BEGIN PGP MESSAGE-----"); if (splitindex == -1) { showAlert("Selected file does not contain any recognized signature data."); return; } var pdfdata = pdf.slice(0, splitindex); var sigdata = pdf.slice(splitindex).toString(); var verify = function (pdfhash, reload) { loadKeyFromLocalStorage(function () { verifyMessage(sigdata, function (msg, fprint) { parseAndDisplaySignature(msg, pdfhash, true, fprint); }, function (err) { console.error(err); try { readSignatureExternally(sigdata, function (msg, keyprint, signername, verified, ok) { if (!ok) { showAlert("Error: could not parse signature data."); return; } // If system GPG has the public key, use that. // Otherwise, try looking up the fingerprint online and if we get hits // add them to the local keyring and try verifying again. if (verified) { parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername); } else { if (typeof reload == 'undefined' || reload == false) { lookupPublicKey(keyprint, function (res) { if (res == false) { parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername); } else { importPublicKeysFromRegistry(res, function () { verify(pdfhash, true); }); } }); } else { parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername); } } }); } catch (ex) { console.error(ex); var base64 = sigdata.split("\n\n", 2)[1].split("\n-----END PGP MESSAGE-----")[0]; base64 = base64.substring(0, base64.lastIndexOf("\n")).replaceAll("\n", ""); try { var msg = window.atob(base64).split("START", 2)[1].split("END", 2)[0]; parseAndDisplaySignature(msg, pdfhash, false, null); } catch (exx) { console.error(exx); } } }); }); if (typeof reload == 'undefined' || reload == false) { if (typeof nw != 'undefined') { pdfjsLib.getDocument(pdf).promise.then(function (pdfDoc_) { pdfDoc = pdfDoc_; renderAllPages(pdfDoc); pdfZoom("fitheight"); }); } else { var fileReader = new FileReader(); fileReader.onload = function () { pdfjsLib.getDocument(new Uint8Array(this.result)).promise.then(function (pdfDoc_) { pdfDoc = pdfDoc_; renderAllPages(pdfDoc); pdfZoom("fitheight"); }); }; fileReader.readAsArrayBuffer(html5file); } $(".enable-when-doc-open").removeClass("disabled"); } }; if (typeof nw != 'undefined') { verify(calculateSHA256HashOfString(pdfdata)); } else { window.crypto.subtle.digest("SHA-256", (new TextEncoder()).encode(pdfdata)) .then(hash => { window.hash = hash; // here hash is an arrayBuffer, // so we'll connvert it to its hex version let result = ''; const view = new DataView(hash); for (let i = 0; i < hash.byteLength; i += 4) { result += ('00000000' + view.getUint32(i).toString(16)).slice(-8); } verify(result); }); } }; if (typeof nw != 'undefined') { // running in NW.js so we have Node analyze(Buffer.from(getFileAsUint8Array(path).buffer)); } else { // no Node :( var fileReader = new FileReader(); fileReader.onload = function (e) { analyze(e.target.result); } fileReader.readAsBinaryString(html5file); } }, ".pdf"); } function parseAndDisplaySignature(msg, pdfhash, verified, fingerprint, signername) { var msgparts = {}; // Decode message contents var msglines = msg.split("\n"); for (var i = 0; i < msglines.length; i++) { if (msglines[i].includes(":")) { var parts = msglines[i].split(":", 2); msgparts[parts[0]] = parts[1]; } } if (typeof msgparts["HASH"] == "string") { if (msgparts["HASH"] == pdfhash) { if (verified) { $("#verifyModalStatusMessage").html(" File contents match signature. File has not been changed since notarization."); $("#verifyModalStatusMessage").removeClass(); $("#verifyModalStatusMessage").addClass(["alert", "alert-success"]); } else { $("#verifyModalStatusMessage").html(" File contents match signature; however, \ could not verify signature authenticity. It's possible the file was changed then re-signed by an unknown person. If you have the \ public key file for the notary that signed the file, click here to use it, \ then run the analyze tool again to prove if it was changed since notarization."); $("#verifyModalStatusMessage").removeClass(); $("#verifyModalStatusMessage").addClass(["alert", "alert-warning"]); } } else { $("#verifyModalStatusMessage").html(" File contents do not match signature. Document has been modified since notarization."); $("#verifyModalStatusMessage").removeClass(); $("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]); } } else { $("#verifyModalStatusMessage").html(" No file hash found in document signature. Could not verify document integrity."); $("#verifyModalStatusMessage").removeClass(); $("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]); } // Add extra data to a list below the big message $("#verifyModalDetailedInfoList").html(""); if (typeof msgparts["DATE"] == "string" && isNaN(msgparts["DATE"]) == false) { var datestr = formatTimestamp("F j, Y g:i a", msgparts["DATE"]); $("#verifyModalDetailedInfoList").append('
  • Notarization date/time: ' + datestr + '
  • '); } if (typeof msgparts["NOTARY"] == "string") { $("#verifyModalDetailedInfoList").append('
  • Notary: ' + sanitizeHTMLString(msgparts["NOTARY"]) + '
  • '); } if (typeof msgparts["STATE"] == "string") { $("#verifyModalDetailedInfoList").append('
  • State: ' + sanitizeHTMLString(msgparts["STATE"]).toUpperCase() + '
  • '); } if (typeof msgparts["OTS"] == "string") { try { var bytearray = new nwglobal.Array(); var bytestrarray = msgparts["OTS"].match(/.{1,3}/g); for (var i = 0; i < bytestrarray.length; i++) { bytearray.push(bytestrarray[i] * 1); } const fileHashArr = nwglobal.Uint8Array.from(Buffer.from(pdfhash, 'hex')); const detached = OpenTimestamps.DetachedTimestampFile.fromHash(new OpenTimestamps.Ops.OpSHA256(), fileHashArr); const detachedOts = OpenTimestamps.DetachedTimestampFile.deserialize(bytearray); let options = { ignoreBitcoinNode: true, timeout: 5000 }; OpenTimestamps.verify(detachedOts, detached, options).then(verifyResult => { if (typeof verifyResult != "undefined") { if (typeof verifyResult.bitcoin != undefined) { $("#verifyModalDetailedInfoList").append('
  • ' + 'Signing time witnessed by the Bitcoin network, proving it was signed within a few hours of ' + formatTimestamp("g A", verifyResult.bitcoin.timestamp) + ' on ' + formatTimestamp("F j, Y", verifyResult.bitcoin.timestamp) + '.' + '
  • '); } if (typeof verifyResult.litecoin != undefined) { $("#verifyModalDetailedInfoList").append('
  • ' + 'Signing time witnessed by the Litecoin network, proving it was signed within a few hours of ' + formatTimestamp("g A", verifyResult.litecoin.timestamp) + ' on ' + formatTimestamp("F j, Y", verifyResult.litecoin.timestamp) + '.' + '
  • '); } } }); } catch (ex) { console.error(ex); } } $("#verifyModalDetailedInfoList").append('
  • Actual file hash: ' + pdfhash + '
  • '); if (typeof msgparts["HASH"] == "string") { $("#verifyModalDetailedInfoList").append('
  • Signed file hash: ' + sanitizeHTMLString(msgparts["HASH"]) + '
  • '); } if (typeof fingerprint == "string") { if (fingerprint.length > 16) { var fingerprintstart = fingerprint.substr(0, fingerprint.length - 16); var fingerprintend = fingerprint.substr(-16); } else { var fingerprintstart = ""; var fingerprintend = fingerprint; } $("#verifyModalDetailedInfoList").append('
  • Public key ID: ' + fingerprintstart + '' + fingerprintend + '
  • '); lookupPublicKey(fingerprint, function (result) { if (result == false) { return; } if (result.length == 1) { $("#verifyModalDetailedInfoList").append('
  • ' + 'The signing notary\'s information was found in an online database.
    ' + '' + (result[0].name == null ? '' : 'Name: ' + sanitizeHTMLString(result[0].name) + '
    ') + (result[0].email == null ? '' : 'Email: ' + sanitizeHTMLString(result[0].email) + '
    ') + (result[0].state == null ? '' : 'State: ' + sanitizeHTMLString(result[0].state.toUpperCase()) + '
    ') + (result[0].location == null ? '' : 'Location: ' + sanitizeHTMLString(result[0].location) + '
    ') + (result[0].idnumber == null ? '' : 'Commission ID: ' + sanitizeHTMLString(result[0].idnumber) + '
    ') + (result[0].commissionexpires == null ? '' : 'Commission Expires: ' + formatTimestamp("F j, Y", result[0].commissionexpires)) + '
    ' + '
  • '); } }); } if (typeof signername == "string") { $("#verifyModalDetailedInfoList").append('
  • Owner of public key: ' + sanitizeHTMLString(signername) + '
  • '); } new bootstrap.Modal(document.getElementById('verifyModal')).show(); } function generatePDF(callback) { var canvases = $("#page-canvas-container .page-canvas"); var statustextEl = $("#statustext"); statustextEl.html(" Processing document..."); const pdf = new jsPDF({ unit: "in", compress: true }); // creating a PDF creates a blank page that we don't want to use, // as we haven't done the calculations yet pdf.deletePage(1); // Render each page in order with a pause in between to keep UI responsive var processPage = function (i) { if (i < canvases.length) { statustextEl.html(" Processing page " + (i + 1) + " of " + canvases.length + "..."); console.log("Processing " + (i + 1)); var canvas = canvases[i]; var widthpx = canvas.getContext("2d").canvas.width; var heightpx = canvas.getContext("2d").canvas.height; var pageWidthInches = widthpx / (pdfAssumedDPI * pdfPageScale); var pageHeightInches = heightpx / (pdfAssumedDPI * pdfPageScale); console.log(pageWidthInches + " x " + pageHeightInches); var pageFormat = [pageWidthInches, pageHeightInches]; var pageOrientation = (pageWidthInches > pageHeightInches ? "landscape" : "portrait"); pdf.addPage(pageFormat, pageOrientation); pdf.addImage(canvases[i].toDataURL(), 0, 0, pageWidthInches, pageHeightInches); i++; setTimeout(function () { processPage(i) }, 100); } else { statustextEl.html(""); callback(pdf); } } processPage(0); } function getPDFAsByteArray(pdf) { return pdf.output("arraybuffer"); } function makeAndSaveSignedPDF(pdf, savepath, callback) { var pdfbuffer = pdf.output("arraybuffer"); const hashstr = calculateSHA256HashOfBuffer(pdfbuffer); var detached = OpenTimestamps.DetachedTimestampFile.fromHash(new OpenTimestamps.Ops.OpSHA256(), nwglobal.Uint8Array.from(Buffer.from(hashstr, 'hex'))); var otsbytes = ""; var sign = function () { var message = "START" + "\nV:" + certificateVersion + "\nHASH:" + hashstr + "\nDATE:" + time() + (otsbytes != "" ? "\nOTS:" + otsbytes : "") + "\nNOTARY:" + getStorage("notary_name") + "\nSTATE:" + getStorage("notary_state") + "\nEND\n"; signMessage(message, keymgr, function (sig) { writeToFile(savepath, Buffer.from(pdfbuffer)); appendToFile(savepath, sig); //writeToFile(savepath + ".notsigned.pdf", Buffer.from(pdfbuffer)); writeToFile(savepath + ".sig", sig); callback({ signature: sig, hash: hashstr }); }); }; OpenTimestamps.stamp(detached).then(() => { var bytearray = detached.serializeToBytes(); var bytestr = ""; for (var i = 0; i < bytearray.length; i++) { bytestr += (bytearray[i] + "").padStart(3, "0"); } otsbytes = bytestr; sign(); }).catch(() => { sign(); }); } function savePDF() { disableGuideBox(); var statustextEl = $("#statustext"); loadKeyFromLocalStorage(function (message, ok) { if (ok) { openSaveFileDialog(function (path) { generatePDF(function (pdf) { statustextEl.html(" Signing document..."); makeAndSaveSignedPDF(pdf, path, function (result) { statustextEl.html(" Signed and saved!"); showAlert(" File signed and saved. SHA256 of file (excluding appended signature): " + result.hash); setTimeout(function () { statustextEl.html(""); }, 5000); }); }); }, "signed.pdf", ".pdf"); } else { statustextEl.html(""); showAlert("Error: " + message); } }); } function pdfZoom(str) { disableGuideBox(); if ($("#page-canvas-container .page-canvas").length == 0) { setTimeout(function () { pdfZoom(str); }, 100); return; } var widthpx = $("#page-canvas-container .page-canvas").css("width").replace("px", "") * 1; var zoomstep = 100; switch (str) { case "out": $("#page-canvas-container .page-canvas").css("height", "auto"); widthpx -= zoomstep; $("#page-canvas-container .page-canvas").css("width", widthpx + "px"); break; case "in": $("#page-canvas-container .page-canvas").css("height", "auto"); widthpx += zoomstep; $("#page-canvas-container .page-canvas").css("width", widthpx + "px"); break; case "fitwidth": $("#page-canvas-container .page-canvas").css("width", "calc(100% - 1rem)"); $("#page-canvas-container .page-canvas").css("height", "auto"); break; case "fitheight": $("#page-canvas-container .page-canvas").css("height", "calc(100% - 1rem)"); $("#page-canvas-container .page-canvas").css("width", "auto"); break; } } function getNewCanvas(pagenumber) { var canvas = document.createElement('canvas'); canvas.id = "pdf-canvas-page-" + pagenumber; canvas.className = "page-canvas"; return canvas; } function addPage() { if ($("#page-canvas-container .page-canvas").length == 0) { showToast("Please open a document first."); return; } pageNumber++; var canvas = getNewCanvas(pageNumber); var prevPageCanvas = $("#page-canvas-container .page-canvas#pdf-canvas-page-" + (pageNumber - 1))[0]; canvas.width = prevPageCanvas.getContext("2d").canvas.width; canvas.height = prevPageCanvas.getContext("2d").canvas.height; var ctx = canvas.getContext('2d'); ctx.fillStyle = 'white'; ctx.fillRect(0, 0, canvas.width, canvas.height); $("#page-canvas-container").append(canvas); } function renderAllPages() { var startingPageNumber = pageNumber; var thisDocPageCount = pdfDoc.numPages; for (var i = 1; i <= pdfDoc.numPages; i++) { pdfDoc.getPage(i).then(function (page) { var viewport = page.getViewport({scale: pdfPageScale}); var canvas = getNewCanvas(page.pageNumber + startingPageNumber); canvas.height = viewport.height; canvas.width = viewport.width; $("#page-canvas-container").append(canvas); // Render PDF page into canvas context var renderContext = { canvasContext: canvas.getContext("2d"), viewport: viewport }; page.render(renderContext); }); } pageNumber = pageNumber + thisDocPageCount; //document.getElementById('page_count').textContent = pageNumber; }