mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 23:09:29 -07:00
Add support f or signature profiles
This commit is contained in:
parent
fe423b54e0
commit
c654b5646c
7 changed files with 224 additions and 96 deletions
|
@ -5,6 +5,8 @@ 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]
|
||||||
|
### Changed
|
||||||
|
- Allow setting of custom HKP server
|
||||||
|
|
||||||
## [2.3.4] - 2021-01-02
|
## [2.3.4] - 2021-01-02
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bent": "^7.3.12",
|
"bent": "^7.3.12",
|
||||||
"doipjs": "^0.8.5",
|
"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",
|
||||||
|
|
|
@ -29,24 +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) {
|
router.get('/hkp/:server/:input', function(req, res) {
|
||||||
res.render('profile', { mode: "hkp", uid: req.params.input, server: req.params.server })
|
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;
|
||||||
|
|
|
@ -231,9 +231,55 @@ 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 {
|
||||||
|
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 {
|
try {
|
||||||
let keyURI;
|
let keyURI;
|
||||||
if (opts.mode === 'hkp' && opts.server) {
|
if (opts.mode === 'hkp' && opts.server) {
|
||||||
|
@ -250,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;
|
||||||
|
@ -259,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}`;
|
||||||
|
@ -337,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>`;
|
||||||
|
@ -362,9 +421,40 @@ async function displayProfile(opts) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let primaryClaims
|
|
||||||
|
|
||||||
feedback = "";
|
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 ✔</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;
|
||||||
|
|
||||||
if (userMail) {
|
if (userMail) {
|
||||||
verifications.forEach((userId, i) => {
|
verifications.forEach((userId, i) => {
|
||||||
if (!keyData.users[i].userId) {
|
if (!keyData.users[i].userId) {
|
||||||
|
@ -394,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;
|
||||||
|
@ -469,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;
|
||||||
|
@ -1029,6 +1120,7 @@ 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"),
|
elProfileServer = document.body.querySelector("#profileServer"),
|
||||||
|
@ -1180,6 +1272,19 @@ if (elProfileUid) {
|
||||||
let 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
|
||||||
|
@ -1220,7 +1325,10 @@ if (elProfileUid) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (elProfileMode.innerHTML !== 'sig') {
|
||||||
displayProfile(opts);
|
displayProfile(opts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elUtilWKD) {
|
if (elUtilWKD) {
|
||||||
|
|
|
@ -347,6 +347,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;
|
||||||
|
|
|
@ -11,10 +11,19 @@ main.container.container--profile
|
||||||
span#profileUid(style='display: none;') #{uid}
|
span#profileUid(style='display: none;') #{uid}
|
||||||
span#profileServer(style='display: none;') #{server}
|
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…
|
||||||
|
else
|
||||||
p Loading keys & verifying proofs…
|
p Loading keys & verifying proofs…
|
||||||
footer
|
footer
|
||||||
p
|
p
|
||||||
|
|
|
@ -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.5:
|
doipjs@^0.9.0:
|
||||||
version "0.8.5"
|
version "0.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.8.5.tgz#f27770551304314db2921dd019a983367c9258e6"
|
resolved "https://registry.yarnpkg.com/doipjs/-/doipjs-0.9.0.tgz#628af8316ea40904d8695ef372e9f0d0211c25d2"
|
||||||
integrity sha512-zTIwAHUKn8t/EL36ZUIJ1Oyp+S0B2ifthPUJw7hC+8XBrHVxf9Q8koCQ92TF1CWY6eKT3eAlEEkR0BwqLpwWhw==
|
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"
|
||||||
|
|
Loading…
Reference in a new issue