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
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); |