Add webpack bundling

This commit is contained in:
Yarmo Mackenbach 2022-02-25 19:18:46 +01:00
parent 61a440cf65
commit 4fa50ab069
No known key found for this signature in database
GPG key ID: 37367F4AF4087AD1
20 changed files with 2861 additions and 79 deletions

View file

@ -1,6 +1,12 @@
FROM node:14-alpine
WORKDIR /app
COPY . .
RUN yarn --production --pure-lockfile
RUN yarn run build:static
EXPOSE 3000
CMD yarn start

View file

@ -12,6 +12,7 @@
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-validator": "^6.13.0",
"fork-awesome": "^1.2.0",
"got": "^11.8.2",
"jstransformer-markdown-it": "^2.1.0",
"libravatar": "^3.0.0",
@ -21,12 +22,21 @@
"string-replace-middleware": "^1.0.2"
},
"devDependencies": {
"css-loader": "^6.6.0",
"license-check-and-add": "^4.0.3",
"nodemon": "^2.0.7"
"mini-css-extract-plugin": "^2.5.3",
"nodemon": "^2.0.7",
"style-loader": "^3.3.1",
"webpack": "^5.69.1",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.2"
},
"scripts": {
"start": "node ./",
"dev": "NODE_ENV=development ./node_modules/.bin/nodemon --config nodemon.json ./",
"dev": "yarn run watch & yarn run build:static:dev",
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production",
"build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
"license:check": "./node_modules/.bin/license-check-and-add check",
"license:add": "./node_modules/.bin/license-check-and-add add",
"license:remove": "./node_modules/.bin/license-check-and-add remove"

View file

@ -30,34 +30,6 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
const express = require('express')
const router = require('express').Router()
router.get('/doip.min.js', function(req, res) {
res.sendFile(`node_modules/doipjs/dist/doip.min.js`, { root: `${__dirname}/../` })
})
router.get('/doip.js', function(req, res) {
res.sendFile(`node_modules/doipjs/dist/doip.js`, { root: `${__dirname}/../` })
})
router.get('/openpgp.min.js', function(req, res) {
res.sendFile(`node_modules/openpgp/dist/openpgp.min.js`, { root: `${__dirname}/../` })
})
router.get('/openpgp.min.js.map', function(req, res) {
res.sendFile(`node_modules/openpgp/dist/openpgp.min.js.map`, { root: `${__dirname}/../` })
})
router.get('/qrcode.min.js', function(req, res) {
res.sendFile(`node_modules/qrcode/build/qrcode.min.js`, { root: `${__dirname}/../` })
})
router.get('/qrcode.min.js.map', function(req, res) {
res.sendFile(`node_modules/qrcode/build/qrcode.min.js.map`, { root: `${__dirname}/../` })
})
router.get('/dialog-polyfill.js', function(req, res) {
res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.js`, { root: `${__dirname}/../` })
})
router.get('/dialog-polyfill.css', function(req, res) {
res.sendFile(`node_modules/dialog-polyfill/dist/dialog-polyfill.css`, { root: `${__dirname}/../` })
})
router.use('/', express.static('static'))
module.exports = router

50
static-src/index.js Normal file
View file

@ -0,0 +1,50 @@
/*
Copyright (C) 2022 Yarmo Mackenbach
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/>.
*/
// Import JS libraries
import * as kx from'./keyoxide'
import * as kxKey from'./kx-key'
import * as kxClaim from'./kx-claim'
import * as ui from'./ui'
import * as utils from'./utils'
// Import CSS files
import './styles.css'
import './kx-styles.css'
// Add functions to window
window.showQR = utils.showQR
// Register custom elements
customElements.define('kx-key', kxKey.Key)
customElements.define('kx-claim', kxClaim.Claim)
// Run scripts
ui.init()
kx.init()

39
static-src/keyoxide.js Normal file
View file

@ -0,0 +1,39 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
export function init() {
// Verify all claims
const claims = document.querySelectorAll('kx-claim');
claims.forEach(function(claim) {
if (claim.hasAttribute('data-skip') && claim.getAttribute('data-skip')) {
return;
}
claim.verify();
});
}

220
static-src/kx-claim.js Normal file
View file

@ -0,0 +1,220 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
import * as doip from "doipjs"
export class Claim extends HTMLElement {
// Specify the attributes to observe
static get observedAttributes() {
return ['data-claim'];
}
constructor() {
// Call super
super();
}
attributeChangedCallback(name, oldValue, newValue) {
this.updateContent(newValue);
}
async verify() {
const claim = new doip.Claim(JSON.parse(this.getAttribute('data-claim')));
await claim.verify({
proxy: {
policy: 'adaptive',
hostname: 'PLACEHOLDER__PROXY_HOSTNAME'
}
});
this.setAttribute('data-claim', JSON.stringify(claim));
}
updateContent(value) {
const root = this;
const claim = new doip.Claim(JSON.parse(value));
switch (claim.matches[0].serviceprovider.name) {
case 'dns':
case 'xmpp':
case 'irc':
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name.toUpperCase();
break;
default:
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name;
break;
}
root.querySelector('.info .title').innerText = claim.matches[0].profile.display;
try {
if (claim.status === 'verified') {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.verification.result ? 'success' : 'failed');
} else {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
}
} catch (error) {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed');
}
const elContent = root.querySelector('.content');
elContent.innerHTML = ``;
// Handle failed ambiguous claim
if (claim.status === 'verified' && !claim.verification.result && claim.isAmbiguous()) {
root.querySelector('.info .subtitle').innerText = '---';
const subsection_alert = elContent.appendChild(document.createElement('div'));
subsection_alert.setAttribute('class', 'subsection');
const subsection_alert_icon = subsection_alert.appendChild(document.createElement('img'));
subsection_alert_icon.setAttribute('src', '/static/img/alert-decagram.png');
subsection_alert_icon.setAttribute('alt', '');
subsection_alert_icon.setAttribute('aria-hidden', 'true');
const subsection_alert_text = subsection_alert.appendChild(document.createElement('div'));
const message = subsection_alert_text.appendChild(document.createElement('p'));
message.innerHTML = `None of the matched service providers could be verified. Keyoxide was not able to determine which was the correct service provider or why the verification process failed.`;
return;
}
// Links to profile and proof
const subsection_links = elContent.appendChild(document.createElement('div'));
subsection_links.setAttribute('class', 'subsection');
const subsection_links_icon = subsection_links.appendChild(document.createElement('img'));
subsection_links_icon.setAttribute('src', '/static/img/link.png');
subsection_links_icon.setAttribute('alt', '');
subsection_links_icon.setAttribute('aria-hidden', 'true');
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].profile.uri) {
profile_link.innerHTML = `Profile link: <a rel="me" href="${claim.matches[0].profile.uri}" aria-label="link to profile">${claim.matches[0].profile.uri}</a>`;
} else {
profile_link.innerHTML = `Profile link: not accessible from browser`;
}
const proof_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].proof.uri) {
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.uri}" aria-label="link to profile">${claim.matches[0].proof.uri}</a>`;
} else {
proof_link.innerHTML = `Proof link: not accessible from browser`;
}
// QR Code
if (claim.matches[0].profile.qr) {
elContent.appendChild(document.createElement('hr'));
const subsection_qr = elContent.appendChild(document.createElement('div'));
subsection_qr.setAttribute('class', 'subsection');
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.png');
subsection_qr_icon.setAttribute('alt', '');
subsection_qr_icon.setAttribute('aria-hidden', 'true');
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
const button_profileQR = subsection_qr_text.appendChild(document.createElement('button'));
button_profileQR.innerText = `Show profile QR`;
button_profileQR.setAttribute('onClick', `window.showQR('${claim.matches[0].profile.qr}', 'url')`);
button_profileQR.setAttribute('aria-label', `Show QR code linking to profile`);
}
elContent.appendChild(document.createElement('hr'));
// Claim verification status
const subsection_status = elContent.appendChild(document.createElement('div'));
subsection_status.setAttribute('class', 'subsection');
const subsection_status_icon = subsection_status.appendChild(document.createElement('img'));
subsection_status_icon.setAttribute('src', '/static/img/decagram.png');
subsection_status_icon.setAttribute('alt', '');
subsection_status_icon.setAttribute('aria-hidden', 'true');
const subsection_status_text = subsection_status.appendChild(document.createElement('div'));
const verification = subsection_status_text.appendChild(document.createElement('p'));
if (claim.status === 'verified') {
verification.innerHTML = `Claim verification has completed.`;
subsection_status_icon.setAttribute('src', '/static/img/check-decagram.png');
subsection_status_icon.setAttribute('alt', '');
subsection_status_icon.setAttribute('aria-hidden', 'true');
} else {
verification.innerHTML = `Claim verification is in progress&hellip;`;
return;
}
elContent.appendChild(document.createElement('hr'));
// Result of claim verification
const subsection_result = elContent.appendChild(document.createElement('div'));
subsection_result.setAttribute('class', 'subsection');
const subsection_result_icon = subsection_result.appendChild(document.createElement('img'));
subsection_result_icon.setAttribute('src', '/static/img/shield-search.png');
subsection_result_icon.setAttribute('alt', '');
subsection_result_icon.setAttribute('aria-hidden', 'true');
const subsection_result_text = subsection_result.appendChild(document.createElement('div'));
const result = subsection_result_text.appendChild(document.createElement('p'));
result.innerHTML = `The claim <strong>${claim.verification.result ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
// Additional info
if (claim.verification.proof.viaProxy) {
elContent.appendChild(document.createElement('hr'));
const subsection_info = elContent.appendChild(document.createElement('div'));
subsection_info.setAttribute('class', 'subsection');
const subsection_info_icon = subsection_info.appendChild(document.createElement('img'));
subsection_info_icon.setAttribute('src', '/static/img/information.png');
subsection_info_icon.setAttribute('alt', '');
subsection_info_icon.setAttribute('aria-hidden', 'true');
const subsection_info_text = subsection_info.appendChild(document.createElement('div'));
const result_proxyUsed = subsection_info_text.appendChild(document.createElement('p'));
result_proxyUsed.innerHTML = `A proxy was used to fetch the proof: <a href="https://PLACEHOLDER__PROXY_HOSTNAME" aria-label="Link to proxy server">PLACEHOLDER__PROXY_HOSTNAME</a>`;
}
// TODO Display errors
// if (claim.verification.errors.length > 0) {
// console.log(claim.verification);
// elContent.appendChild(document.createElement('hr'));
// const subsection_errors = elContent.appendChild(document.createElement('div'));
// subsection_errors.setAttribute('class', 'subsection');
// const subsection_errors_icon = subsection_errors.appendChild(document.createElement('img'));
// subsection_errors_icon.setAttribute('src', '/static/img/alert-circle.png');
// const subsection_errors_text = subsection_errors.appendChild(document.createElement('div'));
// claim.verification.errors.forEach(message => {
// const error = subsection_errors_text.appendChild(document.createElement('p'));
// if (message instanceof Error) {
// error.innerText = message.message;
// } else {
// error.innerText = message;
// }
// });
// }
}
}

83
static-src/kx-key.js Normal file
View file

@ -0,0 +1,83 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
export class Key extends HTMLElement {
// Specify the attributes to observe
static get observedAttributes() {
return ['data-keydata'];
}
constructor() {
// Call super
super();
}
attributeChangedCallback(name, oldValue, newValue) {
this.updateContent(newValue);
}
updateContent(value) {
const root = this;
const data = JSON.parse(value);
root.querySelector('.info .subtitle').innerText = data.key.fetchMethod;
root.querySelector('.info .title').innerText = data.fingerprint;
const elContent = root.querySelector('.content');
elContent.innerHTML = ``;
// Link to key
const subsection_links = elContent.appendChild(document.createElement('div'));
subsection_links.setAttribute('class', 'subsection');
const subsection_links_icon = subsection_links.appendChild(document.createElement('img'));
subsection_links_icon.setAttribute('src', '/static/img/link.png');
subsection_links_icon.setAttribute('alt', '');
subsection_links_icon.setAttribute('aria-hidden', 'true');
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
profile_link.innerHTML = `Key link: <a href="${data.key.uri}" aria-label="Link to cryptographic key">${data.key.uri}</a>`;
elContent.appendChild(document.createElement('hr'));
// QR Code
const subsection_qr = elContent.appendChild(document.createElement('div'));
subsection_qr.setAttribute('class', 'subsection');
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.png');
subsection_qr_icon.setAttribute('alt', '');
subsection_qr_icon.setAttribute('aria-hidden', 'true');
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
}
}

254
static-src/kx-styles.css Normal file
View file

@ -0,0 +1,254 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
* {
box-sizing: border-box;
}
details.kx-item {
width: 100%;
border-radius: 8px;
}
details.kx-item p {
margin: 0;
word-break: break-word;
}
details.kx-item a {
color: var(--blue-700);
}
details.kx-item hr {
border: none;
border-top: 2px solid var(--purple-100);
}
details.kx-item .content {
padding: 12px;
border: solid 3px var(--purple-100);
border-top: 0px;
border-radius: 0px 0px 8px 8px;
}
details.kx-item summary {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: var(--purple-100);
border: solid 3px var(--purple-100);
border-radius: 8px;
list-style: none;
cursor: pointer;
}
details.kx-item summary::-webkit-details-marker {
display: none;
}
details.kx-item summary:hover, summary:focus {
border-color: var(--purple-400);
}
details[open] summary {
border-radius: 8px 8px 0px 0px;
}
details.kx-item summary .info {
flex: 1;
}
details.kx-item summary .info .title {
font-size: 1.1em;
}
details.kx-item summary .claim__description p {
font-size: 1.4rem;
line-height: 2rem;
}
details.kx-item summary .claim__links p, p.subtle-links {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 1rem;
color: var(--grey-700);
}
details.kx-item summary .claim__links a, summary .claim__links span, p.subtle-links a {
font-size: 1rem;
margin: 0 10px 0 0;
color: var(--grey-700);
}
details.kx-item summary .subtitle {
color: var(--purple-700);
}
details.kx-item summary .verificationStatus {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 100%;
color: #fff;
font-size: 2rem;
user-select: none;
}
details.kx-item summary .verificationStatus::after {
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
}
details.kx-item summary .verificationStatus .inProgress {
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
details.kx-item summary .verificationStatus[data-value="success"] {
content: "v";
background-color: var(--green-600);
}
details.kx-item summary .verificationStatus[data-value="success"]::after {
content: "✔";
}
details.kx-item summary .verificationStatus[data-value="failed"] {
background-color: var(--red-400);
}
details.kx-item summary .verificationStatus[data-value="failed"]::after {
content: "✕";
}
details.kx-item summary .verificationStatus[data-value="running"] .inProgress {
opacity: 1;
}
details.kx-item .subsection {
display: flex;
align-items: center;
gap: 16px;
}
details.kx-item .subsection > img {
width: 24px;
height: 24px;
opacity: 0.4;
}
details.kx-item .inProgress {
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--purple-400);
background: -moz-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -webkit-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -o-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -ms-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: linear-gradient(to right, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
details.kx-item .inProgress:before {
width: 50%;
height: 50%;
background: var(--purple-400);
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
details.kx-item .inProgress:after {
background: var(--purple-100);
width: 65%;
height: 65%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
details.kx-item button {
padding: 0.4rem 0.8rem;
margin-right: 8px;
text-decoration: none;
text-transform: uppercase;
background-color: #fff;
border: solid 2px var(--purple-400);
border-radius: 4px;
cursor: pointer;
}
details.kx-item button:hover {
background-color: var(--purple-500);
border-color: var(--purple-500);
color: #fff;
}
@media screen and (max-width: 640px) {
details.kx-item summary .claim__description p {
font-size: 1.2rem;
}
details.kx-item summary .claim__links a, p.subtle-links a {
font-size: 0.9rem;
}
}
@media screen and (max-width: 480px) {
summary .claim__description p {
font-size: 1rem;
}
details.kx-item summary .verificationStatus {
width: 36px;
height: 36px;
font-size: 1.6rem;
}
details.kx-item .inProgress {
width: 36px;
height: 36px;
}
}
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

744
static-src/styles.css Normal file
View file

@ -0,0 +1,744 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
@import '../node_modules/fork-awesome/css/fork-awesome.css';
@import '../node_modules/dialog-polyfill/dist/dialog-polyfill.css';
:root {
--grey-500: hsl(0, 0%, 50%);
--grey-600: hsl(0, 0%, 40%);
--grey-700: hsl(0, 0%, 30%);
--grey-900: hsl(0, 0%, 10%);
--green-300: hsl(110, 45%, 70%);
--green-400: hsl(110, 45%, 60%);
--green-600: hsl(110, 45%, 40%);
--red-400: hsl(10, 60%, 60%);
--blue-500: hsl(201, 80%, 59%);
--blue-700: hsl(201, 90%, 30%);
--purple-50: hsl(250, 30%, 98%);
--purple-100: hsl(250, 48%, 95%);
--purple-200: hsl(250, 48%, 90%);
--purple-300: hsl(250, 48%, 85%);
--purple-400: hsl(250, 48%, 70%);
--purple-500: hsl(250, 48%, 65%);
--purple-600: hsl(250, 48%, 60%);
--purple-700: hsl(250, 48%, 55%);
--purple-900: hsl(250, 38%, 45%);
--yellow-100: hsl(56, 100%, 95%);
--yellow-200: hsl(56, 100%, 90%);
--yellow-500: hsl(56, 100%, 65%);
}
* {
box-sizing: border-box;
}
:focus {
outline: none;
box-shadow: 0 0 0 3px lightskyblue;
}
input:focus, textarea:focus {
background: azure;
}
input[type="radio"]:focus + label {
box-shadow: 0 0 0 3px lightskyblue;
background: azure !important;
color: var(--grey-900) !important;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
padding: 1.6rem 0 0;
line-height: 1.4rem;
font-family: sans-serif;
color: var(--grey-900);
}
/* HELPERS */
.spacer {
flex: 1;
}
.no-margin {
margin: 0 !important;
}
.full-width {
display: block;
width: 100% !important;
}
/* LAYOUT */
header {
margin: 0 1.6rem 1.6rem;
}
header nav {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
}
header nav a.logo {
width: 64px;
height: 64px;
font-size: 1.6rem;
text-transform: uppercase;
text-decoration: none;
color: var(--purple-700);
}
header nav a.logo img {
width: 100%;
}
nav a.text {
/* font-size: 0.9em; */
margin: 0;
padding: 0.5em 1em;
text-transform: uppercase;
text-decoration: none;
color: var(--purple-700);
border-radius: 4px;
}
nav a.text:hover, nav a.text:active {
color: #fff;
background-color: var(--purple-500);
}
main {
flex: 1;
margin: 0 1.6rem;
}
footer {
margin: 4.8rem 0 0;
padding: 0 1.6rem 1.6rem;
background-color: var(--purple-900);
color: var(--purple-200);
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
}
section.profile p, .demo p {
font-size: 1.2rem;
}
.demo {
margin: 4.8rem auto;
}
.card {
margin: 0 0 1.6rem;
padding: 0 1.2rem;
background-color: #fff;
background-color: var(--purple-50);
border: 2px solid var(--purple-200);
border-radius: 4px;
}
.card.card--transparent {
padding-left: 0;
padding-right: 0;
background-color: transparent;
border: 0;
}
.card--profileHeader {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
gap: 24px;
}
.card--profileHeader p, .card--profileHeader small {
margin: 0;
}
.card--small-profile {
display: flex;
flex-direction: column;
text-align: center;
}
.card--small-profile-dummy {
opacity: 0.5;
border: 0;
}
.card--small-profile .name {
font-size: 1.4em;
}
.card--small-profile p {
margin-top: 0;
}
.card--small-profile p span.fingerprint {
display: inline-block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.8rem;
}
#profileName {
font-size: 1.6rem;
color: var(--grey-700);
}
#profileURLFingerprint {
font-size: 1rem;
margin: 0 0 1.2rem;
}
.hcards {
display: grid;
grid-gap: 1.2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-bottom: 1.6rem;
}
.hcards .card {
margin: 0;
}
.hcards--col-1-2, .hcards--col-2-1 {
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
}
.hcards--col-1-2 .card, .hcards--col-2-1 .card {
grid-column: 1 / 2;
}
@media screen and (min-width: 1024px) {
.hcards--max-3 {
grid-template-columns: 1fr 1fr 1fr;
}
.hcards--col-1-2, .hcards--col-2-1 {
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (min-width: 720px) {
.hcards--col-2-1 .card:nth-of-type(1) {
grid-column: 1 / -2;
}
.hcards--col-2-1 .card:nth-of-type(2) {
grid-column: -2 / -1;
}
.hcards--col-1-2 .card:nth-of-type(1) {
grid-column: 1 / 2;
}
.hcards--col-1-2 .card:nth-of-type(2) {
grid-column: 2 / -1;
}
}
.warning {
padding: calc(0.8rem - 2px) 0.8rem;
background-color: var(--yellow-200);
border: solid 2px var(--yellow-500);
}
.warning p:first-of-type {
margin-top: 0;
}
.warning p:last-of-type {
margin-bottom: 0;
}
kx-claim {
display: block;
margin: 12px 0;
}
#profileAvatar {
display: inline-block;
min-width: 96px;
max-width: 128px;
line-height: 0;
text-align: center;
border-radius: 50%;
}
/* TYPOGRAPHY */
h1 {
font-size: 1.6em;
margin: 3.2rem 0 1.6rem;
font-weight: normal;
color: var(--purple-700);
cursor: default;
}
h2 {
font-size: 1.4em;
margin: 3.2rem 0 1.6rem;
font-weight: normal;
color: var(--purple-700);
cursor: default;
}
h2 small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--purple-600);
color: #fff;
border-radius: 4px;
}
h3 {
margin: 1.6rem 0;
font-size: 1.3em;
line-height: 1.6rem;
color: var(--grey-700);
font-weight: normal;
/* text-align: center; */
cursor: default;
}
h3 small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--purple-400);
color: #fff;
border-radius: 4px;
}
h4 {
margin: 1.6rem 0;
font-size: 1em;
line-height: 1.6rem;
color: var(--grey-600);
/* color: var(--purple-700); */
font-weight: bold;
cursor: default;
}
h4 small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--purple-400);
color: #fff;
border-radius: 4px;
}
p {
margin: 1.6rem 0;
}
p.warning {
padding: 8px;
background-color: #fffadc;
border: solid 1px #ffeea8;
}
a {
color: var(--blue-700);
}
ul {
padding-left: 1em;
list-style: '- ';
}
main h1:first-of-type {
margin-top: 1.6rem;
}
footer h1 {
margin-bottom: 0.8rem;
color: var(--purple-200);
font-size: 1.2rem;
font-weight: bold;
}
footer a {
display: inline-block;
color: var(--purple-100);
height: 32px;
}
code {
padding: 2px 4px;
background-color: var(--purple-100);
border: 1px solid var(--purple-500);
}
pre {
padding: 8px 12px;
background-color: var(--purple-100);
border: 1px solid var(--purple-500);
overflow-x: auto;
line-height: 1.2rem;
font-size: 1rem;
}
pre code {
padding: 0;
background-color: 0px;
border: 0px;
}
#qr {
display: block;
width: 100% !important;
max-width: 256px !important;
height: auto !important;
margin: 0 auto 16px;
}
/* FORM ELEMENTS */
.form-wrapper {
align-items: center;
padding-top: 1.4rem;
padding-bottom: 1.6rem;
margin-bottom: 48px;
}
.form-wrapper form {
display: flex;
flex-direction: column;
margin: 0;
}
.form-wrapper h2 {
margin-top: 0;
}
form input[type="text"], form input[type="search"] {
margin: 8px 0;
padding: 4px;
border: 1px solid #444;
font-size: 0.9rem;
}
form textarea {
width: 100%;
height: 128px;
margin: 8px 0;
resize: vertical;
font-size: 0.9rem;
border: 1px solid #444;
}
.button-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0;
}
.radio-wrapper {
display: flex;
flex-wrap: wrap;
margin: 8px 0;
}
.radio-wrapper input[type="radio"] {
position: absolute;
opacity: 0;
z-index: -1;
}
.radio-wrapper input[type="radio"] + label {
margin: 0;
padding: 2px 8px;
background-color: #fff;
border: solid var(--purple-400);
border-width: 2px 1px;
cursor: pointer;
}
.radio-wrapper input[type="radio"]:first-of-type + label {
border-radius: 4px 0 0 4px;
border-left-width: 2px;
}
.radio-wrapper input[type="radio"]:last-of-type + label {
border-radius: 0 4px 4px 0;
border-right-width: 2px;
}
.radio-wrapper input[type="radio"]:focus + label {
z-index: 1;
}
.radio-wrapper input[type="radio"] + label:hover {
background-color: var(--purple-100);
border-color: var(--purple-500);
}
.radio-wrapper input[type="radio"]:checked + label {
color: #fff;
background-color: var(--purple-600);
border-color: var(--purple-600);
}
input[type="button"], input[type="submit"], button, a.button {
display: inline-block;
min-height: 36px;
margin: 8px 0;
padding: 4px 8px;
font-family: sans-serif;
font-size: 0.9rem;
text-decoration: none;
text-transform: uppercase;
color: #333;
background-color: #fff;
border: solid 2px var(--purple-400);
border-radius: 4px;
cursor: pointer;
}
input[type="button"]:focus, input[type="submit"]:focus, button:focus, a.button:focus {
background-color: azure;
}
input[type="button"]:hover, input[type="submit"]:hover, button:hover, a.button:hover {
background-color: var(--purple-500);
border-color: var(--purple-500);
color: #fff;
}
a.button i {
font-size: 1.4em;
}
a.button.button--liberapay {
padding: 8px 16px;
font-size: 0.95rem;
color: #333;
background-color: #ffee16;
border: 0;
}
a.button.button--liberapay:hover {
background-color: #fff463;
}
/* DIALOGS */
dialog {
width: 100% !important;
max-width: 800px !important;
padding: 0 !important;
word-wrap: anywhere;
}
dialog > div {
padding: 1em;
}
dialog form[method="Dialog"] {
margin: 1em 0 0 !important;
}
dialog form[method="Dialog"] input {
width: auto;
}
dialog p {
font-size: 1rem !important;
margin: 1rem 0;
}
dialog p:first-of-type {
margin-top: 0;
}
/* KX-ITEM */
.kx-item details {
width: 100%;
border-radius: 8px;
}
.kx-item details p {
margin: 0;
word-break: break-word;
font-size: 1rem;
}
.kx-item details a {
color: var(--blue-700);
}
.kx-item details hr {
border: none;
border-top: 2px solid var(--purple-100);
}
.kx-item details .content {
padding: 12px;
border: solid 3px var(--purple-100);
border-top: 0px;
border-radius: 0px 0px 8px 8px;
}
.kx-item details summary {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: var(--purple-100);
border: solid 3px var(--purple-100);
border-radius: 8px;
list-style: none;
cursor: pointer;
}
.kx-item details summary::-webkit-details-marker {
display: none;
}
.kx-item details summary:hover, summary:focus {
border-color: var(--purple-400);
}
details[open] summary {
border-radius: 8px 8px 0px 0px;
}
.kx-item details summary .info {
flex: 1;
}
.kx-item details summary .info .title {
font-size: 1.1em;
}
.kx-item details summary .claim__description p {
font-size: 1.4rem;
line-height: 2rem;
}
.kx-item details summary .claim__links p, p.subtle-links {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 1rem;
color: var(--grey-700);
}
.kx-item details summary .claim__links a, summary .claim__links span, p.subtle-links a {
font-size: 1rem;
margin: 0 10px 0 0;
color: var(--grey-700);
}
.kx-item details summary .subtitle {
color: var(--purple-700);
}
.kx-item details summary .verificationStatus {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 100%;
color: #fff;
font-size: 2rem;
user-select: none;
}
.kx-item details summary .verificationStatus::after {
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
}
.kx-item details summary .verificationStatus .inProgress {
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.kx-item details summary .verificationStatus[data-value="success"] {
content: "v";
background-color: var(--green-600);
}
.kx-item details summary .verificationStatus[data-value="success"]::after {
content: "✔";
}
.kx-item details summary .verificationStatus[data-value="failed"] {
background-color: var(--red-400);
}
.kx-item details summary .verificationStatus[data-value="failed"]::after {
content: "✕";
}
.kx-item details summary .verificationStatus[data-value="running"] .inProgress {
opacity: 1;
}
.kx-item details .subsection {
display: flex;
align-items: center;
gap: 16px;
}
.kx-item details .subsection > img {
width: 24px;
height: 24px;
opacity: 0.4;
}
.kx-item details .inProgress {
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--purple-400);
background: -moz-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -webkit-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -o-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: -ms-linear-gradient(left, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
background: linear-gradient(to right, var(--purple-400) 10%, rgba(255, 255, 255, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.kx-item details .inProgress:before {
width: 50%;
height: 50%;
background: var(--purple-400);
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
.kx-item details .inProgress:after {
background: var(--purple-100);
width: 65%;
height: 65%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.kx-item details button {
padding: 0.4rem 0.8rem;
margin-right: 8px;
text-decoration: none;
text-transform: uppercase;
background-color: #fff;
border: solid 2px var(--purple-400);
border-radius: 4px;
cursor: pointer;
}
.kx-item details button:hover {
background-color: var(--purple-500);
border-color: var(--purple-500);
color: #fff;
}
@media screen and (max-width: 640px) {
.kx-item details summary .claim__description p {
font-size: 1.2rem;
}
.kx-item details summary .claim__links a, p.subtle-links a {
font-size: 0.9rem;
}
}
@media screen and (max-width: 480px) {
summary .claim__description p {
font-size: 1rem;
}
.kx-item details summary .verificationStatus {
width: 36px;
height: 36px;
font-size: 1.6rem;
}
.kx-item details .inProgress {
width: 36px;
height: 36px;
}
}
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

400
static-src/ui.js Normal file
View file

@ -0,0 +1,400 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
import dialogPolyfill from 'dialog-polyfill'
import QRCode from 'qrcode'
import * as openpgp from 'openpgp'
import * as utils from './utils'
// Prepare element selectors
const elFormSignatureProfile = document.body.querySelector("#formGenerateSignatureProfile")
const elFormEncrypt = document.body.querySelector("#dialog--encryptMessage form")
const elFormVerify = document.body.querySelector("#dialog--verifySignature form")
const elFormSearch = document.body.querySelector("#search")
const elProfileUid = document.body.querySelector("#profileUid")
const elProfileMode = document.body.querySelector("#profileMode")
const elProfileServer = document.body.querySelector("#profileServer")
const elModeSelect = document.body.querySelector("#modeSelect")
const elUtilWKD = document.body.querySelector("#form-util-wkd")
const elUtilQRFP = document.body.querySelector("#form-util-qrfp")
const elUtilQR = document.body.querySelector("#form-util-qr")
const elUtilProfileURL = document.body.querySelector("#form-util-profile-url")
// Initialize UI elements and event listeners
export function init() {
// Register modals
document.querySelectorAll('dialog').forEach(function(d) {
dialogPolyfill.registerDialog(d);
d.addEventListener('click', function(ev) {
if (ev && ev.target != d) {
return;
}
d.close();
});
});
// Run context-dependent scripts
if (elFormEncrypt) {
runEncryptionForm()
}
if (elFormVerify) {
runVerificationForm()
}
if (elFormSearch) {
runSearchForm()
}
if (elModeSelect) {
runModeSelector()
}
if (elProfileUid) {
runProfileGenerator()
}
if (elUtilWKD) {
runWKDUtility()
}
if (elUtilQRFP) {
runQRFPUtility()
}
if (elUtilQR) {
runQRUtility
}
if (elUtilProfileURL) {
runProfileURLUtility
}
}
const runEncryptionForm = () => {
elFormEncrypt.onsubmit = async function (evt) {
evt.preventDefault();
try {
// Fetch a key if needed
await utils.fetchProfileKey();
// Encrypt the message
let config = openpgp.config;
config.show_comment = false;
config.show_version = false;
let encrypted = await openpgp.encrypt({
message: openpgp.message.fromText(elFormEncrypt.querySelector('.input').value),
publicKeys: window.kx.key.object,
config: config
});
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 runVerificationForm = () => {
elFormVerify.onsubmit = async function (evt) {
evt.preventDefault();
try {
// Fetch a key if needed
await utils.fetchProfileKey();
// 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: window.kx.key.object
});
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}`;
}
}
}
const runSearchForm = () => {
elFormSearch.onsubmit = function (evt) {
evt.preventDefault();
const protocol = elFormSearch.querySelector("input[type='radio']:checked").value;
const identifier = elFormSearch.querySelector("input[type='search']").value;
if (protocol == 'sig') {
window.location.href = `/${protocol}`;
} else {
window.location.href = `/${protocol}/${encodeURIComponent(identifier)}`;
}
}
elFormSearch.querySelectorAll("input[type='radio']").forEach(function (el) {
el.oninput = function (evt) {
evt.preventDefault();
if (evt.target.getAttribute('id') === 'protocol-sig') {
elFormSearch.querySelector("input[type='search']").setAttribute('disabled', true);
} else {
elFormSearch.querySelector("input[type='search']").removeAttribute('disabled');
}
}
});
elFormSearch.querySelector("input[type='radio']:checked").dispatchEvent(new Event('input'));
}
const runModeSelector = () => {
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"));
}
const runProfileGenerator = () => {
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
opts = {
input: profileUid,
mode: "wkd"
}
} else {
// Match fingerprint for hkp
opts = {
input: profileUid,
mode: "hkp"
}
}
break;
case "hkp":
opts = {
input: profileUid,
server: elProfileServer.innerHTML,
mode: elProfileMode.innerHTML
}
break;
case "wkd":
opts = {
input: profileUid,
mode: elProfileMode.innerHTML
}
break;
case "keybase":
let match = profileUid.match(/(.*)\/(.*)/);
opts = {
username: match[1],
fingerprint: match[2],
mode: elProfileMode.innerHTML
}
break;
}
if (elProfileMode.innerHTML !== 'sig') {
keyoxide.displayProfile(opts);
}
}
const runWKDUtility = () => {
elUtilWKD.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = document.body.querySelector("#input");
const elOutput = document.body.querySelector("#output");
const elOutputDirect = document.body.querySelector("#output_url_direct");
const elOutputAdvanced = document.body.querySelector("#output_url_advanced");
let match;
elInput.addEventListener("input", async function(evt) {
if (evt.target.value) {
if (/(.*)@(.{1,}\..{1,})/.test(evt.target.value)) {
match = evt.target.value.match(/(.*)@(.*)/);
elOutput.innerText = await utils.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 utils.computeWKDLocalPart(evt.target.value);
elOutputDirect.innerText = "Waiting for input";
elOutputAdvanced.innerText = "Waiting for input";
}
} else {
elOutput.innerText = "Waiting for input";
elOutputDirect.innerText = "Waiting for input";
elOutputAdvanced.innerText = "Waiting for input";
}
});
elInput.dispatchEvent(new Event("input"));
}
const runQRFPUtility = () => {
elUtilQRFP.onsubmit = function (evt) {
evt.preventDefault();
}
const qrTarget = document.getElementById('qrcode');
const qrContext = qrTarget.getContext('2d');
const qrOpts = {
errorCorrectionLevel: 'H',
margin: 1,
width: 256,
height: 256
};
const elInput = document.body.querySelector("#input");
elInput.addEventListener("input", async function(evt) {
if (evt.target.value) {
QRCode.toCanvas(qrTarget, evt.target.value, qrOpts, function (error) {
if (error) {
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
console.error(error);
}
});
} else {
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
}
});
elInput.dispatchEvent(new Event("input"));
}
const runQRUtility = () => {
elUtilQR.onsubmit = function (evt) {
evt.preventDefault();
}
const qrTarget = document.getElementById('qrcode');
const qrContext = qrTarget.getContext('2d');
const qrOpts = {
errorCorrectionLevel: 'L',
margin: 1,
width: 256,
height: 256
};
const elInput = document.body.querySelector("#input");
if (elInput.innerText) {
elInput.innerText = decodeURIComponent(elInput.innerText);
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;
}
});
} else {
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
}
}
const runProfileURLUtility = () => {
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 utils.generateProfileURL(data);
});
elSource.addEventListener("input", async function(evt) {
data = {
input: elInput.value,
source: elSource.value
};
elOutput.innerText = await utils.generateProfileURL(data);
});
elInput.dispatchEvent(new Event("input"));
}

