Add signature analysis and viewer tool (close #7)

master
Skylar Ittner 3 years ago
parent 9b6e18e04c
commit acc27de96a

@ -100,6 +100,24 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
</div>
</div>
<div class="modal fade" id="verifyModal" tabindex="-1" aria-labelledby="verifyModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="verifyModalLabel">Digital Signature Analysis Report</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="alert alert-dark" role="alert" id="verifyModalStatusMessage"></div>
<ul class="list-group" id="verifyModalDetailedInfoList"></ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<div class="card d-flex flex-column h-100">
<div class="card-body">
<div class="btn-toolbar d-inline-block" role="toolbar">
@ -107,6 +125,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
<div class="btn btn-primary" onclick="addPDF();"><i class="fas fa-file-pdf"></i> Add PDF</div>
<div class="btn btn-primary" onclick="savePDF();"><i class="fas fa-save"></i> Save Signed PDF</div>
<div class="btn btn-primary" onclick="closePDF(true);"><i class="fas fa-trash"></i> Close PDF</div>
<div class="btn btn-primary" onclick="analyzeSignedPDF();"><i class="fas fa-search"></i> Analyze PDF</div>
<div class="btn btn-primary" onclick="openSettingsModal();"><i class="fas fa-cog"></i> Settings</div>
</div>
</div>

@ -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 <test@netsyms.com>"
@ -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.

@ -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("<i class=\"fas fa-check-circle\"></i> File contents match signature. File has not been changed since notarization.");
$("#verifyModalStatusMessage").removeClass();
$("#verifyModalStatusMessage").addClass(["alert", "alert-success"]);
} else {
$("#verifyModalStatusMessage").html("<i class=\"fas fa-question-circle\"></i> 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, <span class=\"btn btn-outline-secondary btn-sm\"onclick=\"openPublicKeyFile()\">click here</span> to use it, \
then try running the analyze tool again.");
$("#verifyModalStatusMessage").removeClass();
$("#verifyModalStatusMessage").addClass(["alert", "alert-warning"]);
}
} else {
$("#verifyModalStatusMessage").html("<i class=\"fas fa-exclamation-circle\"></i> File contents do not match signature. Document has been modified since notarization.");
$("#verifyModalStatusMessage").removeClass();
$("#verifyModalStatusMessage").addClass(["alert", "alert-danger"]);
}
} else {
$("#verifyModalStatusMessage").html("<i class=\"fas fa-exclamation-circle\"></i> 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('<li class="list-group-item"><i class="far fa-calendar-alt fa-fw"></i> Notarization date/time: ' + datestr + '</li>');
}
if (typeof msgparts["NOTARY"] == "string") {
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-user fa-fw"></i> Notary: ' + sanitizeHTMLString(msgparts["NOTARY"]) + '</li>');
}
if (typeof msgparts["STATE"] == "string") {
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-map-marked-alt fa-fw"></i> State: ' + sanitizeHTMLString(msgparts["STATE"]).toUpperCase() + '</li>');
}
if (typeof fingerprint == "string") {
$("#verifyModalDetailedInfoList").append('<li class="list-group-item"><i class="fas fa-fingerprint fa-fw"></i> Signature fingerprint: ' + fingerprint + '</li>');
}
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);

@ -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.

Loading…
Cancel
Save