Add online notary key registry, add button to erase private key, don't autogenerate keys (so user reads the docs first)

master
Skylar Ittner 3 years ago
parent 3db51d0cf5
commit 99b8173f7e

@ -72,23 +72,28 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
Signatures are generated using your private key, which you must keep secret.
Someone with the private key can modify a signed PDF without detection.
<b>Protect your private key like you protect your notary stamp/seal.</b>
<br>
<br><br>
A corresponding public key is also available; anyone with your public key can
verify you signed a document, but cannot do anything else.
It is recommended to post your public key somewhere public like a website.
This way, people can ensure documents you notarized are valid without contacting you for a
copy of your public key. It is also recommended to
<b>back up your private key</b> in case your computer malfunctions. This will ensure you don't need
to make a new one. A new key won't be able to verify older signatures or vice versa. Some states
require you use only one key for the entire term of your commission.
To make verifying documents easier, the creator of this software
maintains a public online registry of public keys; you can upload your
key and notary profile with the button below.
<br><br>
It is also recommended to <b>back up your private key</b> in case your computer malfunctions.
This will ensure you don't need to make a new one. A new key won't be able to verify older signatures or vice versa.
Some states require you use only one key for the entire term of your commission.
</p>
<a class="btn btn-info m-1" target="_blank" href="https://docs.netsyms.com/docs/IPENtool/Cryptography%20101/"><i class="fas fa-graduation-cap"></i> Learn More</a>
<div class="btn btn-primary m-1" onclick="loadKeyFromLocalStorageWithUserFeedback()"><i class="fas fa-unlock"></i> Create/unlock private key</div>
<div class="btn btn-primary m-1" onclick="unloadKey()"><i class="fas fa-lock"></i> Lock private key</div>
<div class="btn btn-primary m-1" onclick="exportPublicKey()"><i class="fas fa-file-export"></i> Export public key</div>
<div class="btn btn-primary m-1" onclick="createKeyWithUserFeedback()"><i class="fas fa-key"></i> Create new private key</div>
<div class="btn btn-primary m-1" onclick="unloadKey()"><i class="fas fa-lock"></i> Lock private key (require password on next use)</div>
<br>
<div class="btn btn-primary m-1" onclick="exportPrivateKey()"><i class="fas fa-download"></i> Back up private key</div>
<div class="btn btn-primary m-1" onclick="importPrivateKey()"><i class="fas fa-upload"></i> Restore private key</div>
<br>
<div class="btn btn-danger m-1" onclick="exportPrivateKey()"><i class="fas fa-download"></i> Back up private key</div>
<div class="btn btn-danger m-1" onclick="importPrivateKey()"><i class="fas fa-upload"></i> Restore private key</div>
<div class="btn btn-primary m-1" onclick="exportPublicKeyToFile()"><i class="fas fa-file-export"></i> Export public key to file</div>
<div class="btn btn-primary m-1" onclick="exportPublicKeyToRegistry()"><i class="fas fa-cloud-upload-alt"></i> Upload public key to online registry</div>
<br><br><br>
<div class="btn btn-danger m-1" onclick="erasePrivateKey()"><i class="fas fa-exclamation-triangle"></i> Erase private key</div>
</div>
<div class="col-12 col-md-6 col-lg-4" id="appOptionsSettings">
<h5><i class="fas fa-sliders-h"></i> App Options</h5>
@ -281,6 +286,18 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
</div>
</div>
<div class="modal fade" id="okCancelPromptModal" tabindex="-1" aria-labelledby="okCancelPromptModalText" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-body p-1" id="okCancelPromptModalText"></div>
<div class="modal-footer p-1">
<button type="button" class="btn btn-secondary" onclick="okCancelPromptModalCallback(false);" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="okCancelPromptModalCallback(true);" data-bs-dismiss="modal">Okay</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
@ -296,7 +313,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
</div>
</div>
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 999999;">
<div class="position-fixed bottom-0 start-50 translate-middle-x p-3" style="z-index: 999999;">
<div id="toastBox" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-body"></div>
</div>

