You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

573 lines
19 KiB
JavaScript

/*
* Copyright 2021 Netsyms Technologies.
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
var keymgr;
var keyring = new kbpgp.keyring.KeyRing();
/**
* 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}
*/
function loadKeyFromLocalStorage(callback) {
if (typeof keymgr != "undefined") {
callback("Key already loaded.", true);
return;
}
$("#lockstatus").css("display", "none");
if (!inStorage("signingkey") || getStorage("signingkey") == "undefined") {
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 unlock key. Password is probably incorrect.", false);
return;
}
keymgr = key;
keyring.add_key_manager(keymgr);
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", "");
showToast("<i class='fas fa-lock'></i> Signing key locked.");
}
function loadKeyFromLocalStorageWithUserFeedback() {
loadKeyFromLocalStorage(function (msg, ok) {
if (ok) {
showToast("<i class='fas fa-unlock'></i> " + msg);
} else {
showAlert("Error: " + msg);
}
});
}
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
* @param {string} pass key password
* @param {function} callback Passed a new keymanager for the key.
* @returns {undefined}
*/
function loadPrivateKey(armoredkey, pass, callback) {
kbpgp.KeyManager.import_from_armored_pgp({
armored: armoredkey
}, function (err, key) {
if (!err) {
if (key.is_pgp_locked()) {
key.unlock_pgp({
passphrase: pass
}, function (err) {
if (!err) {
console.log("Loaded private key with passphrase");
callback(key);
} else {
console.error(err);
callback(undefined);
}
});
} else {
console.log("Loaded private key w/o passphrase");
callback(key);
}
} else {
console.error(err);
callback(undefined);
}
});
}
/**
* Sign a message with a key and return the signed message in the callback.
* @param {type} text
* @param {type} key
* @param {type} callback
* @returns {undefined}
*/
function signMessage(text, key, callback) {
var params = {
msg: text,
sign_with: key
};
kbpgp.box(params, function (err, result_string, result_buffer) {
//console.log(err, result_string, result_buffer);
callback(result_string);
});
}
/**
* 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>"
* @param {string} passphrase protects the key
* @param {function} callback Passed the keymanager for the new key
* @returns {undefined}
*/
function generatePrivateKey(userid, passphrase, callback) {
var statustextEl = $("#statustext");
statustextEl.html("<i class='fas fa-spin fa-spinner'></i> Generating cryptographic key...");
setTimeout(function () {
var F = kbpgp["const"].openpgp;
var opts = {
userid: userid,
primary: {
nbits: 2048,
flags: F.certify_keys | F.sign_data | F.auth | F.encrypt_comm | F.encrypt_storage,
expire_in: 0 // never expire
},
subkeys: []
};
kbpgp.KeyManager.generate(opts, function (err, alice) {
if (!err) {
alice.sign({}, function (err) {
alice.export_pgp_private({
passphrase: passphrase
}, function (err, pgp_private) {
statustextEl.html("<i class='fas fa-check'></i> Key generated!");
setTimeout(function () {
statustextEl.html("");
}, 5000);
callback(alice);
});
});
}
});
}, 100);
}
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) {
keymgr.export_pgp_public({}, function (err, pgp_public) {
if (err) {
callback(false);
} else {
callback(pgp_public);
}
});
} else {
showAlert("Error: " + message);
}
});
}
/**
* This should be modified to prompt the user for a backup password,
* but that doesn't work. https://github.com/keybase/kbpgp/issues/211
* @returns {undefined}
*/
function exportPrivateKey() {
loadKeyFromLocalStorage(function (message, ok) {
if (!ok) {
showAlert("Error: " + message);
return;
}
openSaveFileDialog(function (path) {
keymgr.export_pgp_private({}, function (err, pgp_private) {
if (err) {
showAlert("Something went wrong.");
} else {
writeToFile(path, pgp_private);
showAlert("Private key backup saved.");
}
});
}, "private-key.asc", ".asc");
});
}
function importPrivateKey() {
if (inStorage("signingkey") && getStorage("signingkey") != "undefined") {
if (!confirm("The restored key will replace the current key, which will be unrecoverable unless you made a backup. Continue?")) {
return;
}
}
keymgr = null;
openFileDialog(function (path) {
var keyfile = getFileAsString(path);
showPasswordPrompt("Enter password for imported key (password was set when exported):", function (pass) {
loadPrivateKey(keyfile, pass, function (key) {
if (typeof key == "undefined") {
showAlert("Could not import key. Password is probably incorrect.");
return;
}
keymgr = key;
setStorage("signingkey", keymgr.armored_pgp_private);
showAlert("Private key imported.");
});
});
}, ".asc");
}
/**
* Call the native system GPG to "decrypt" a PGP signature. This should work when the hacky "base64 decode and search for strings" method fails.
* @param {String} sigdata
* @param {Function} callback (string|null) message, (string|null) fingerprint, (string|null) signername, (bool) verified, (bool) success
* @returns {undefined}
*/
function readSignatureExternally(sigdata, callback) {
const exec = require('child_process').exec;
const os = require('os');
const process = require('process');
const sigfilepath = getNewTempFilePath() + ".asc";
writeToFile(sigfilepath, sigdata);
var gpgexecutable = "gpg";
switch (os.platform()) {
case "win32":
// Most systems will have it here
gpgexecutable = '"C:\\Program Files (x86)\\gnupg\\bin\\gpg.exe"';
if (!fs.existsSync(gpgexecutable)) {
// Let's hope it's in %PATH%
gpgexecutable = "gpg.exe";
}
break;
case "linux":
break;
default:
break;
}
var command = gpgexecutable + " -vv --decrypt " + sigfilepath;
exec(command, function (error, stdout, stderr) {
console.log(stdout);
var msg = null;
if (stdout.length > 50) {
msg = stdout;
} else {
callback(null, null, null, false, false);
}
var verified = false;
var signername = null;
console.log(stderr);
var keyid = null;
var keyidregex = /(keyid|RSA key) ([A-F0-9]+)/;
if (keyidregex.test(stderr)) {
keyid = stderr.match(keyidregex)[2];
}
var goodsigregex = /Good signature from "([a-zA-Z0-9\s]+) <.+@.+>"/;
if (goodsigregex.test(stderr)) {
// GPG actually has a matching public key, so that's cool
verified = true;
signername = stderr.match(goodsigregex)[1];
}
callback(msg, keyid, signername, verified, true);
});
}
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, html5file) {
var importpk = function (keyfile) {
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: " + res);
}
});
};
if (typeof nw != 'undefined') {
var keyfile = getFileAsString(path);
importpk(keyfile);
} else {
var fileReader = new FileReader();
fileReader.onload = function (e) {
importpk(e.target.result);
}
fileReader.readAsText(html5file);
}
}, ".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}
*/
setInterval(function () {
if (typeof keymgr == "undefined") {
$("#lockstatus").css("display", "");
} else {
$("#lockstatus").css("display", "none");
}
}
, 1000);