Add online notary key registry, add button to erase private key, don't autogenerate keys (so user reads the docs first)
This commit is contained in:
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-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="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-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>
|
||||
|
||||
273
src/js/crypto.js
273
src/js/crypto.js
@ -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,18 +20,8 @@ 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) {
|
||||
if (typeof key == "undefined") {
|
||||
callback("Could not generate key.", false);
|
||||
return;
|
||||
}
|
||||
keymgr = key;
|
||||
keyring.add_key_manager(keymgr);
|
||||
setStorage("signingkey", keymgr.armored_pgp_private);
|
||||
callback("Signing key generated.", true);
|
||||
});
|
||||
});
|
||||
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) {
|
||||
@ -41,12 +31,31 @@ function loadKeyFromLocalStorage(callback) {
|
||||
}
|
||||
keymgr = key;
|
||||
keyring.add_key_manager(keymgr);
|
||||
callback("Signing key unlocked.", true);
|
||||
callback("Private key unlocked.", true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 generate key.", false);
|
||||
return;
|
||||
}
|
||||
keymgr = key;
|
||||
keyring.add_key_manager(keymgr);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
function unloadKey() {
|
||||
keymgr = undefined;
|
||||
$("#lockstatus").css("display", "");
|
||||
@ -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() {
|
||||
function exportPublicKeyToFile() {
|
||||
getOwnPublicKey(function (pgp_public) {
|
||||
if (pgp_public == false) {
|
||||
showAlert("Something went wrong.");
|
||||
} else {
|
||||
openSaveFileDialog(function (path) {
|
||||
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) {
|
||||
openSaveFileDialog(function (path) {
|
||||
keymgr.export_pgp_public({}, function (err, pgp_public) {
|
||||
if (err) {
|
||||
showAlert("Something went wrong.");
|
||||
} else {
|
||||
writeToFile(path, pgp_public);
|
||||
}
|
||||
});
|
||||
}, "public-key.asc", ".asc");
|
||||
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') {
|
||||
|
||||
@ -26,3 +26,11 @@ function showPasswordPrompt(message, callback) {
|
||||
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…
x
Reference in New Issue
Block a user