diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac1f5a9..88e5eae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
+### Added
+- Support for signature profiles
+### Changed
+- Allow setting of custom HKP server
## [2.3.4] - 2021-01-02
### Fixed
diff --git a/package.json b/package.json
index 3bf5184..d2e9b06 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"main": "index.js",
"dependencies": {
"bent": "^7.3.12",
- "doipjs": "^0.8.4",
+ "doipjs": "^0.9.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-validator": "^6.8.0",
diff --git a/routes/profile.js b/routes/profile.js
index ab50bb3..855f9c2 100644
--- a/routes/profile.js
+++ b/routes/profile.js
@@ -29,20 +29,28 @@ more information on this, and how to apply and follow the GNU AGPL, see 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;
+ // Reset the avatar
+ document.body.querySelector('#profileAvatar').style = 'display: none';
+ document.body.querySelector('#profileAvatar').src = '/static/img/avatar_placeholder.png';
+
+ if (opts.mode == 'sig') {
+ try {
+ sigVerification = await doip.signatures.verify(opts.input);
+
+ if (sigVerification.errors.length > 0) {
+ throw(sigVerification.errors.join(', '))
+ }
+
+ keyData = sigVerification.publicKey
+ fingerprint = sigVerification.fingerprint
+
+ const sigData = await openpgp.cleartext.readArmored(opts.input);
+ const sigText = sigData.getText();
+ let sigKeys = [];
+ sigText.split('\n').forEach((line, i) => {
+ const match = line.match(/^(.*)\=(.*)$/i);
+ if (!match || !match[1]) {
+ return;
+ }
+ switch (match[1].toLowerCase()) {
+ case 'key':
+ sigKeys.push(match[2]);
+ break;
+
+ default:
+ break;
+ }
+ });
+
+ if (sigKeys.length === 0) {
+ throw('No key URI found');
+ }
+
+ sigKeyUri = sigKeys[0];
+ } catch (e) {
+ feedback += `There was a problem reading the signature.
`;
+ feedback += `${e}
`;
+ 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 += `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;
+ }
}
const userPrimary = await keyData.getPrimaryUser();
@@ -254,9 +306,17 @@ async function displayProfile(opts) {
let imgUri = null;
// Determine WKD or HKP link
- switch (opts.mode) {
+ let keyUriMode = opts.mode;
+ let keyUriId = opts.input;
+ if (opts.mode === 'sig') {
+ const keyUriMatch = sigKeyUri.match(/(.*):(.*)/);
+ keyUriMode = keyUriMatch[1];
+ keyUriId = keyUriMatch[2];
+ }
+
+ switch (keyUriMode) {
case "wkd":
- const [, localPart, domain] = /(.*)@(.*)/.exec(opts.input);
+ const [, localPart, domain] = /(.*)@(.*)/.exec(keyUriId);
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}`;
@@ -332,18 +392,22 @@ async function displayProfile(opts) {
feedback += ``;
feedback += ``;
feedback += `
`;
- feedback += `
`;
+ feedback += `
`;
feedback += `
`;
feedback += ``;
feedback += `
`;
- feedback += `
`;
+ feedback += `
`;
feedback += `
`;
// Display feedback
document.body.querySelector('#profileData').innerHTML = feedback;
try {
- verifications = await doip.claims.verify(keyData, fingerprint, {'proxyPolicy':'adaptive'})
+ if (sigVerification) {
+ verifications = sigVerification.claims
+ } else {
+ verifications = await doip.claims.verify(keyData, fingerprint, {'proxyPolicy':'adaptive'})
+ }
} catch (e) {
feedback += `There was a problem verifying the claims.
`;
feedback += `${e}
`;
@@ -357,25 +421,102 @@ async function displayProfile(opts) {
return;
}
- let primaryClaims;
-
feedback = "";
- if (userMail) {
- verifications.forEach((userId, i) => {
- if (!keyData.users[i].userId) {
- keyData.users[i].userId = {
- email: 'email not specified'
- };
- }
- if (keyData.users[i].userId.email !== userMail) {
+ if (opts.mode === 'sig') {
+ feedback += ``;
+ feedback += `
`;
+ feedback += `
proofs
`;
+ feedback += `
`;
+
+ verifications = verifications.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData))
+ verifications = verifications.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0));
+
+ verifications.forEach((claim, i) => {
+ const claimData = claim.serviceproviderData;
+ if (!claimData.serviceprovider.name) {
+ return;
+ }
+ feedback += ``;
+ feedback += `
${claimData.serviceprovider.name}
`;
+ feedback += `
`;
+ feedback += `
`;
+ });
+ } else {
+ let primaryClaims;
+
+ if (userMail) {
+ verifications.forEach((userId, i) => {
+ if (!keyData.users[i].userId) {
+ keyData.users[i].userId = {
+ email: 'email not specified'
+ }
+ }
+
+ if (keyData.users[i].userId.email != userMail) {
+ return;
+ }
+
+ feedback += ``;
+ feedback += `
`;
+ // feedback += `
`;
+ feedback += `
${keyData.users[i].userId.email} primary
`;
+ feedback += `
`;
+
+ if (userId.length == 0) {
+ feedback += ``;
+ feedback += `
`;
+ feedback += `
No claims associated
`;
+ feedback += `
`;
+ return;
+ }
+
+ userId = userId.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData))
+ userId = userId.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0));
+
+ primaryClaims = userId;
+
+ userId.forEach((claim, i) => {
+ const claimData = claim.serviceproviderData;
+ if (!claimData.serviceprovider.name) {
+ return;
+ }
+ feedback += ``;
+ feedback += `
${claimData.serviceprovider.name}
`;
+ feedback += `
`;
+ feedback += `
`;
+ });
+ });
+ }
+
+ verifications.forEach((userId, i) => {
+ if (userMail && keyData.users[i].userId.email == userMail) {
return;
}
feedback += ``;
feedback += `
`;
- // feedback += `
`;
- feedback += `
${keyData.users[i].userId.email} primary
`;
+ feedback += `
${keyData.users[i].userId.email}
`;
feedback += `
`;
if (userId.length === 0) {
@@ -389,7 +530,13 @@ async function displayProfile(opts) {
userId = userId.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData))
userId = userId.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0));
- primaryClaims = userId
+ if (primaryClaims && primaryClaims.toString() == userId.toString()) {
+ feedback += ``;
+ feedback += `
`;
+ feedback += `
Identical to primary
`;
+ feedback += `
`;
+ return;
+ }
userId.forEach((claim, i) => {
const claimData = claim.serviceproviderData;
@@ -414,57 +561,6 @@ async function displayProfile(opts) {
});
}
- verifications.forEach((userId, i) => {
- if (userMail && keyData.users[i].userId.email == userMail) {
- return;
- }
-
- feedback += ``;
- feedback += `
`;
- feedback += `
${keyData.users[i].userId.email}
`;
- feedback += `
`;
-
- if (userId.length == 0) {
- feedback += ``;
- feedback += `
`;
- feedback += `
No claims associated
`;
- feedback += `
`;
- return;
- }
-
- userId = userId.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData))
- userId = userId.sort((a,b) => (a.serviceproviderData.serviceprovider.name > b.serviceproviderData.serviceprovider.name) ? 1 : ((b.serviceproviderData.serviceprovider.name > a.serviceproviderData.serviceprovider.name) ? -1 : 0));
-
- if (primaryClaims && primaryClaims.toString() == userId.toString()) {
- feedback += ``;
- feedback += `
`;
- feedback += `
Identical to primary
`;
- feedback += `
`;
- return;
- }
-
- userId.forEach((claim, i) => {
- const claimData = claim.serviceproviderData;
- if (!claimData.serviceprovider.name) {
- return;
- }
- feedback += ``;
- feedback += `
${capitalizeLetteredServices(claimData.serviceprovider.name)}
`;
- feedback += `
`;
- feedback += `
`;
- });
- });
-
// Display feedback
document.body.querySelector('#profileProofs').innerHTML = feedback;
}
@@ -1024,8 +1120,10 @@ async function fetchWithTimeout(url, timeout = 3000) {
let elFormVerify = document.body.querySelector("#form-verify"),
elFormEncrypt = document.body.querySelector("#form-encrypt"),
elFormProofs = document.body.querySelector("#form-proofs"),
+ elFormSignatureProfile = document.body.querySelector("#form-generate-signature-profile"),
elProfileUid = document.body.querySelector("#profileUid"),
elProfileMode = document.body.querySelector("#profileMode"),
+ elProfileServer = document.body.querySelector("#profileServer"),
elModeSelect = document.body.querySelector("#modeSelect"),
elUtilWKD = document.body.querySelector("#form-util-wkd"),
elUtilQRFP = document.body.querySelector("#form-util-qrfp"),
@@ -1171,9 +1269,22 @@ if (elFormProofs) {
}
if (elProfileUid) {
- let match, opts, profileUid = elProfileUid.innerHTML;
+ let opts, profileUid = elProfileUid.innerHTML;
switch (elProfileMode.innerHTML) {
default:
+ case "sig":
+ elFormSignatureProfile.onsubmit = function (evt) {
+ evt.preventDefault();
+
+ opts = {
+ input: document.body.querySelector("#plaintext_input").value,
+ mode: elProfileMode.innerHTML
+ }
+
+ displayProfile(opts)
+ }
+ break;
+
case "auto":
if (/.*@.*/.test(profileUid)) {
// Match email for wkd
@@ -1191,6 +1302,13 @@ if (elProfileUid) {
break;
case "hkp":
+ opts = {
+ input: profileUid,
+ server: elProfileServer.innerHTML,
+ mode: elProfileMode.innerHTML
+ }
+ break;
+
case "wkd":
opts = {
input: profileUid,
@@ -1207,7 +1325,10 @@ if (elProfileUid) {
}
break;
}
- displayProfile(opts);
+
+ if (elProfileMode.innerHTML !== 'sig') {
+ displayProfile(opts);
+ }
}
if (elUtilWKD) {
@@ -1347,4 +1468,4 @@ function capitalizeLetteredServices(serviceName) {
return servName.toUpperCase();
}
return serviceName;
-}
\ No newline at end of file
+}
diff --git a/static/styles.css b/static/styles.css
index 6ee0047..2a9753d 100644
--- a/static/styles.css
+++ b/static/styles.css
@@ -369,6 +369,11 @@ a.proofQR:hover {
background-color: #6abb5a;
}
+#form-generate-signature-profile {
+ margin-bottom: 2em;
+ font-size: 0.9rem;
+}
+
#qrcode {
display: flex;
justify-content: center;
diff --git a/views/profile.pug b/views/profile.pug
index 4973923..a8546d2 100644
--- a/views/profile.pug
+++ b/views/profile.pug
@@ -9,12 +9,22 @@ head
main.container.container--profile
.content
span#profileUid(style='display: none;') #{uid}
+ span#profileServer(style='display: none;') #{server}
span#profileMode(style='display: none;') #{mode}
+ if (mode == 'sig')
+ #profileSigInput
+ form#form-generate-signature-profile(method='post')
+ p Please enter the raw profile signature below and press "Generate profile".
+ textarea#plaintext_input(name='plaintext_input')
+ input(type='submit', name='submit', value='Generate profile').bigBtn
#profileHeader
img#profileAvatar(src='/static/img/avatar_placeholder.png' alt='avatar' style='display: none')
p#profileName
#profileData
- p Loading keys & verifying proofs…
+ if (mode == 'sig')
+ p Waiting for input…
+ else
+ p Loading keys & verifying proofs…
footer
p
| Generated by
@@ -23,6 +33,6 @@ main.container.container--profile
a(href="https://codeberg.org/keyoxide/web/releases")= settings.keyoxide_version
| ).
-script(src='/static/openpgp.min.js')
-script(src='/static/doip.js')
+script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
+script(type='application/javascript' src='/static/doip.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
diff --git a/views/template.base.pug b/views/template.base.pug
index 24b8f48..80fcebe 100644
--- a/views/template.base.pug
+++ b/views/template.base.pug
@@ -36,6 +36,6 @@ main.container
a(href='https://fosstodon.org/@keyoxide') Mastodon
p © 2020 Keyoxide contributors
-script(src='/static/openpgp.min.js')
-script(src='/static/qrcode.min.js')
+script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
+script(type='application/javascript' src='/static/qrcode.min.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
diff --git a/yarn.lock b/yarn.lock
index a95342d..b077b4b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -703,10 +703,10 @@ doctypes@^1.1.0:
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
-doipjs@^0.8.4:
- version "0.8.4"
- resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.8.4.tgz#41046de8b9c69afd633d452e09f92a80930f2e0a"
- integrity sha512-eJdrwClJ5fU3D4oIl+cGMSLEaHJ2AJnF4KvlO095aq+ztJQWi5WYwnqM6j5ASj9pMUlA3OAyYKhIPdZSlUF5uA==
+doipjs@^0.9.0:
+ version "0.9.0"
+ resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.9.0.tgz#628af8316ea40904d8695ef372e9f0d0211c25d2"
+ integrity sha512-Tw9Ep9vyWNFx4cBmNNtkE/gLakBY32+A09WLosBpAdtvm573h0N/Jww/IL5cr0gu5947pbqxUCnwkQySRB3N1A==
dependencies:
bent "^7.3.12"
browserify "^17.0.0"