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 += `${claimData.profile.display}`; + if (claim.isVerified) { + feedback += `verified ✔`; + } else { + feedback += `unverified`; + } + if (claim.isVerified && claimData.profile.qr) { + feedback += `${icon_qr}`; + } + 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 += `${claimData.profile.display}`; + if (claim.isVerified) { + feedback += `verified ✔`; + } else { + feedback += `unverified`; + } + if (claim.isVerified && claimData.profile.qr) { + feedback += `${icon_qr}`; + } + 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 += `${claimData.profile.display}`; - if (claim.isVerified) { - feedback += `verified ✔`; - } else { - feedback += `unverified`; - } - if (claim.isVerified && claimData.profile.qr) { - feedback += `${icon_qr}`; - } - 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"