Merge branch 'dev' into js-and-css-impromement

This commit is contained in:
KiddyTheKid 2021-01-07 15:30:21 -05:00
commit e8686f10e5
8 changed files with 245 additions and 97 deletions

View file

@ -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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- Support for signature profiles
### Changed
- Allow setting of custom HKP server
## [2.3.4] - 2021-01-02 ## [2.3.4] - 2021-01-02
### Fixed ### Fixed

View file

@ -5,7 +5,7 @@
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"bent": "^7.3.12", "bent": "^7.3.12",
"doipjs": "^0.8.4", "doipjs": "^0.9.0",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-validator": "^6.8.0", "express-validator": "^6.8.0",

View file

@ -29,20 +29,28 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/ */
const router = require('express').Router(); const router = require('express').Router();
router.get('/sig', function(req, res) {
res.render('profile', { mode: 'sig' })
});
router.get('/wkd/:input', function(req, res) { router.get('/wkd/:input', function(req, res) {
res.render('profile', { mode: "wkd", uid: req.params.input }) res.render('profile', { mode: 'wkd', uid: req.params.input })
}); });
router.get('/hkp/:input', function(req, res) { router.get('/hkp/:input', function(req, res) {
res.render('profile', { mode: "hkp", uid: req.params.input }) res.render('profile', { mode: 'hkp', uid: req.params.input })
});
router.get('/hkp/:server/:input', function(req, res) {
res.render('profile', { mode: 'hkp', uid: req.params.input, server: req.params.server })
}); });
router.get('/keybase/:username/:fingerprint', function(req, res) { router.get('/keybase/:username/:fingerprint', function(req, res) {
res.render('profile', { mode: "keybase", uid: `${req.params.username}/${req.params.fingerprint}` }) res.render('profile', { mode: 'keybase', uid: `${req.params.username}/${req.params.fingerprint}` })
}); });
router.get('/:input', function(req, res) { router.get('/:input', function(req, res) {
res.render('profile', { mode: "auto", uid: req.params.input }) res.render('profile', { mode: 'auto', uid: req.params.input })
}); });
module.exports = router; module.exports = router;

View file