134
static-src/utils.js Normal file
View file

@ -0,0 +1,134 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
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/>.
*/
import * as openpgp from 'openpgp'
import QRCode from 'qrcode'
// Compute local part of Web Key Directory URL
export async function computeWKDLocalPart(message) {
const data = openpgp.util.str_to_Uint8Array(message.toLowerCase());
const hash = await openpgp.crypto.hash.sha1(data);
return openpgp.util.encodeZBase32(hash);
}
// Generate Keyoxide profile URL
export async function generateProfileURL(data) {
let hostname = window.location.hostname;
if (data.input == "") {
return "Waiting for input...";
}
switch (data.source) {
case "wkd":
return `https://${hostname}/${data.input}`;
break;
case "hkp":
if (/.*@.*\..*/.test(data.input)) {
return `https://${hostname}/hkp/${data.input}`;
} else {
return `https://${hostname}/${data.input}`;
}
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);
return `https://${hostname}/keybase/${match[1]}/${match[2]}`;
break;
}
}
// Fetch OpenPGP key based on information stored in window
export async function fetchProfileKey() {
if (window.kx.key.object && window.kx.key.object instanceof openpgp.key.Key) {
return;
}
const rawKeyData = await fetch(window.kx.key.url)
let key, errorMsg
try {
key = (await openpgp.key.read(new Uint8Array(await rawKeyData.clone().arrayBuffer()))).keys[0]
} catch(error) {
errorMsg = error.message
}
if (!key) {
try {
key = (await openpgp.key.readArmored(await rawKeyData.clone().text())).keys[0]
} catch (error) {
errorMsg = error.message
}
}
if (key) {
window.kx.key.object = key
return
} else {
throw new Error(`Public key could not be fetched (${errorMsg})`)
}
}
// Show QR modal
export function showQR(input, type) {
const qrTarget = document.getElementById('qr');
const qrContext = qrTarget.getContext('2d');
const qrOpts = {
errorCorrectionLevel: 'L',
margin: 1,
width: 256,
height: 256
};
if (input) {
if (type === 'url') {
input = decodeURIComponent(input);
}
if (type === 'fingerprint') {
input = `OPENPGP4FPR:${input.toUpperCase()}`
}
QRCode.toCanvas(qrTarget, input, qrOpts, function(error) {
if (error) {
document.querySelector("#qr--altLink").innerText = "";
document.querySelector("#qr--altLink").href = "#";
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
console.error(error);
} else {
document.querySelector("#qr--altLink").innerText = input;
document.querySelector("#qr--altLink").href = input;
document.querySelector('#dialog--qr').showModal();
}
});
} else {
qrContext.clearRect(0, 0, qrTarget.width, qrTarget.height);
}
}

