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.