async function verifySignature(opts) {
// Init
const elRes = document.body.querySelector("#result");
const elResContent = document.body.querySelector("#resultContent");
let keyData, feedback, signature, verified, valid;
// Reset feedback
elRes.innerHTML = "";
elResContent.innerHTML = "";
try {
// Get key data
keyData = await fetchKeys(opts);
// Handle missing signature
if (opts.signature == null) { throw("No signature was provided."); }
// Try two different methods of signature reading
let readError = null;
try {
signature = await openpgp.message.readArmored(opts.signature);
} catch(e) {
readError = e;
}
try {
signature = await openpgp.cleartext.readArmored(opts.signature);
} catch(e) {
readError = e;
}
if (signature == null) { throw(readError) };
// Verify the signature
verified = await openpgp.verify({
message: signature,
publicKeys: keyData.publicKey
});
valid = verified.signatures[0].valid;
} catch (e) {
console.error(e);
elRes.innerHTML = e;
elRes.classList.remove('green');
elRes.classList.add('red');
return;
}
// Init feedback to empty string
feedback = '';
// If content was extracted from signature
if (keyData.sigContent) {
elResContent.innerHTML = "Signature content:
"+sigContent+"";
}
// Provide different feedback depending on key input mode
if (opts.mode == "signature" && keyData.sigUserId) {
if (valid) {
feedback += "The message was signed by the userId extracted from the signature.
";
feedback += 'UserId: '+keyData.sigUserId+'
';
feedback += "Fingerprint: "+keyData.fingerprint+"
";
elRes.classList.remove('red');
elRes.classList.add('green');
} else {
feedback += "The message's signature COULD NOT BE verified using the userId extracted from the signature.
";
feedback += 'UserId: '+keyData.sigUserId+'
';
elRes.classList.remove('green');
elRes.classList.add('red');
}
} else if (opts.mode == "signature" && keyData.sigKeyId) {
if (valid) {
feedback += "The message was signed by the keyId extracted from the signature.
";
feedback += 'KeyID: '+keyData.sigKeyId+'
';
feedback += "Fingerprint: "+keyData.fingerprint+"
";
feedback += "!!! You should manually verify the fingerprint to confirm the signer's identity !!!";
elRes.classList.remove('red');
elRes.classList.add('green');
} else {
feedback += "The message's signature COULD NOT BE verified using the keyId extracted from the signature.
";
feedback += 'KeyID: '+keyData.sigKeyId+'
';
elRes.classList.remove('green');
elRes.classList.add('red');
}
} else {
if (valid) {
feedback += "The message was signed by the provided key ("+opts.mode+").
";
feedback += "Fingerprint: "+keyData.fingerprint+"
";
elRes.classList.remove('red');
elRes.classList.add('green');
} else {
feedback += "The message's signature COULD NOT BE verified using the provided key ("+opts.mode+").
";
elRes.classList.remove('green');
elRes.classList.add('red');
}
}
// Display feedback
elRes.innerHTML = feedback;
};
async function encryptMessage(opts) {
// Init
const elEnc = document.body.querySelector("#messageEncrypted");
const elRes = document.body.querySelector("#result");
let keyData, feedback, message, encrypted;
// Reset feedback
elRes.innerHTML = "";
elEnc.value = "";
try {
// Get key data
keyData = await fetchKeys(opts);
// Handle missing message
if (opts.message == null) {
throw("No message was provided.");
}
// Encrypt the message
encrypted = await openpgp.encrypt({
message: openpgp.message.fromText(opts.message),
publicKeys: keyData.publicKey
});
} catch (e) {
console.error(e);
elRes.innerHTML = e;
elRes.classList.remove('green');
elRes.classList.add('red');
return;
}
// Display encrypted data
elEnc.value = encrypted.data;
};
async function verifyProofs(opts) {
// Init
const elRes = document.body.querySelector("#result");
let keyData, feedback = "", message, encrypted;
// Reset feedback
elRes.innerHTML = "";
try {
// Get key data
keyData = await fetchKeys(opts);
} catch (e) {
console.error(e);
elRes.innerHTML = e;
elRes.classList.remove('green');
elRes.classList.add('red');
return;
}
let notation, isVerified, verifications = [];
for (var i = 0; i < keyData.notations.length; i++) {
notation = keyData.notations[i];
if (notation[0] != "proof@metacode.biz") { continue; }
verifications.push(await verifyProof(notation[1], keyData.fingerprint));
}
// One-line sorting function (order verifications by type)
verifications = verifications.sort((a,b) => (a.type > b.type) ? 1 : ((b.type > a.type) ? -1 : 0));
// Generate feedback
feedback += `
`;
for (var i = 0; i < verifications.length; i++) {
if (verifications[i].type == "null") { continue; }
feedback += `${verifications[i].type}: `;
feedback += `${verifications[i].display}`;
if (verifications[i].isVerified) {
feedback += `verified ✔`;
} else {
feedback += `proof`;
}
feedback += `
`;
}
feedback += `
`;
// Display feedback
elRes.innerHTML = feedback;
}
async function displayProfile(opts) {
let keyData, feedback = "", notation, isVerified, verifications = [];
try {
keyData = await fetchKeys(opts);
} catch (e) {
feedback += `There was a problem fetching the keys.
`;
feedback += `${e}
`;
document.body.querySelector('#profileData').innerHTML = feedback;
document.body.querySelector('#profileName').innerHTML = "Could not load profile";
return;
}
let userData = keyData.user.user.userId;
document.body.querySelector('#profileName').innerHTML = userData.name;
document.body.querySelector('#profileAvatar').style = "";
document.body.querySelector('#profileAvatar').src = `https://www.gravatar.com/avatar/${SparkMD5.hash(userData.email)}?s=128&d=mm`;
document.title = `${userData.name} - Keyoxide`;
for (var i = 0; i < keyData.notations.length; i++) {
notation = keyData.notations[i];
if (notation[0] != "proof@metacode.biz") { continue; }
verifications.push(await verifyProof(notation[1], keyData.fingerprint));
}
// One-line sorting function (order verifications by type)
verifications = verifications.sort((a,b) => (a.type > b.type) ? 1 : ((b.type > a.type) ? -1 : 0));
// Generate feedback
feedback += ``;
feedback += `
`;
feedback += `
general information
`;
feedback += `
`;
for (var i = 0; i < keyData.publicKey.users.length; i++) {
feedback += ``;
feedback += `
email
`;
feedback += `
`;
feedback += `
`;
}
feedback += ``;
feedback += `
fingerprint
`;
feedback += `
`;
feedback += `
`;
if (verifications.length > 0) {
feedback += ``;
feedback += `
`;
feedback += `
proofs
`;
feedback += `
`;
for (var i = 0; i < verifications.length; i++) {
if (!verifications[i].type) { continue; }
feedback += ``;
feedback += `
${verifications[i].type}
`;
feedback += `
`;
feedback += `
`;
}
}
feedback += ``;
feedback += `
`;
feedback += `
actions
`;
feedback += `
`;
feedback += ``;
feedback += `
`;
feedback += `
`;
feedback += `
`;
feedback += ``;
feedback += `
`;
feedback += `
`;
feedback += `
`;
// Display feedback
document.body.querySelector('#profileData').innerHTML = feedback;
}
async function verifyProof(url, fingerprint) {
// Init
let reVerify, match, output = {url: url, type: null, proofUrl: url, proofUrlFetch: null, isVerified: false, display: null};
// DNS
if (/^dns:/.test(url)) {
output.type = "domain";
output.display = url.replace(/dns:/, '').replace(/\?type=TXT/, '');
output.proofUrl = `https://dns.shivering-isles.com/dns-query?name=${output.display}&type=TXT`;
output.proofUrlFetch = output.proofUrl;
output.url = `https://${output.display}`;
try {
response = await fetch(output.proofUrlFetch, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
json = await response.json();
reVerify = new RegExp(`openpgp4fpr:${fingerprint}`);
json.Answer.forEach((item, i) => {
if (reVerify.test(item.data)) {
output.isVerified = true;
}
});
} catch (e) {
} finally {
return output;
}
}
// Twitter
if (/^https:\/\/twitter.com/.test(url)) {
output.type = "twitter";
match = url.match(/https:\/\/twitter\.com\/(.*)\/status\/(.*)/);
output.display = `@${match[1]}`;
output.url = `https://twitter.com/${match[1]}`;
output.proofUrlFetch = `/server/verifyTweet.php?id=${match[2]}&fp=${fingerprint}`;
try {
response = await fetch(output.proofUrlFetch, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
json = await response.json();
output.isVerified = json.verified;
} catch (e) {
} finally {
return output;
}
}
// HN
if (/^https:\/\/news.ycombinator.com/.test(url)) {
output.type = "hn";
match = url.match(/https:\/\/news.ycombinator.com\/user\?id=(.*)/);
output.display = match[1];
output.proofUrlFetch = `https://hacker-news.firebaseio.com/v0/user/${match[1]}.json`;
try {
response = await fetch(output.proofUrlFetch, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
json = await response.json();
reVerify = new RegExp(`openpgp4fpr:${fingerprint}`);
if (reVerify.test(json.about)) {
output.isVerified = true;
}
} catch (e) {
} finally {
return output;
}
}
// Reddit
if (/^https:\/\/www.reddit.com\/user/.test(url)) {
output.type = "reddit";
match = url.match(/https:\/\/www.reddit.com\/user\/(.*)\/comments\/(.*)\/(.*)\//);
output.display = match[1];
output.url = `https://www.reddit.com/user/${match[1]}`;
output.proofUrlFetch = `/server/verifyReddit.php?user=${match[1]}&comment=${match[2]}&fp=${fingerprint}`;
try {
response = await fetch(output.proofUrlFetch, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
json = await response.json();
output.isVerified = json.verified;
} catch (e) {
} finally {
return output;
}
}
// Github
if (/^https:\/\/gist.github.com/.test(url)) {
output.type = "github";
match = url.match(/https:\/\/gist.github.com\/(.*)\/(.*)/);
output.display = match[1];
output.url = `https://github.com/${match[1]}`;
output.proofUrlFetch = `https://api.github.com/gists/${match[2]}`;
try {
response = await fetch(output.proofUrlFetch, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
json = await response.json();
reVerify = new RegExp(`[Verifying my OpenPGP key: openpgp4fpr:${fingerprint}]`);
if (reVerify.test(json.files["openpgp.md"].content)) {
output.isVerified = true;
}
} catch (e) {
} finally {
return output;
}
}
// Catchall
try {
response = await fetch(url, {
headers: {
Accept: 'application/json'
},
credentials: 'omit'
});
if (!response.ok) {
throw new Error('Response failed: ' + response.status);
}
json = await response.json();
if ('attachment' in json) {
// Potentially Mastodon
match = url.match(/https:\/\/(.*)\/@(.*)/);
json.attachment.forEach((item, i) => {
if (item.value === fingerprint) {
output.type = "mastodon";
output.display = `@${match[2]}@${[match[1]]}`;
output.proofUrlFetch = json.url;
output.isVerified = true;
}
});
}
} catch (e) {
} finally {
return output;
}
}
async function fetchKeys(opts) {
// Init
let lookupOpts, wkd, hkd, sig, lastPrimarySig;
let output = {
publicKey: null,
user: null,
notations: null,
sigKeyId: null,
sigUserId: null,
sigContent: null
};
// Fetch keys depending on the input mode
switch (opts.mode) {
case "plaintext":
output.publicKey = (await openpgp.key.readArmored(opts.input)).keys[0];
if (!output.publicKey) {
throw("Error: No public keys could be fetched from the plaintext input.");
}
break;
case "wkd":
wkd = new openpgp.WKD();
lookupOpts = {
email: opts.input
};
output.publicKey = (await wkd.lookup(lookupOpts)).keys[0];
if (!output.publicKey) {
throw("Error: No public keys could be fetched using WKD.");
}
break;
case "hkp":
if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
hkp = new openpgp.HKP(opts.server);
lookupOpts = {
query: opts.input
};
output.publicKey = await hkp.lookup(lookupOpts);
output.publicKey = (await openpgp.key.readArmored(output.publicKey)).keys[0];
if (!output.publicKey) {
throw("Error: No public keys could be fetched from the HKP server.");
}
break;
case "signature":
sig = (await openpgp.signature.readArmored(opts.signature));
if ('compressed' in sig.packets[0]) {
sig = sig.packets[0];
output.sigContent = (await openpgp.stream.readToEnd(await sig.packets[1].getText()));
};
output.sigUserId = sig.packets[0].signersUserId;
output.sigKeyId = (await sig.packets[0].issuerKeyId.toHex());
if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
hkp = new openpgp.HKP(opts.server);
lookupOpts = {
query: output.sigUserId ? output.sigUserId : output.sigKeyId
};
output.publicKey = await hkp.lookup(lookupOpts);
output.publicKey = (await openpgp.key.readArmored(output.publicKey)).keys[0];
if (!output.publicKey) {
throw("Error: No public keys could be extracted from the signature.");
}
break;
}
// Gather more data about the primary key and user
output.fingerprint = await output.publicKey.primaryKey.getFingerprint();
output.user = await output.publicKey.getPrimaryUser();
lastPrimarySig = output.user.selfCertification;
output.notations = lastPrimarySig.notations || [];
return output;
}
function encodeZBase32(data) {
// Source: https://github.com/openpgpjs/openpgpjs/blob/master/src/util.js
if (data.length === 0) {
return "";
}
const ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
const SHIFT = 5;
const MASK = 31;
let buffer = data[0];
let index = 1;
let bitsLeft = 8;
let result = '';
while (bitsLeft > 0 || index < data.length) {
if (bitsLeft < SHIFT) {
if (index < data.length) {
buffer <<= 8;
buffer |= data[index++] & 0xff;
bitsLeft += 8;
} else {
const pad = SHIFT - bitsLeft;
buffer <<= pad;
bitsLeft += pad;
}
}
bitsLeft -= SHIFT;
result += ALPHABET[MASK & (buffer >> bitsLeft)];
}
return result;
}
async function computeWKDLocalPart(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hash = await crypto.subtle.digest('SHA-1', data);
return encodeZBase32(new Uint8Array(hash));
}
// General purpose
let elFormVerify = document.body.querySelector("#form-verify"),
elFormEncrypt = document.body.querySelector("#form-encrypt"),
elFormProofs = document.body.querySelector("#form-proofs"),
elProfileUid = document.body.querySelector("#profileUid"),
elProfileMode = document.body.querySelector("#profileMode"),
elUtilWKD = document.body.querySelector("#form-util-wkd");
if (elFormVerify) {
elFormVerify.onsubmit = function (evt) {
evt.preventDefault();
let opts = {
signature: null,
mode: null,
input: null,
server: null,
};
opts.signature = document.body.querySelector("#signature").value;
if (document.body.querySelector("#publicKey").value != "") {
opts.input = document.body.querySelector("#publicKey").value;
opts.mode = "plaintext";
} else if (document.body.querySelector("#wkd").value != "") {
opts.input = document.body.querySelector("#wkd").value;
opts.mode = "wkd";
} else if (document.body.querySelector("#hkp_input").value != "") {
opts.input = document.body.querySelector("#hkp_input").value;
opts.server = document.body.querySelector("#hkp_server").value;
opts.mode = "hkp";
} else {
opts.mode = "signature";
}
verifySignature(opts);
};
}
if (elFormEncrypt) {
elFormEncrypt.onsubmit = function (evt) {
evt.preventDefault();
let opts = {
message: null,
mode: null,
input: null,
server: null,
};
opts.message = document.body.querySelector("#message").value;
if (document.body.querySelector("#publicKey").value != "") {
opts.input = document.body.querySelector("#publicKey").value;
opts.mode = "plaintext";
} else if (document.body.querySelector("#wkd").value != "") {
opts.input = document.body.querySelector("#wkd").value;
opts.mode = "wkd";
} else if (document.body.querySelector("#hkp_input").value != "") {
opts.input = document.body.querySelector("#hkp_input").value;
opts.server = document.body.querySelector("#hkp_server").value;
opts.mode = "hkp";
} else {
opts.mode = "signature";
}
encryptMessage(opts);
};
}
if (elFormProofs) {
elFormProofs.onsubmit = function (evt) {
evt.preventDefault();
let opts = {
mode: null,
input: null,
server: null,
};
if (document.body.querySelector("#publicKey").value != "") {
opts.input = document.body.querySelector("#publicKey").value;
opts.mode = "plaintext";
} else if (document.body.querySelector("#wkd").value != "") {
opts.input = document.body.querySelector("#wkd").value;
opts.mode = "wkd";
} else if (document.body.querySelector("#hkp_input").value != "") {
opts.input = document.body.querySelector("#hkp_input").value;
opts.server = document.body.querySelector("#hkp_server").value;
opts.mode = "hkp";
} else {
opts.mode = null;
}
verifyProofs(opts);
};
}
if (elProfileUid) {
let match, opts, profileUid = elProfileUid.innerHTML;
switch (elProfileMode.innerHTML) {
default:
case "auto":
if (/.*@.*/.test(profileUid)) {
// Match email for wkd
opts = {
input: profileUid,
mode: "wkd"
}
} else {
// Match fingerprint for hkp
opts = {
input: profileUid,
mode: "hkp"
}
}
break;
case "hkp":
case "wkd":
opts = {
input: profileUid,
mode: elProfileMode.innerHTML
}
break;
}
displayProfile(opts);
}
if (elUtilWKD) {
elUtilWKD.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = document.body.querySelector("#input");
const elOutput = document.body.querySelector("#output");
elInput.addEventListener("input", async function(evt) {
if (evt.target.value) {
elOutput.value = await computeWKDLocalPart(evt.target.value);
} else {
elOutput.value = "";
}
});
}