View file

@ -1,11 +1,5 @@
extends templates/base.pug
block js
script(type='application/javascript' defer src='/static/openpgp.min.js' charset='utf-8')
script(type='application/javascript' defer src='/static/doip.min.js' charset='utf-8')
script(type='application/javascript' defer src='/static/kx-claim.js' charset='utf-8')
script(type='application/javascript' defer src='/static/scripts.js' charset='utf-8')
block content
.demo
kx-claim.kx-item(data-claim= demoData data-skip="true")

View file

@ -31,18 +31,6 @@ mixin generateUser(user, isPrimary)
else
p Proof link: not accessible from browser
block js
script(type='application/javascript' src='/static/qrcode.min.js' charset='utf-8')
script(type='application/javascript' src='/static/dialog-polyfill.js' charset='utf-8')
script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
script(type='application/javascript' src='/static/doip.min.js' charset='utf-8')
script(type='application/javascript' src='/static/kx-claim.js' charset='utf-8')
script(type='application/javascript' src='/static/kx-key.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
block css
link(rel='stylesheet' href='/static/dialog-polyfill.css')
block content
script.
kx = {

View file

@ -16,8 +16,6 @@ html(lang='en')
include ../partials/footer.pug
link(rel='stylesheet' href='/static/styles.css')
link(rel='stylesheet' href='https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css' integrity='sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY=' crossorigin='anonymous')
block css
block js
link(rel='stylesheet' href='/static/main.css')
script(type='application/javascript' defer src='/static/openpgp.js' charset='utf-8')
script(type='application/javascript' defer src='/static/main.js' charset='utf-8')

View file

@ -1,9 +1,5 @@
extends ../templates/base.pug
block js
script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.util.js' charset='utf-8')
block content
section.narrow
h1 Profile URL

View file

@ -1,10 +1,5 @@
extends ../templates/base.pug
block 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.util.js' charset='utf-8')
block content
section.narrow
h1 QR Code

View file

@ -1,10 +1,5 @@
extends ../templates/base.pug
block 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.util.js' charset='utf-8')
block content
section.narrow
h1 QR Code

View file

@ -1,10 +1,5 @@
extends ../templates/base.pug
block js
script(type='application/javascript' src='/static/openpgp.min.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.js' charset='utf-8')
script(type='application/javascript' src='/static/scripts.util.js' charset='utf-8')
block content
section.narrow
h1 Web Key Directory generator

48
webpack.config.js Normal file
View file

@ -0,0 +1,48 @@
const path = require('path')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
module.exports = (env) => {
let config
if (env.static) {
config = {
mode: env.mode,
entry: {
main: {
import: './static-src/index.js',
dependOn: 'openpgp',
},
openpgp: './node_modules/openpgp/dist/openpgp.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'static'),
},
watch: env.mode == "development",
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin(),
],
}
} else {
return {}
}
if (env.mode == 'development') {
config.plugins.push(new BundleAnalyzerPlugin({
openAnalyzer: false
}))
}
return config
}

875
yarn.lock

File diff suppressed because it is too large Load diff