@ -9,7 +9,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.
* Load and unlock the private key in localstorage, prompting user as needed.
* @param {function} callback Passed two arguments: message for user, and boolean true if OK false if error.
* @returns {undefined}
*/
@ -20,30 +20,39 @@ function loadKeyFromLocalStorage(callback) {
}
$("#lockstatus").css("display", "none");
if (!inStorage("signingkey") || getStorage("signingkey") == "undefined") {
showPasswordPrompt("Generating a new signing key (might take a while, be patient). Enter a password to protect it. You'll need to save this password somewhere safe; it cannot be recovered.", function (pass) {
generatePrivateKey(getStorage("notary_name") + " <" + (inStorage("notary_email") ? getStorage("notary_email") : "null@null.com") + ">", pass, function (key) {
callback("You do not have a private key. Click Settings to create one.", false);
return;
} else {
showPasswordPrompt("Enter password to unlock signing key:", function (pass) {
loadPrivateKey(getStorage("signingkey"), pass, function (key) {
if (typeof key == "undefined") {
callback("Could not generate key.", false);
callback("Could not unlock key. Password is probably incorrect.", false);
return;
}
keymgr = key;
keyring.add_key_manager(keymgr);
setStorage("signingkey", keymgr.armored_pgp_private);
callback("Signing key generated.", true);
callback("Private key unlocked.", true);
});
});
} else {
showPasswordPrompt("Enter password to unlock signing key:", function (pass) {
loadPrivateKey(getStorage("signingkey"), pass, function (key) {
}
}
function createKey(callback) {
if (!inStorage("signingkey") || getStorage("signingkey") == "undefined") {
showPasswordPrompt("Generating a new private key (might take a while, be patient). Enter a password to protect it. You'll need to memorize or write down this password; it cannot be recovered.", function (pass) {
generatePrivateKey(getStorage("notary_name") + " <" + (inStorage("notary_email") ? getStorage("notary_email") : "noemailaddressprovided@example.com") + ">", pass, function (key) {
if (typeof key == "undefined") {
callback("Could not unlock key. Password is probably incorrect.", false);
callback("Could not generate key.", false);
return;
}
keymgr = key;
keyring.add_key_manager(keymgr);
callback("Signing key unlocked.", true);
setStorage("signingkey", keymgr.armored_pgp_private);
callback("Private key generated.", true);
});
});
} else {
callback("You already have a private key. You must erase it before generating a new one.", false);
}
}
@ -63,6 +72,16 @@ function loadKeyFromLocalStorageWithUserFeedback() {
});
}
function createKeyWithUserFeedback() {
createKey(function (msg, ok) {
if (ok) {
showToast("<i class='fas fa-check'></i> " + msg);
} else {
showAlert("Error: " + msg);
}
});
}
/**
* Load a private key.
* @param {string} armoredkey PGP private key
@ -184,18 +203,68 @@ function generatePrivateKey(userid, passphrase, callback) {
}, 100);
}
function exportPublicKey() {
loadKeyFromLocalStorage(function (message, ok) {
if (ok) {
function exportPublicKeyToFile() {
getOwnPublicKey(function (pgp_public) {
if (pgp_public == false) {
showAlert("Something went wrong.");
} else {
openSaveFileDialog(function (path) {
keymgr.export_pgp_public({}, function (err, pgp_public) {
if (err) {
showAlert("Something went wrong.");
} else {
writeToFile(path, pgp_public);
}
});
writeToFile(path, pgp_public);
showAlert("Public key saved.");
}, "public-key.asc", ".asc");
}
});
}
function exportPublicKeyToRegistry() {
if (!inStorage("signingkey") || getStorage("signingkey") == "undefined") {
showAlert("You must create and back up your private key first.");
return;
}
showOkCancelPrompt("Double-check that your notary profile is complete and that you pressed the Save button after making any changes before continuing.", function (ok) {
if (!ok) {
return;
}
getOwnPublicKey(function (pgp_public) {
if (pgp_public == false) {
showAlert("Something went wrong.");
} else {
submitPublicKeyToRegistry(
pgp_public,
getStorage("notary_name"),
getStorage("notary_email"),
getStorage("notary_location"),
getStorage("notary_expires"),
getStorage("notary_idnumber"),
getStorage("notary_state"),
function (msg, ok) {
if (!ok) {
showAlert("Error: " + msg);
} else {
showAlert(msg);
}
}
);
}
});
});
}
/**
* Get user's own public key, prompting for key password if needed.
* @param {function} callback cb(result): public key string or false on error
* @returns {undefined}
*/
function getOwnPublicKey(callback) {
loadKeyFromLocalStorage(function (message, ok) {
if (ok) {
keymgr.export_pgp_public({}, function (err, pgp_public) {
if (err) {
callback(false);
} else {
callback(pgp_public);
}
});
} else {
showAlert("Error: " + message);
}
@ -324,14 +393,11 @@ function calculateSHA256HashOfString(str) {
function openPublicKeyFile() {
openFileDialog(function (path, html5file) {
var importpk = function (keyfile) {
kbpgp.KeyManager.import_from_armored_pgp({
armored: keyfile
}, function (err, pubkeymgr) {
if (!err) {
keyring.add_key_manager(pubkeymgr);
addPublicKeyToKeyring(keyfile, function (res) {
if (res === true) {
showAlert("Public key file loaded. You can now analyze PDFs signed by the key's owner.");
} else {
showAlert("Error loading public key: " + err);
showAlert("Error loading public key: " + res);
}
});
};
@ -349,6 +415,150 @@ function openPublicKeyFile() {
}, ".asc");
}
/**
*
* @param {type} keyfile
* @param {type} callback cb(result): result is true if successful, an error string if failed.
* @returns {undefined}
*/
function addPublicKeyToKeyring(keyfile, callback) {
kbpgp.KeyManager.import_from_armored_pgp({
armored: keyfile
}, function (err, pubkeymgr) {
if (!err) {
keyring.add_key_manager(pubkeymgr);
callback(true);
} else {
callback(err);
}
});
}
/**
* Look up a full or partial public key fingerprint with the Netsyms notary registry.
* @param {string} fingerprint
* @param {function} callback cb(result): `result` is an array of notary info and keys (see below), or `false` if there was an error or no results.
* @returns {undefined}
*
* result = [{
* fingerprint,
* name,
* email,
* location,
* commissionexpires,
* idnumber,
* state,
* publickey
* }]
*
* All but fingerprint and publickey could be null.
*
*/
function lookupPublicKey(fingerprint, callback) {
$.ajax({
url: "https://data.netsyms.net/v1/notary/fetchkey/",
dataType: "json",
data: {
fingerprint: fingerprint
},
success: function (resp) {
if (resp.count == 0) {
callback(false);
return;
}
callback(resp.results);
},
error: function () {
callback(false);
}
});
}
/**
* Import multiple public keys and only callback when all are done.
* @param {type} keys see lookupPublicKey()
* @param {function} callback
* @returns {undefined}
*/
function importPublicKeysFromRegistry(keys, callback) {
var i = 0;
var loop = function (keys) {
addPublicKeyToKeyring(keys[i].publickey, function () {
i++;
if (i < keys.length) {
loop(keys);
} else {
callback();
}
});
};
loop(keys);
}
/**
* Upload a public key to the Netsyms notary registry server.
* @param {string} pubkey PGP public key file contents, armored
* @param {string} name Notary name
* @param {string} email Notary email
* @param {string} location Notary location
* @param {string} expires Commission expiration date; server will parse.
* @param {string} idnumber Commission ID number
* @param {string} state Two-char state
* @param {function} callback ((string) message, (bool) okaytrue_errorfalse)
* @returns {undefined}
*/
function submitPublicKeyToRegistry(pubkey, name, email, location, expires, idnumber, state, callback) {
$.ajax({
url: "https://data.netsyms.net/v1/notary/publishkey/",
method: "POST",
dataType: "json",
data: {
publickey: pubkey,
name: name,
email: email,
location: location,
commissionexpires: expires,
idnumber: idnumber,
state: state
},
success: function (resp) {
if (resp.status == "OK") {
callback(resp.msg, true);
} else if (resp.status == "ERROR") {
callback(resp.msg, false);
} else {
callback("The registry server didn't send a valid response.", false);
}
},
error: function () {
callback("There was a problem communicating with the registry server. Try again later.", false);
}
});
}
/**
* Erase the local private key data with lots of prompting and dire warnings.
* @returns {undefined}
*/
function erasePrivateKey() {
showOkCancelPrompt("<div style=\"background: black; padding: 1rem;\"><b><i class='fas fa-skull-crossbones'></i> DANGER: THIS WILL RESULT IN DATA LOSS -- READ CAREFULLY</b><br><br>\n\
Erasing your private key means you won't be able to notarize or sign electronic documents without generating a new key. \n\
If you have not exported your public key, electronically verifying documents you have signed will be impossible.\n\
If you plan on using your private key in the future, press cancel and back up your private key to a file.\n\
Some states require you use the same key for the entire length of your commission.\n\
<br><br><b>IF YOU CONTINUE, YOUR PRIVATE KEY WILL NOT BE RECOVERABLE WITHOUT A BACKUP.</b></div>", function (ok) {
if (!ok) {
return;
}
var txt = prompt("To erase your private key, type \"ERASE MY SIGNING KEY\"");
if (txt.toUpperCase() != "ERASE MY SIGNING KEY") {
return;
}
unloadKey();
localStorage.removeItem("signingkey");
alert("Signing key erased.");
});
}
/**
* Show visual indicator when private key is not loaded/unlocked.
* @returns {undefined}
@ -359,4 +569,5 @@ setInterval(function () {
} else {
$("#lockstatus").css("display", "none");
}
}, 1000);
}
, 1000);

@ -70,51 +70,75 @@ function analyzeSignedPDF() {
var pdfdata = pdf.slice(0, splitindex);
var sigdata = pdf.slice(splitindex).toString();
var verify = function (pdfhash) {
var verify = function (pdfhash, reload) {
loadKeyFromLocalStorage(function () {
verifyMessage(sigdata, function (msg, fprint) {
parseAndDisplaySignature(msg, pdfhash, true, fprint);
}, function (err) {
console.error(err);
console.log(sigdata);
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 (ex) {
readSignatureExternally(sigdata, function (msg, keyprint, signername, verified, ok) {
if (!ok) {
showAlert("Error: could not parse signature data.");
return;
}
parseAndDisplaySignature(msg, pdfhash, verified, keyprint, signername);
// 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 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_) {
if (typeof reload == 'undefined' || reload == false) {
if (typeof nw != 'undefined') {
pdfjsLib.getDocument(pdf).promise.then(function (pdfDoc_) {
pdfDoc = pdfDoc_;
renderAllPages(pdfDoc);
pdfZoom("fitheight");
});
};
fileReader.readAsArrayBuffer(html5file);
} 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");
}
$(".enable-when-doc-open").removeClass("disabled");
};
if (typeof nw != 'undefined') {

@ -25,4 +25,12 @@ function showPasswordPrompt(message, callback) {
$("#passwordModalInput").val("");
passwordModalCallback = callback;
new bootstrap.Modal(document.getElementById('passwordModal')).show();
}
var okCancelPromptModalCallback = function (okay) {};
function showOkCancelPrompt(message, callback) {
$("#okCancelPromptModalText").html(message);
okCancelPromptModalCallback = callback;
new bootstrap.Modal(document.getElementById('okCancelPromptModal')).show();
}
Loading…
Cancel
Save