diff --git a/src/index.html b/src/index.html index be0263a..252ef12 100644 --- a/src/index.html +++ b/src/index.html @@ -100,6 +100,24 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. + +
diff --git a/src/js/crypto.js b/src/js/crypto.js index f33088c..db11a8c 100644 --- a/src/js/crypto.js +++ b/src/js/crypto.js @@ -6,6 +6,7 @@ */ var keymgr; +var keyring = new kbpgp.keyring.KeyRing(); /** * Load and unlock the private key in localstorage, prompting user as needed. If there is no key, generates, saves, and loads a new one. @@ -26,6 +27,7 @@ function loadKeyFromLocalStorage(callback) { return; } keymgr = key; + keyring.add_key_manager(keymgr); setStorage("signingkey", keymgr.armored_pgp_private); callback("Signing key generated.", true); }); @@ -37,6 +39,7 @@ function loadKeyFromLocalStorage(callback) { return; } keymgr = key; + keyring.add_key_manager(keymgr); callback("Signing key unlocked.", true); }); } @@ -110,6 +113,33 @@ function signMessage(text, key, callback) { }); } +/** + * Read a signed PGP message and return the contents and signer's fingerprint/key ID. + * @param {string} pgpmsg "-----BEGIN PGP MESSAGE----- ..." + * @param {function} callback function(message, fingerprint) {} + * @param {function} onerror function(errormessage) {} + * @returns {undefined} + */ +function verifyMessage(pgpmsg, callback, onerror) { + kbpgp.unbox({keyfetch: keyring, armored: pgpmsg}, function (err, literals) { + if (err != null) { + onerror(err); + } else { + var message = literals[0].toString(); + var fingerprint = null; + var ds = km = null; + ds = literals[0].get_data_signer(); + if (ds) { + km = ds.get_key_manager(); + } + if (km) { + fingerprint = km.get_pgp_fingerprint().toString('hex'); + } + callback(message, fingerprint); + } + }); +} + /** * Generate a new private key. * @param {string} userid Something like "Test User " @@ -231,6 +261,34 @@ function importPrivateKey() { }, ".asc"); } +function calculateSHA256HashOfBuffer(buffer) { + const hasha = require('hasha'); + var hashstr = hasha(Buffer.from(buffer), {algorithm: 'sha256'}); + return hashstr; +} + +function calculateSHA256HashOfString(str) { + const hasha = require('hasha'); + var hashstr = hasha(str, {algorithm: 'sha256'}); + return hashstr; +} + +function openPublicKeyFile() { + openFileDialog(function (path) { + var keyfile = getFileAsString(path); + + kbpgp.KeyManager.import_from_armored_pgp({ + armored: keyfile + }, function (err, pubkeymgr) { + if (!err) { + keyring.add_key_manager(pubkeymgr); + alert("Public key file loaded. You can now analyze PDFs signed by the key's owner."); + } else { + alert("Error loading public key: " + err); + } + }); + }, ".asc"); +} /** * Show visual indicator when private key is not loaded/unlocked. diff --git a/src/js/pdf.js b/src/js/pdf.js index c3e4730..85c0d6d 100644 --- a/src/js/pdf.js +++ b/src/js/pdf.js @@ -16,9 +16,6 @@ function addPDF() { openFileDialog(function (path) { var filedata = getFileAsUint8Array(path); - /** - * Asynchronously downloads PDF. - */ pdfjsLib.getDocument(filedata).promise.then(function (pdfDoc_) { pdfDoc = pdfDoc_; @@ -40,6 +37,101 @@ function closePDF(showuserconfirm) { $("#page-canvas-container .page-canvas").remove(); } +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) { + var pdf = Buffer.from(getFileAsUint8Array(path).buffer); + + var splitindex = pdf.indexOf("-----BEGIN PGP MESSAGE-----"); + if (splitindex == -1) { + alert("Selected file does not contain any recognized signature data."); + return; + } + var pdfdata = pdf.slice(0, splitindex); + var sigdata = pdf.slice(splitindex).toString(); + + var pdfhash = calculateSHA256HashOfString(pdfdata); + + verifyMessage(sigdata, function (msg, fprint) { + parseAndDisplaySignature(msg, pdfhash, true, fprint); + }, function (err) { + 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 = atob(base64).split("START", 2)[1].split("END", 2)[0]; + parseAndDisplaySignature(msg, pdfhash, false, null); + } catch (ex) { + console.error(ex); + alert("Error: could not parse signature data."); + } + }); + + pdfjsLib.getDocument(pdf).promise.then(function (pdfDoc_) { + pdfDoc = pdfDoc_; + + renderAllPages(pdfDoc); + pdfZoom("fitheight"); + }); + + }, ".pdf"); +} + +function parseAndDisplaySignature(msg, pdfhash, verified, fingerprint) { + 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 try running the analyze tool again."); + $("#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 fingerprint == "string") { + $("#verifyModalDetailedInfoList").append('
  • Signature fingerprint: ' + fingerprint + '
  • '); + } + new bootstrap.Modal(document.getElementById('verifyModal')).show(); +} + function generatePDF(callback) { var canvases = $("#page-canvas-container .page-canvas"); var statustextEl = $("#statustext"); @@ -84,15 +176,14 @@ function getPDFAsByteArray(pdf) { } function makeAndSaveSignedPDF(pdf, savepath, callback) { - const hasha = require('hasha'); - var pdfbuffer = pdf.output("arraybuffer"); - var hashstr = hasha(Buffer.from(pdfbuffer), {algorithm: 'sha256'}); - - var message = "HASH:" + hashstr + var hashstr = calculateSHA256HashOfBuffer(pdfbuffer); + var message = "START" + + "\nHASH:" + hashstr + + "\nDATE:" + time() + "\nNOTARY:" + getStorage("notary_name") + "\nSTATE:" + getStorage("notary_state") - + "\n"; + + "\nEND\n"; signMessage(message, keymgr, function (sig) { writeToFile(savepath, Buffer.from(pdfbuffer)); appendToFile(savepath, sig); diff --git a/src/js/util.js b/src/js/util.js index e0819ee..52b68d0 100644 --- a/src/js/util.js +++ b/src/js/util.js @@ -16,6 +16,12 @@ function uuidv4() { }); } +function sanitizeHTMLString(str) { + var element = document.createElement('div'); + element.innerText = str; + return element.innerHTML; +} + /** * Take a UNIX timestamp (seconds since Jan 1 1970) and format it. * (Mostly) compatible with PHP's date() function.