keyoxide-web/static/scripts.js

1024 lines
37 KiB
JavaScript
Raw Normal View History

2020-07-30 04:05:11 -06:00
/*
2021-01-11 06:58:47 -07:00
Copyright (C) 2021 Yarmo Mackenbach
2020-07-30 04:05:11 -06:00
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
2020-06-25 10:01:06 -06:00
async function verifySignature(opts) {
2020-06-26 05:06:32 -06:00
// Init
2020-06-25 10:01:06 -06:00
const elRes = document.body.querySelector("#result");
const elResContent = document.body.querySelector("#resultContent");
2020-06-26 05:06:32 -06:00
let keyData, feedback, signature, verified, valid;
2020-06-25 10:01:06 -06:00
2020-06-26 05:06:32 -06:00
// Reset feedback
2020-06-25 10:01:06 -06:00
elRes.innerHTML = "";
2020-07-01 18:44:16 -06:00
elRes.classList.remove('green');
elRes.classList.remove('red');
2020-06-25 10:01:06 -06:00
elResContent.innerHTML = "";
try {
2020-06-26 05:06:32 -06:00
// Get key data
keyData = await fetchKeys(opts);
2020-06-25 10:01:06 -06:00
2020-06-26 05:06:32 -06:00
// Handle missing signature
if (opts.signature == null) { throw("No signature was provided."); }
2020-06-25 10:01:06 -06:00
2020-06-26 05:06:32 -06:00
// Try two different methods of signature reading
2020-06-25 10:01:06 -06:00
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;
}
2020-06-26 05:06:32 -06:00
if (signature == null) { throw(readError) };
2020-06-25 10:01:06 -06:00
2020-06-26 05:06:32 -06:00
// Verify the signature
2020-06-25 10:01:06 -06:00
verified = await openpgp.verify({
message: signature,
publicKeys: keyData.publicKey
2020-06-25 10:01:06 -06:00
});
2020-06-26 05:09:18 -06:00
valid = verified.signatures[0].valid;
2020-06-25 10:01:06 -06:00
} catch (e) {
console.error(e);
elRes.innerHTML = e;
elRes.classList.remove('green');
elRes.classList.add('red');
return;
}
2020-06-26 05:06:32 -06:00
// Init feedback to empty string
2020-06-25 10:01:06 -06:00
feedback = '';
2020-06-26 05:06:32 -06:00
// If content was extracted from signature
if (keyData.sigContent) {
2020-06-25 10:01:06 -06:00
elResContent.innerHTML = "<strong>Signature content:</strong><br><span style=\"white-space: pre-line\">"+sigContent+"</span>";
}
2020-06-26 05:06:32 -06:00
// Provide different feedback depending on key input mode
if (opts.mode == "signature" && keyData.sigUserId) {
2020-06-25 10:01:06 -06:00
if (valid) {
feedback += "The message was signed by the userId extracted from the signature.<br>";
feedback += 'UserId: '+keyData.sigUserId+'<br>';
feedback += "Fingerprint: "+keyData.fingerprint+"<br>";
2020-06-25 10:01:06 -06:00
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.<br>";
2020-06-26 04:58:02 -06:00
feedback += 'UserId: '+keyData.sigUserId+'<br>';
2020-06-25 10:01:06 -06:00
elRes.classList.remove('green');
elRes.classList.add('red');
}
} else if (opts.mode == "signature" && keyData.sigKeyId) {
2020-06-25 10:01:06 -06:00
if (valid) {
feedback += "The message was signed by the keyId extracted from the signature.<br>";
feedback += 'KeyID: '+keyData.sigKeyId+'<br>';
feedback += "Fingerprint: "+keyData.fingerprint+"<br><br>";
2020-06-25 10:01:06 -06:00
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.<br>";
2020-06-26 04:58:02 -06:00
feedback += 'KeyID: '+keyData.sigKeyId+'<br>';
2020-06-25 10:01:06 -06:00
elRes.classList.remove('green');
elRes.classList.add('red');
}
} else {
if (valid) {
feedback += "The message was signed by the provided key ("+opts.mode+").<br>";
feedback += "Fingerprint: "+keyData.fingerprint+"<br>";
2020-06-25 10:01:06 -06:00
elRes.classList.remove('red');
elRes.classList.add('green');
} else {
feedback += "The message's signature COULD NOT BE verified using the provided key ("+opts.mode+").<br>";
elRes.classList.remove('green');
elRes.classList.add('red');
}
}
2020-06-26 05:06:32 -06:00
// Display feedback
2020-06-25 10:01:06 -06:00
elRes.innerHTML = feedback;
};
async function encryptMessage(opts) {
2020-06-26 05:06:32 -06:00
// Init
const elEnc = document.body.querySelector("#message");
2020-06-25 10:01:06 -06:00
const elRes = document.body.querySelector("#result");
const elBtn = document.body.querySelector("[name='submit']");
let keyData, feedback, message, encrypted;
2020-06-25 10:01:06 -06:00
2020-06-26 05:06:32 -06:00
// Reset feedback
2020-06-25 10:01:06 -06:00
elRes.innerHTML = "";
2020-07-01 18:44:16 -06:00
elRes.classList.remove('green');
elRes.classList.remove('red');
2020-06-25 10:01:06 -06:00
try {
2020-06-26 05:06:32 -06:00
// Get key data
keyData = await fetchKeys(opts);
2020-06-25 10:01:06 -06:00
2020-06-26 05:06:32 -06:00
// Handle missing message
2020-06-25 10:01:06 -06:00
if (opts.message == null) {
2020-06-26 05:06:32 -06:00
throw("No message was provided.");
2020-06-25 10:01:06 -06:00
}
2020-06-26 05:06:32 -06:00
// Encrypt the message
2020-06-25 10:01:06 -06:00
encrypted = await openpgp.encrypt({
message: openpgp.message.fromText(opts.message),
publicKeys: keyData.publicKey
2020-06-25 10:01:06 -06:00
});
} catch (e) {
console.error(e);
elRes.innerHTML = e;
elRes.classList.remove('green');
elRes.classList.add('red');
return;
}
2020-06-26 05:06:32 -06:00
// Display encrypted data
2020-06-25 10:01:06 -06:00
elEnc.value = encrypted.data;
elEnc.toggleAttribute("readonly");
elBtn.setAttribute("disabled", "true");
2020-06-25 10:01:06 -06:00
};
2021-03-01 07:01:34 -07:00
async function displayProfile(opts) {
/// UTILITY FUNCTIONS
// Sort claims by name and filter for errors
const sortClaims = (claims) => {
claims = claims.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData));
claims = claims.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0));
return claims;
2020-06-26 07:08:22 -06:00
}
2021-03-01 07:01:34 -07:00
// Find the primary claims
const getPrimaryClaims = (primaryUserId, userIds, verifications) => {
let primaryClaims = null;
userIds.forEach((userId, i) => {
if (!primaryClaims && userId.userId && userId.userId.email === primaryUserId.email) {
primaryClaims = sortClaims(verifications[i]);
}
});
return primaryClaims;
2020-08-14 16:48:48 -06:00
}
2021-03-01 07:01:34 -07:00
// Generate a HTML string for the profile header
const generateProfileHeaderHTML = (data) => {
if (!data) {
data = {
profileHide: true,
name: '',
fingerprint: '',
key: {
url: '',
mode: '',
server: '',
id: '',
},
avatarURL: '/static/img/avatar_placeholder.png',
}
2020-08-15 00:41:04 -06:00
}
2020-08-14 16:48:48 -06:00
2021-03-01 07:01:34 -07:00
// TODO Add support for custom HKP server
return `
<div class="profile__avatar" style="${data.profileHide ? "display='none" : ''}">
<a class="avatar" href="#">
<img id="profileAvatar" src="${data.avatarURL}" alt="avatar">
</a>
</div>
<div class="profile__description" style="${data.profileHide ? "display='none" : ''}">
<p id="profileName">${data.name}</p>
<p id="profileURLFingerprint">
<a href="${data.key.url}">${data.fingerprint}</a>
</p>
<div class="buttons">
<button onClick="document.querySelector('#dialog--encryptMessage').showModal();">Encrypt message</button>
<button onClick="document.querySelector('#dialog--verifySignature').showModal();">Verify signature</button>
2021-03-01 07:01:34 -07:00
</div>
</div>
`;
2020-06-26 07:08:22 -06:00
}
2021-03-01 07:01:34 -07:00
// Generate a HTML string for each userId and associated claims
2021-03-30 08:22:03 -06:00
const generateProfileUserIdHTML = (userId, claims, dialogIds, opts) => {
2021-03-01 07:01:34 -07:00
// Init output
let output = '';
2021-03-03 05:23:33 -07:00
2021-03-01 07:01:34 -07:00
// Add claim header to output
output += `<h2>${userId.email}${opts.isPrimary ? ' <small class="primary">primary</small>' : ''}</h2>`;
// Handle claims identical to primary
if ('isIdenticalToPrimary' in opts && opts.isIdenticalToPrimary) {
output += `
<div class="claim">
<div class="claim__main">
<div class="claim__description">
<p>Identical to primary claims</p>
</div>
</div>
</div>
`;
return output;
}
2020-06-26 07:08:22 -06:00
2021-03-01 07:01:34 -07:00
// Handle "no claims"
if (claims.length == 0) {
output += `
<div class="claim">
<div class="claim__main">
<div class="claim__description">
<p>No claims associated</p>
</div>
</div>
</div>
`;
return output;
2020-06-27 06:59:48 -06:00
}
2020-06-26 07:08:22 -06:00
2021-03-01 07:01:34 -07:00
claims = sortClaims(claims);
2020-06-26 09:52:01 -06:00
2021-03-01 07:01:34 -07:00
// Generate output for each claim
claims.forEach((claim, i) => {
2021-03-30 08:22:03 -06:00
const claimData = claim.serviceproviderData;
2021-03-01 07:01:34 -07:00
if (!claimData.serviceprovider.name) {
return;
}
2021-03-03 05:23:33 -07:00
2021-03-01 07:01:34 -07:00
output += `
<div class="claim">
<div class="claim__main">
<div class="claim__description">
<p>${claimData.profile.display}</p>
</div>
<div class="claim__links">
<p>
<span>${capitalizeLetteredServices(claimData.serviceprovider.name)}</span>
<a href="${claimData.profile.uri}">View&nbsp;account</a>
<a href="${claimData.proof.uri}">View&nbsp;proof</a>
2021-03-31 07:11:12 -06:00
`;
if (claimData.profile.qr) {
output += `
<a href="/util/qr/${encodeURIComponent(claimData.profile.qr)}">View&nbsp;QR</a>
`;
}
output += `
2021-03-30 08:22:03 -06:00
<button onClick="document.querySelector('#dialog--${dialogIds[i]}').showModal();">Details</button>
2021-03-01 07:01:34 -07:00
</p>
</div>
</div>
<div class="claim__verification claim__verification--${claim.isVerified ? "true" : "false"}">${claim.isVerified ? "✔" : "✕"}</div>
</div>
`;
2021-03-30 08:22:03 -06:00
});
return output;
}
// Generate a HTML string for each userId and associated claims
const generateClaimDialogHTML = (claims, dialogIds) => {
// Generate dialog for each claim
let output = '';
claims.forEach((claim, i) => {
const claimData = claim.serviceproviderData;
output += `
<dialog id="dialog--${dialogIds[i]}">
<div>
<p>
The claim's service provider is <strong>${claimData.serviceprovider.name}</strong>.
</p>
<p>
The claim points to: <a href="${claimData.profile.uri}">${claimData.profile.uri}</a>.
</p>
<p>
The supposed proof is located at: <a href="${claimData.proof.uri}">${claimData.proof.uri}</a>.
</p>
`;
if (claimData.proof.fetch) {
output += `
<p class="warning">
Due to technical restraints, the machine had to fetch the proof from: <a href="${claimData.proof.fetch}">${claimData.proof.fetch}</a>. This link may not work for you.
</p>
`;
}
if (claimData.customRequestHandler) {
output += `
<p class="warning">
This claim's verification process was more complex than most verifications. The link(s) above may offer only limited insight into the verification process.
</p>
`;
}
output += `
<p>
The claim <strong>${claim.isVerified ? 'has been' : 'could not be'} verified</strong> by this proof.
</p>
`;
if (claim.errors.length > 0) {
output += `
<p class="warning">
The verification encountered errors: ${JSON.stringify(claim.errors)}.
</p>
`;
}
output += `
<form method="dialog">
<input type="submit" value="Close" />
</form>
</div>
</dialog>
`;
});
2021-03-01 07:01:34 -07:00
return output;
2020-06-26 07:08:22 -06:00
}
2020-06-26 09:52:01 -06:00
2021-03-01 07:01:34 -07:00
/// MAIN
// Init variables
2021-01-07 08:44:33 -07:00
let keyData, keyLink, sigVerification, sigKeyUri, fingerprint, feedback = "", verifications = [];
2020-07-23 02:36:42 -06:00
2021-03-02 07:26:13 -07:00
const doipOpts = {
proxyPolicy: 'adaptive',
}
2021-01-07 08:44:33 -07:00
// Reset the avatar
2021-03-01 07:01:34 -07:00
document.body.querySelector('#profileHeader').src = generateProfileHeaderHTML(null)
2021-01-07 08:44:33 -07:00
if (opts.mode == 'sig') {
try {
2021-03-02 07:26:13 -07:00
sigVerification = await doip.signatures.verify(opts.input, doipOpts);
2021-01-09 08:28:42 -07:00
keyData = sigVerification.publicKey.data;
fingerprint = sigVerification.publicKey.fingerprint;
2021-01-07 08:44:33 -07:00
} catch (e) {
feedback += `<p>There was a problem reading the signature.</p>`;
if ('errors' in e) {
feedback += `<code>${e.errors.join(', ')}</code>`;
} else {
feedback += `<code>${e}</code>`;
}
2021-01-07 08:44:33 -07:00
document.body.querySelector('#profileData').innerHTML = feedback;
document.body.querySelector('#profileName').innerHTML = "Could not load profile";
return;
}
} else {
try {
let keyURI;
if (opts.mode === 'hkp' && opts.server) {
keyURI = `${opts.mode}:${opts.server}:${opts.input}`
} else {
keyURI = `${opts.mode}:${opts.input}`
}
keyData = await doip.keys.fetch.uri(keyURI);
fingerprint = keyData.keyPacket.getFingerprint();
} catch (e) {
feedback += `<p>There was a problem fetching the keys.</p>`;
feedback += `<code>${e}</code>`;
document.body.querySelector('#profileData').innerHTML = feedback;
document.body.querySelector('#profileName').innerHTML = "Could not load profile";
return;
2021-01-05 05:20:06 -07:00
}
2020-06-29 03:18:06 -06:00
}
2020-08-14 16:48:48 -06:00
2021-03-01 07:01:34 -07:00
// Get data of primary userId
2020-12-11 04:06:21 -07:00
const userPrimary = await keyData.getPrimaryUser();
const userData = userPrimary.user.userId;
const userName = userData.name ? userData.name : userData.email;
const userMail = userData.email ? userData.email : null;
2020-06-26 09:52:01 -06:00
2021-03-01 07:01:34 -07:00
// TODO Get image from user attribute
2020-08-20 07:24:29 -06:00
let imgUri = null;
2020-07-03 04:06:55 -06:00
// Determine WKD or HKP link
2021-01-07 08:44:33 -07:00
let keyUriMode = opts.mode;
2021-01-09 08:28:42 -07:00
let keyUriServer = null;
2021-01-07 08:44:33 -07:00
let keyUriId = opts.input;
if (opts.mode === 'sig') {
2021-01-09 08:28:42 -07:00
const keyUriMatch = sigVerification.publicKey.uri.match(/([^:]*)(?:\:(.*))?:(.*)/);
2021-01-07 08:44:33 -07:00
keyUriMode = keyUriMatch[1];
2021-01-09 08:28:42 -07:00
keyUriServer = keyUriMatch[2];
keyUriId = keyUriMatch[3];
2021-01-07 08:44:33 -07:00
}
2021-03-02 07:26:13 -07:00
2021-01-07 08:44:33 -07:00
switch (keyUriMode) {
2020-07-05 07:04:12 -06:00
case "wkd":
2021-01-07 08:44:33 -07:00
const [, localPart, domain] = /(.*)@(.*)/.exec(keyUriId);
2020-07-05 07:04:12 -06:00
const localEncoded = await computeWKDLocalPart(localPart.toLowerCase());
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`;
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`;
2020-07-03 04:06:55 -06:00
2020-07-03 04:14:50 -06:00
try {
2020-07-05 07:04:12 -06:00
keyLink = await fetch(urlAdvanced).then(function(response) {
2020-07-03 04:06:55 -06:00
if (response.status === 200) {
2020-07-05 07:04:12 -06:00
return urlAdvanced;
2020-07-03 04:06:55 -06:00
}
});
2020-07-03 04:15:37 -06:00
} catch (e) {
2020-07-03 04:16:24 -06:00
console.warn(e);
2020-07-03 04:06:55 -06:00
}
2020-07-05 07:04:12 -06:00
if (!keyLink) {
try {
keyLink = await fetch(urlDirect).then(function(response) {
if (response.status === 200) {
return urlDirect;
}
});
} catch (e) {
console.warn(e);
}
}
if (!keyLink) {
2021-01-09 08:28:42 -07:00
keyLink = `https://${keyUriServer ? keyUriServer : 'keys.openpgp.org'}/pks/lookup?op=get&options=mr&search=0x${fingerprint}`;
2020-07-05 07:04:12 -06:00
}
break;
case "hkp":
2021-01-09 08:28:42 -07:00
keyLink = `https://${keyUriServer ? keyUriServer : 'keys.openpgp.org'}/pks/lookup?op=get&options=mr&search=0x${fingerprint}`;
2020-07-05 07:04:12 -06:00
break;
case "keybase":
keyLink = opts.keyLink;
break;
2020-07-03 04:06:55 -06:00
}
2021-03-01 07:01:34 -07:00
// Generate profile header
2020-07-23 02:44:07 -06:00
const profileHash = openpgp.util.str_to_hex(openpgp.util.Uint8Array_to_str(await openpgp.crypto.hash.md5(openpgp.util.str_to_Uint8Array(userData.email))));
2021-03-01 07:01:34 -07:00
document.body.querySelector('#profileHeader').innerHTML = generateProfileHeaderHTML({
profileHide: false,
name: userName,
fingerprint: fingerprint,
key: {
url: keyLink,
mode: keyUriMode,
server: keyUriServer,
id: keyUriId,
},
avatarURL: imgUri ? imgUri : `https://www.gravatar.com/avatar/${profileHash}?s=128&d=mm`
})
2020-07-30 15:51:47 -06:00
document.title = `${userName} - Keyoxide`;
2020-06-26 09:52:01 -06:00
2020-12-11 04:06:21 -07:00
try {
2021-01-07 08:44:33 -07:00
if (sigVerification) {
verifications = sigVerification.claims
} else {
2021-03-02 07:26:13 -07:00
verifications = await doip.claims.verify(keyData, fingerprint, doipOpts)
2021-01-07 08:44:33 -07:00
}
2020-12-11 04:06:21 -07:00
} catch (e) {
feedback += `<p>There was a problem verifying the claims.</p>`;
feedback += `<code>${e}</code>`;
document.body.querySelector('#profileData').innerHTML = feedback;
document.body.querySelector('#profileName').innerHTML = "Could not load profile";
return;
}
// Exit if no notations are available
2020-12-11 04:06:21 -07:00
if (verifications.length == 0) {
return;
}
2021-03-30 08:22:03 -06:00
let feedbackDialog = "";
let dialogIds = null;
2020-12-11 04:06:21 -07:00
feedback = "";
2021-01-07 08:44:33 -07:00
if (opts.mode === 'sig') {
2021-03-01 07:01:34 -07:00
const claims = sortClaims(verifications);
2021-03-30 08:22:03 -06:00
dialogIds = new Uint32Array(claims.length);
window.crypto.getRandomValues(dialogIds);
feedback += generateProfileUserIdHTML(userData, claims, dialogIds, {isPrimary: false});
feedbackDialog += generateClaimDialogHTML(claims, dialogIds);
2021-01-07 08:44:33 -07:00
} else {
2021-03-01 07:01:34 -07:00
const primaryClaims = getPrimaryClaims(userData, keyData.users, verifications);
2021-03-30 08:22:03 -06:00
dialogIds = new Uint32Array(primaryClaims.length);
window.crypto.getRandomValues(dialogIds);
feedback += generateProfileUserIdHTML(userData, primaryClaims, dialogIds, {isPrimary: true});
feedbackDialog += generateClaimDialogHTML(primaryClaims, dialogIds);
2021-01-07 08:44:33 -07:00
2021-03-01 07:01:34 -07:00
keyData.users.forEach((user, i) => {
if (!user.userId || userData.email && user.userId && user.userId.email === userData.email) {
2020-12-11 04:06:21 -07:00
return;
}
2021-03-01 07:01:34 -07:00
const claims = sortClaims(verifications[i])
const opts = {
isPrimary: false,
isIdenticaltoPrimary: primaryClaims && primaryClaims.toString() === claims.toString()
2021-01-07 08:44:33 -07:00
}
2021-03-30 08:22:03 -06:00
dialogIds = new Uint32Array(claims.length);
window.crypto.getRandomValues(dialogIds);
feedback += generateProfileUserIdHTML(user.userId, claims, dialogIds, opts);
feedbackDialog += generateClaimDialogHTML(claims, dialogIds);
2021-03-01 07:01:34 -07:00
})
}
2021-03-01 07:01:34 -07:00
feedback += `
2021-03-30 08:22:03 -06:00
<dialog id="dialog--whatisthis">
<div>
<p>
Keyoxide allows anyone to prove that they have accounts on certain websites and, by doing so, establish an online identity. To guarantee the validity of these identity verifications and prevent impersonation, Keyoxide uses secure and well-known encryption paradigms. All claims are verified using bidirectional linking.
</p>
<p>
You are currently viewing someone's Keyoxide profile, including the verification results of their identity claims.
</p>
<p>
More detailed information is available on the <a href="/">What is Keyoxide</a> page.
</p>
<form method="dialog">
<input type="submit" value="Close" />
</form>
</div>
</dialog>
<dialog id="dialog--localverification">
<div>
<p>
The profile page you are currently viewing depends at least partially on a server you may not know, operated by someone you may not have reason to trust.
</p>
<p>
You can choose to perform the identity verification again, but this time completely locally, removing the need to trust unknown servers.
</p>
<p>
On linux/mac/windows, run:
</p>
<pre><code>keyoxide verify ${opts.mode}:${opts.input}</code></pre>
<form method="dialog">
<input type="submit" value="Close" />
</form>
</div>
</dialog>
2021-03-01 07:01:34 -07:00
<p class="subtle-links">
2021-03-30 08:22:03 -06:00
<button onclick="document.querySelector('#dialog--whatisthis').showModal()">What is this?</button>
<button onclick="document.querySelector('#dialog--localverification').showModal()">Perform local verification</button>
2021-03-01 07:01:34 -07:00
</p>`;
// Display feedback
document.body.querySelector('#profileProofs').innerHTML = feedback;
2021-03-30 08:22:03 -06:00
if (feedbackDialog) {
document.body.querySelector('#profileDialogs').innerHTML = feedbackDialog;
}
2020-06-26 07:08:22 -06:00
2021-03-30 08:22:03 -06:00
// Register modals
document.querySelectorAll('dialog').forEach(function(d) {
dialogPolyfill.registerDialog(d);
d.addEventListener('click', function(ev) {
if (ev && ev.target != d) {
return;
}
d.close();
});
});
// Register form listeners
const elFormEncrypt = document.body.querySelector("#dialog--encryptMessage form");
elFormEncrypt.onsubmit = async function (evt) {
evt.preventDefault();
try {
// Encrypt the message
encrypted = await openpgp.encrypt({
message: openpgp.message.fromText(elFormEncrypt.querySelector('.input').value),
publicKeys: keyData
});
elFormEncrypt.querySelector('.output').value = encrypted.data;
} catch (e) {
console.error(e);
elFormEncrypt.querySelector('.output').value = `Could not encrypt message!\n==========================\n${e.message ? e.message : e}`;
}
};
const elFormVerify = document.body.querySelector("#dialog--verifySignature form");
elFormVerify.onsubmit = async function (evt) {
evt.preventDefault();
try {
// Try two different methods of signature reading
let signature = null, verified = null, readError = null;
try {
signature = await openpgp.message.readArmored(elFormVerify.querySelector('.input').value);
} catch(e) {
readError = e;
}
try {
signature = await openpgp.cleartext.readArmored(elFormVerify.querySelector('.input').value);
} catch(e) {
readError = e;
}
if (signature == null) { throw(readError) };
// Verify the signature
verified = await openpgp.verify({
message: signature,
publicKeys: keyData
});
if (verified.signatures[0].valid) {
elFormVerify.querySelector('.output').value = `The message was signed by the profile's key.`;
} else {
elFormVerify.querySelector('.output').value = `The message was NOT signed by the profile's key.`;
}
} catch (e) {
console.error(e);
elFormVerify.querySelector('.output').value = `Could not verify signature!\n===========================\n${e.message ? e.message : e}`;
}
};
2021-03-30 08:22:03 -06:00
}
2020-06-26 07:08:22 -06:00
async function fetchKeys(opts) {
2020-06-26 05:08:04 -06:00
// Init
let lookupOpts, wkd, hkd, sig, lastPrimarySig;
let output = {
publicKey: null,
user: null,
notations: null,
sigKeyId: null,
sigUserId: null,
sigContent: null
};
// Autodetect mode
if (opts.mode == "auto") {
if (/.*@.*\..*/.test(opts.input)) {
opts.mode = "wkd";
} else {
opts.mode = "hkp";
}
}
2020-06-26 05:08:04 -06:00
// 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;
2020-07-05 07:04:12 -06:00
case "keybase":
opts.keyLink = `https://keybase.io/${opts.username}/pgp_keys.asc?fingerprint=${opts.fingerprint}`;
opts.input = `${opts.username}/${opts.fingerprint}`;
try {
opts.plaintext = await fetch(opts.keyLink).then(function(response) {
if (response.status === 200) {
return response;
}
})
.then(response => response.text());
} catch (e) {
2020-07-05 09:25:11 -06:00
throw(`Error: No public keys could be fetched from the Keybase account (${e}).`);
2020-07-05 07:04:12 -06:00
}
output.publicKey = (await openpgp.key.readArmored(opts.plaintext)).keys[0];
if (!output.publicKey) {
throw("Error: No public keys could be read from the Keybase account.");
}
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;
}
2020-06-26 05:08:04 -06:00
// 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;
}
2020-06-29 12:58:34 -06:00
async function computeWKDLocalPart(message) {
2020-07-23 02:53:13 -06:00
const data = openpgp.util.str_to_Uint8Array(message);
const hash = await openpgp.crypto.hash.sha1(data);
return openpgp.util.encodeZBase32(hash);
2020-06-29 12:58:34 -06:00
}
2020-07-05 05:51:23 -06:00
async function generateProfileURL(data) {
2020-08-14 07:16:41 -06:00
let hostname = window.location.hostname;
2020-07-05 05:51:23 -06:00
if (data.input == "") {
return "Waiting for input...";
}
switch (data.source) {
case "wkd":
2020-08-14 07:16:41 -06:00
return `https://${hostname}/${data.input}`;
2020-07-05 05:51:23 -06:00
break;
case "hkp":
if (/.*@.*\..*/.test(data.input)) {
2020-08-14 07:16:41 -06:00
return `https://${hostname}/hkp/${data.input}`;
2020-07-05 05:51:23 -06:00
} else {
2020-08-14 07:16:41 -06:00
return `https://${hostname}/${data.input}`;
2020-07-05 05:51:23 -06:00
}
break;
case "keybase":
const re = /https\:\/\/keybase.io\/(.*)\/pgp_keys\.asc\?fingerprint\=(.*)/;
if (!re.test(data.input)) {
return "Incorrect Keybase public key URL.";
}
const match = data.input.match(re);
2020-08-14 07:16:41 -06:00
return `https://${hostname}/keybase/${match[1]}/${match[2]}`;
2020-07-05 05:51:23 -06:00
break;
}
}
2020-08-14 17:03:45 -06:00
async function fetchWithTimeout(url, timeout = 3000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), timeout)
)
]);
}
// General purpose
let elFormSignatureProfile = document.body.querySelector("#formGenerateSignatureProfile"),
2020-06-28 16:09:31 -06:00
elProfileUid = document.body.querySelector("#profileUid"),
2020-06-29 12:58:34 -06:00
elProfileMode = document.body.querySelector("#profileMode"),
2021-01-05 05:20:06 -07:00
elProfileServer = document.body.querySelector("#profileServer"),
elModeSelect = document.body.querySelector("#modeSelect"),
2020-07-02 14:39:56 -06:00
elUtilWKD = document.body.querySelector("#form-util-wkd"),
2020-07-22 06:58:11 -06:00
elUtilQRFP = document.body.querySelector("#form-util-qrfp"),
2020-07-05 05:51:23 -06:00
elUtilQR = document.body.querySelector("#form-util-qr"),
elUtilProfileURL = document.body.querySelector("#form-util-profile-url");
2020-06-25 10:01:06 -06:00
if (elModeSelect) {
elModeSelect.onchange = function (evt) {
let elAllModes = document.body.querySelectorAll('.modes');
elAllModes.forEach(function(el) {
el.classList.remove('modes--visible');
});
document.body.querySelector(`.modes--${elModeSelect.value}`).classList.add('modes--visible');
}
elModeSelect.dispatchEvent(new Event("change"));
2020-06-26 07:08:22 -06:00
}
2020-06-26 09:52:01 -06:00
if (elProfileUid) {
2021-01-05 05:20:06 -07:00
let opts, profileUid = elProfileUid.innerHTML;
2020-06-28 16:09:31 -06:00
switch (elProfileMode.innerHTML) {
default:
2021-01-07 08:44:33 -07:00
case "sig":
elFormSignatureProfile.onsubmit = function (evt) {
evt.preventDefault();
opts = {
input: document.body.querySelector("#plaintext_input").value,
mode: elProfileMode.innerHTML
}
displayProfile(opts)
}
break;
2020-06-28 16:09:31 -06:00
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;
2020-06-28 16:10:13 -06:00
case "hkp":
2021-01-05 05:20:06 -07:00
opts = {
input: profileUid,
server: elProfileServer.innerHTML,
mode: elProfileMode.innerHTML
}
break;
2020-06-28 16:10:13 -06:00
case "wkd":
2020-06-28 16:09:31 -06:00
opts = {
input: profileUid,
mode: elProfileMode.innerHTML
}
2020-07-05 07:04:12 -06:00
break;
case "keybase":
let match = profileUid.match(/(.*)\/(.*)/);
opts = {
username: match[1],
fingerprint: match[2],
mode: elProfileMode.innerHTML
}
2020-06-28 16:09:31 -06:00
break;
2020-06-26 09:52:01 -06:00
}
2021-01-07 08:44:33 -07:00
if (elProfileMode.innerHTML !== 'sig') {
displayProfile(opts);
}
2020-06-26 09:52:01 -06:00
}
2020-06-29 12:58:34 -06:00
if (elUtilWKD) {
elUtilWKD.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = document.body.querySelector("#input");
const elOutput = document.body.querySelector("#output");
2020-07-23 03:07:49 -06:00
const elOutputDirect = document.body.querySelector("#output_url_direct");
const elOutputAdvanced = document.body.querySelector("#output_url_advanced");
let match;
2020-06-29 12:58:34 -06:00
elInput.addEventListener("input", async function(evt) {
if (evt.target.value) {
2020-07-23 03:07:49 -06:00
if (/(.*)@(.{1,}\..{1,})/.test(evt.target.value)) {
match = evt.target.value.match(/(.*)@(.*)/);
elOutput.innerText = await computeWKDLocalPart(match[1]);
elOutputDirect.innerText = `https://${match[2]}/.well-known/openpgpkey/hu/${elOutput.innerText}?l=${match[1]}`;
elOutputAdvanced.innerText = `https://openpgpkey.${match[2]}/.well-known/openpgpkey/${match[2]}/hu/${elOutput.innerText}?l=${match[1]}`;
} else {
elOutput.innerText = await computeWKDLocalPart(evt.target.value);
elOutputDirect.innerText = "Waiting for input";
elOutputAdvanced.innerText = "Waiting for input";
}
2020-06-29 12:58:34 -06:00
} else {
2020-07-23 03:07:49 -06:00
elOutput.innerText = "Waiting for input";
elOutputDirect.innerText = "Waiting for input";
elOutputAdvanced.innerText = "Waiting for input";
2020-06-29 12:58:34 -06:00
}
});
2020-07-23 03:07:49 -06:00
elInput.dispatchEvent(new Event("input"));
2020-06-29 12:58:34 -06:00
}
2020-07-02 14:39:56 -06:00
2020-07-22 06:58:11 -06:00
if (elUtilQRFP) {
elUtilQRFP.onsubmit = function (evt) {
evt.preventDefault();
}
2020-08-30 04:21:45 -06:00
const qrTarget = document.getElementById('qrcode');
const qrContext = qrTarget.getContext('2d');
const qrOpts = {
errorCorrectionLevel: 'H',
margin: 1,
2020-07-22 06:58:11 -06:00
width: 256,
2020-08-30 04:21:45 -06:00
height: 256
};
2020-07-22 06:58:11 -06:00
const elInput = document.body.querySelector("#input");
elInput.addEventListener("input", async function(evt) {
if (evt.target.value) {
2020-08-30 04:21:45 -06:00
QRCode.toCanvas(qrTarget, evt.target.value, qrOpts, function (error) {
if (error) {
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
console.error(error);
}
});
2020-07-22 06:58:11 -06:00
} else {
2020-08-30 04:21:45 -06:00
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
2020-07-22 06:58:11 -06:00
}
});
elInput.dispatchEvent(new Event("input"));
}
2020-07-02 14:39:56 -06:00
if (elUtilQR) {
elUtilQR.onsubmit = function (evt) {
evt.preventDefault();
}
2020-08-30 04:21:45 -06:00
const qrTarget = document.getElementById('qrcode');
const qrContext = qrTarget.getContext('2d');
const qrOpts = {
errorCorrectionLevel: 'L',
margin: 1,
2020-07-02 14:39:56 -06:00
width: 256,
2020-08-30 04:21:45 -06:00
height: 256
};
2020-07-02 14:39:56 -06:00
const elInput = document.body.querySelector("#input");
2020-07-23 02:36:42 -06:00
if (elInput.innerText) {
elInput.innerText = decodeURIComponent(elInput.innerText);
2020-08-30 04:21:45 -06:00
QRCode.toCanvas(qrTarget, elInput.innerText, qrOpts, function (error) {
if (error) {
document.body.querySelector("#qrcode--altLink").href = "#";
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
console.error(error);
} else {
document.body.querySelector("#qrcode--altLink").href = elInput.innerText;
}
});
2020-07-22 07:37:38 -06:00
} else {
2020-08-30 04:21:45 -06:00
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
2020-07-22 07:37:38 -06:00
}
2020-07-02 14:39:56 -06:00
}
2020-07-05 05:51:23 -06:00
if (elUtilProfileURL) {
elUtilProfileURL.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = document.body.querySelector("#input"),
elSource = document.body.querySelector("#source"),
elOutput = document.body.querySelector("#output");
let data = {
input: elInput.value,
source: elSource.value
};
elInput.addEventListener("input", async function(evt) {
data = {
input: elInput.value,
source: elSource.value
};
elOutput.innerText = await generateProfileURL(data);
});
elSource.addEventListener("input", async function(evt) {
data = {
input: elInput.value,
source: elSource.value
};
elOutput.innerText = await generateProfileURL(data);
});
elInput.dispatchEvent(new Event("input"));
}
function capitalizeLetteredServices(serviceName) {
2021-03-01 07:01:34 -07:00
const servName = serviceName.toLowerCase();
if (servName === 'dns' || servName === 'xmpp' || servName === 'irc') {
return servName.toUpperCase();
}
return serviceName;
}