@ -231,13 +231,64 @@ async function verifyProofs(opts) {
} }
async function displayProfile(opts) { async function displayProfile(opts) {
let keyData, keyLink, fingerprint, feedback = "", notation, isVerified, verifications = []; let keyData, keyLink, sigVerification, sigKeyUri, fingerprint, feedback = "", verifications = [];
let icon_qr = '<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="#ffffff" d="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z" /></svg>'; let icon_qr = '<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="#ffffff" d="M3,11H5V13H3V11M11,5H13V9H11V5M9,11H13V15H11V13H9V11M15,11H17V13H19V11H21V13H19V15H21V19H19V21H17V19H13V21H11V17H15V15H17V13H15V11M19,19V15H17V19H19M15,3H21V9H15V3M17,5V7H19V5H17M3,3H9V9H3V3M5,5V7H7V5H5M3,15H9V21H3V15M5,17V19H7V17H5Z" /></svg>';
// 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 { try {
keyData = await doip.keys.fetch.uri(`${opts.mode}:${opts.input}`); 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 += `<p>There was a problem reading the signature.</p>`;
feedback += `<code>${e}</code>`;
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(); fingerprint = keyData.keyPacket.getFingerprint();
// keyData = await fetchKeys(opts);
} catch (e) { } catch (e) {
feedback += `<p>There was a problem fetching the keys.</p>`; feedback += `<p>There was a problem fetching the keys.</p>`;
feedback += `<code>${e}</code>`; feedback += `<code>${e}</code>`;
@ -245,6 +296,7 @@ async function displayProfile(opts) {
document.body.querySelector('#profileName').innerHTML = "Could not load profile"; document.body.querySelector('#profileName').innerHTML = "Could not load profile";
return; return;
} }
}
const userPrimary = await keyData.getPrimaryUser(); const userPrimary = await keyData.getPrimaryUser();
const userData = userPrimary.user.userId; const userData = userPrimary.user.userId;
@ -254,9 +306,17 @@ async function displayProfile(opts) {
let imgUri = null; let imgUri = null;
// Determine WKD or HKP link // 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": case "wkd":
const [, localPart, domain] = /(.*)@(.*)/.exec(opts.input); const [, localPart, domain] = /(.*)@(.*)/.exec(keyUriId);
const localEncoded = await computeWKDLocalPart(localPart.toLowerCase()); const localEncoded = await computeWKDLocalPart(localPart.toLowerCase());
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`; const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`;
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`; const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`;
@ -332,18 +392,22 @@ async function displayProfile(opts) {
feedback += `</div>`; feedback += `</div>`;
feedback += `<div class="profileDataItem profileDataItem--noLabel">`; feedback += `<div class="profileDataItem profileDataItem--noLabel">`;
feedback += `<div class="profileDataItem__label"></div>`; feedback += `<div class="profileDataItem__label"></div>`;
feedback += `<div class="profileDataItem__value"><a href="/verify/${opts.mode}/${opts.input}">Verify signature</a></div>`; feedback += `<div class="profileDataItem__value"><a href="/verify/${keyUriMode}/${keyUriId}">Verify signature</a></div>`;
feedback += `</div>`; feedback += `</div>`;
feedback += `<div class="profileDataItem profileDataItem--noLabel">`; feedback += `<div class="profileDataItem profileDataItem--noLabel">`;
feedback += `<div class="profileDataItem__label"></div>`; feedback += `<div class="profileDataItem__label"></div>`;
feedback += `<div class="profileDataItem__value"><a href="/encrypt/${opts.mode}/${opts.input}">Encrypt message</a></div>`; feedback += `<div class="profileDataItem__value"><a href="/encrypt/${keyUriMode}/${keyUriId}">Encrypt message</a></div>`;
feedback += `</div>`; feedback += `</div>`;
// Display feedback // Display feedback
document.body.querySelector('#profileData').innerHTML = feedback; document.body.querySelector('#profileData').innerHTML = feedback;
try { try {
if (sigVerification) {
verifications = sigVerification.claims
} else {
verifications = await doip.claims.verify(keyData, fingerprint, {'proxyPolicy':'adaptive'}) verifications = await doip.claims.verify(keyData, fingerprint, {'proxyPolicy':'adaptive'})
}
} catch (e) { } catch (e) {
feedback += `<p>There was a problem verifying the claims.</p>`; feedback += `<p>There was a problem verifying the claims.</p>`;
feedback += `<code>${e}</code>`; feedback += `<code>${e}</code>`;
@ -357,18 +421,49 @@ async function displayProfile(opts) {
return; return;
} }
feedback = "";
if (opts.mode === 'sig') {
feedback += `<div class="profileDataItem profileDataItem--separator profileDataItem--noLabel">`;
feedback += `<div class="profileDataItem__label"></div>`;
feedback += `<div class="profileDataItem__value">proofs</div>`;
feedback += `</div>`;
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 += `<div class="profileDataItem">`;
feedback += `<div class="profileDataItem__label">${claimData.serviceprovider.name}</div>`;
feedback += `<div class="profileDataItem__value">`;
feedback += `<a class="proofDisplay" href="${claimData.profile.uri}" rel="me">${claimData.profile.display}</a>`;
if (claim.isVerified) {
feedback += `<a class="proofUrl proofUrl--verified" href="${claimData.proof.uri}">verified &#10004;</a>`;
} else {
feedback += `<a class="proofUrl" href="${claimData.proof.uri}">unverified</a>`;
}
if (claim.isVerified && claimData.profile.qr) {
feedback += `<a class="proofQR green" href="/util/qr/${encodeURIComponent(claimData.profile.qr)}" target="_blank" title="QR Code">${icon_qr}</a>`;
}
feedback += `</div>`;
feedback += `</div>`;
});
} else {
let primaryClaims; let primaryClaims;
feedback = "";
if (userMail) { if (userMail) {
verifications.forEach((userId, i) => { verifications.forEach((userId, i) => {
if (!keyData.users[i].userId) { if (!keyData.users[i].userId) {
keyData.users[i].userId = { keyData.users[i].userId = {
email: 'email not specified' email: 'email not specified'
}; }
} }
if (keyData.users[i].userId.email !== userMail) { if (keyData.users[i].userId.email != userMail) {
return; return;
} }
@ -378,7 +473,7 @@ async function displayProfile(opts) {
feedback += `<div class="profileDataItem__value">${keyData.users[i].userId.email} <small class="primary">primary</small></div>`; feedback += `<div class="profileDataItem__value">${keyData.users[i].userId.email} <small class="primary">primary</small></div>`;
feedback += `</div>`; feedback += `</div>`;
if (userId.length === 0) { if (userId.length == 0) {
feedback += `<div class="profileDataItem profileDataItem--noLabel">`; feedback += `<div class="profileDataItem profileDataItem--noLabel">`;
feedback += `<div class="profileDataItem__label"></div>`; feedback += `<div class="profileDataItem__label"></div>`;
feedback += `<div class="profileDataItem__value">No claims associated</div>`; feedback += `<div class="profileDataItem__value">No claims associated</div>`;
@ -389,7 +484,7 @@ async function displayProfile(opts) {
userId = userId.filter((a) => (a && a.errors.length == 0 && a.serviceproviderData)) 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)); 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 primaryClaims = userId;
userId.forEach((claim, i) => { userId.forEach((claim, i) => {
const claimData = claim.serviceproviderData; const claimData = claim.serviceproviderData;
@ -397,7 +492,7 @@ async function displayProfile(opts) {
return; return;
} }
feedback += `<div class="profileDataItem">`; feedback += `<div class="profileDataItem">`;
feedback += `<div class="profileDataItem__label">${capitalizeLetteredServices(claimData.serviceprovider.name)}</div>`; feedback += `<div class="profileDataItem__label">${claimData.serviceprovider.name}</div>`;
feedback += `<div class="profileDataItem__value">`; feedback += `<div class="profileDataItem__value">`;
feedback += `<a class="proofDisplay" href="${claimData.profile.uri}" rel="me">${claimData.profile.display}</a>`; feedback += `<a class="proofDisplay" href="${claimData.profile.uri}" rel="me">${claimData.profile.display}</a>`;
if (claim.isVerified) { if (claim.isVerified) {
@ -424,7 +519,7 @@ async function displayProfile(opts) {
feedback += `<div class="profileDataItem__value">${keyData.users[i].userId.email}</div>`; feedback += `<div class="profileDataItem__value">${keyData.users[i].userId.email}</div>`;
feedback += `</div>`; feedback += `</div>`;
if (userId.length == 0) { if (userId.length === 0) {
feedback += `<div class="profileDataItem profileDataItem--noLabel">`; feedback += `<div class="profileDataItem profileDataItem--noLabel">`;
feedback += `<div class="profileDataItem__label"></div>`; feedback += `<div class="profileDataItem__label"></div>`;
feedback += `<div class="profileDataItem__value">No claims associated</div>`; feedback += `<div class="profileDataItem__value">No claims associated</div>`;
@ -464,6 +559,7 @@ async function displayProfile(opts) {
feedback += `</div>`; feedback += `</div>`;
}); });
}); });
}
// Display feedback // Display feedback
document.body.querySelector('#profileProofs').innerHTML = feedback; document.body.querySelector('#profileProofs').innerHTML = feedback;
@ -1024,8 +1120,10 @@ async function fetchWithTimeout(url, timeout = 3000) {
let elFormVerify = document.body.querySelector("#form-verify"), let elFormVerify = document.body.querySelector("#form-verify"),
elFormEncrypt = document.body.querySelector("#form-encrypt"), elFormEncrypt = document.body.querySelector("#form-encrypt"),
elFormProofs = document.body.querySelector("#form-proofs"), elFormProofs = document.body.querySelector("#form-proofs"),
elFormSignatureProfile = document.body.querySelector("#form-generate-signature-profile"),
elProfileUid = document.body.querySelector("#profileUid"), elProfileUid = document.body.querySelector("#profileUid"),
elProfileMode = document.body.querySelector("#profileMode"), elProfileMode = document.body.querySelector("#profileMode"),
elProfileServer = document.body.querySelector("#profileServer"),
elModeSelect = document.body.querySelector("#modeSelect"), elModeSelect = document.body.querySelector("#modeSelect"),
elUtilWKD = document.body.querySelector("#form-util-wkd"), elUtilWKD = document.body.querySelector("#form-util-wkd"),
elUtilQRFP = document.body.querySelector("#form-util-qrfp"), elUtilQRFP = document.body.querySelector("#form-util-qrfp"),
@ -1171,9 +1269,22 @@ if (elFormProofs) {
} }
if (elProfileUid) { if (elProfileUid) {
let match, opts, profileUid = elProfileUid.innerHTML; let opts, profileUid = elProfileUid.innerHTML;
switch (elProfileMode.innerHTML) { switch (elProfileMode.innerHTML) {
default: 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": case "auto":
if (/.*@.*/.test(profileUid)) { if (/.*@.*/.test(profileUid)) {
// Match email for wkd // Match email for wkd
@ -1191,6 +1302,13 @@ if (elProfileUid) {
break; break;
case "hkp": case "hkp":
opts = {
input: profileUid,
server: elProfileServer.innerHTML,
mode: elProfileMode.innerHTML
}
break;
case "wkd": case "wkd":
opts = { opts = {
input: profileUid, input: profileUid,
@ -1207,7 +1325,10 @@ if (elProfileUid) {
} }
break; break;
} }
if (elProfileMode.innerHTML !== 'sig') {
displayProfile(opts); displayProfile(opts);
}
} }
if (elUtilWKD) { if (elUtilWKD) {

View file

@ -369,6 +369,11 @@ a.proofQR:hover {
background-color: #6abb5a; background-color: #6abb5a;
} }
#form-generate-signature-profile {
margin-bottom: 2em;
font-size: 0.9rem;
}
#qrcode { #qrcode {
display: flex; display: flex;
justify-content: center; justify-content: center;

View file

@ -9,11 +9,21 @@ head
main.container.container--profile main.container.container--profile
.content .content
span#profileUid(style='display: none;') #{uid} span#profileUid(style='display: none;') #{uid}
span#profileServer(style='display: none;') #{server}
span#profileMode(style='display: none;') #{mode} 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 #profileHeader
img#profileAvatar(src='/static/img/avatar_placeholder.png' alt='avatar' style='display: none') img#profileAvatar(src='/static/img/avatar_placeholder.png' alt='avatar' style='display: none')
p#profileName p#profileName
#profileData #profileData
if (mode == 'sig')
p Waiting for input&mldr;
else
p Loading keys &amp; verifying proofs&mldr; p Loading keys &amp; verifying proofs&mldr;
footer footer
p p
@ -23,6 +33,6 @@ main.container.container--profile
a(href="https://codeberg.org/keyoxide/web/releases")= settings.keyoxide_version a(href="https://codeberg.org/keyoxide/web/releases")= settings.keyoxide_version
| ). | ).
script(src='/static/openpgp.min.js') script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
script(src='/static/doip.js') script(type='application/javascript' src='/static/doip.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8') script(type='application/javascript' src='/static/scripts.js' charset='utf-8')

View file

@ -36,6 +36,6 @@ main.container
a(href='https://fosstodon.org/@keyoxide') Mastodon a(href='https://fosstodon.org/@keyoxide') Mastodon
p &copy; 2020 Keyoxide contributors p &copy; 2020 Keyoxide contributors
script(src='/static/openpgp.min.js') script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
script(src='/static/qrcode.min.js') script(type='application/javascript' src='/static/qrcode.min.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8') script(type='application/javascript' src='/static/scripts.js' charset='utf-8')

View file

@ -703,10 +703,10 @@ doctypes@^1.1.0:
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk= integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
doipjs@^0.8.4: doipjs@^0.9.0:
version "0.8.4" version "0.9.0"
resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.8.4.tgz#41046de8b9c69afd633d452e09f92a80930f2e0a" resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.9.0.tgz#628af8316ea40904d8695ef372e9f0d0211c25d2"
integrity sha512-eJdrwClJ5fU3D4oIl+cGMSLEaHJ2AJnF4KvlO095aq+ztJQWi5WYwnqM6j5ASj9pMUlA3OAyYKhIPdZSlUF5uA== integrity sha512-Tw9Ep9vyWNFx4cBmNNtkE/gLakBY32+A09WLosBpAdtvm573h0N/Jww/IL5cr0gu5947pbqxUCnwkQySRB3N1A==
dependencies: dependencies:
bent "^7.3.12" bent "^7.3.12"
browserify "^17.0.0" browserify "^17.0.0"