Compare commits

..

10 commits
main ... dev

Author SHA1 Message Date
Ty
abfa9697b1
Update to new doipjs fork 2024-06-14 23:20:19 -06:00
Tyler Beckman
353cd3b1e5
Update lockfile 2024-06-14 17:34:25 -06:00
Tyler Beckman
7b5aa4703a
Merge upstream 2024-06-02 10:26:32 -06:00
André Jaenisch
b8c94ebc0b
refactor: use domain.example for documentation
This is a reserved TLD for technical documentation.

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2024-02-13 19:44:29 +01:00
André Jaenisch
9caa1f6795
feat: update service file
Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2024-02-13 17:26:22 +01:00
André Jaenisch
ba532be4f3
feat: Add SystemD service file
I am running Keyoxide-Web on baremetal on my VPS.
I cloned the repo into `/opt/` and `adduser` a dedicated user for it.
Things you might want to adjust also is the `PORT` value.

Fixes #48.

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2024-02-13 17:24:43 +01:00
Yarmo Mackenbach
567130f634
fix: avoid shadowing escape 2024-02-13 10:05:27 +01:00
Yarmo Mackenbach
a57d24ad6a
feat: improve param escaping 2024-02-13 09:55:30 +01:00
Yarmo Mackenbach
255e99af39
feat: escape parameters 2024-02-12 10:26:24 +01:00
Ty
d34d3027ee
Update to yarn modern, patch doip for SASL auth 2023-08-03 21:09:21 -06:00
10 changed files with 8356 additions and 5675 deletions

11
.gitignore vendored
View file

@ -33,4 +33,13 @@ node_modules
ignore
dist
static
logs
logs
# yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

4
.yarnrc.yml Normal file
View file

@ -0,0 +1,4 @@
nodeLinker: node-modules
npmScopes:
myriation:
npmRegistryServer: https://git.myriation.xyz/api/packages/myriation/npm/

View file

@ -3,9 +3,9 @@ FROM node:20-alpine as builder
WORKDIR /app
COPY . .
RUN yarn --pure-lockfile
RUN yarn run build:server
RUN yarn run build:static
RUN corepack enable
RUN yarn install --immutable
RUN yarn run build:server && yarn run build:static
###

16
keyoxide-web.service Normal file
View file

@ -0,0 +1,16 @@
[Unit]
Description=Keyoxide (Online identity verification)
After=syslog.target
After=network.target
[Service]
User=keyoxide
Group=www-data
WorkingDirectory=/opt/keyoxide-web/
ExecStart=/usr/bin/node /opt/keyoxide-web/dist/index.js
Restart=always
RestartSec=2s
Environment=PORT=5000 DOMAIN=domain.example PROXY_HOSTNAME=domain.example
[Install]
WantedBy=multi-user.target

View file

@ -4,12 +4,13 @@
"description": "Verifying online identity with cryptography",
"main": "./src/index.js",
"type": "module",
"packageManager": "yarn@3.6.1",
"dependencies": {
"ajv": "^8.6.3",
"bent": "^7.3.12",
"body-parser": "^1.19.0",
"colorjs.io": "^0.4.5",
"doipjs": "^1.2.9",
"doipjs": "npm:@myriation/doipjs@1.2.9+myriation.1",
"dotenv": "^16.0.3",
"express": "^4.17.1",
"express-http-context2": "^1.0.0",

View file

@ -42,7 +42,12 @@ const opts = {
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
},
irc: {
nick: process.env.IRC_NICK || null
nick: process.env.IRC_NICK || null,
sasl: Object.keys(process.env).filter(k => k.startsWith('IRC_SASL_USERNAME_')).map(k => ({
username: process.env[k],
password: process.env[`IRC_SASL_PASSWORD_${k.substring('IRC_SASL_USERNAME_'.length)}`],
domainRegex: process.env[`IRC_SASL_DOMAIN_REGEX_${k.substring('IRC_SASL_USERNAME_'.length)}`]
}))
},
matrix: {
instance: process.env.MATRIX_INSTANCE || null,

View file

@ -32,7 +32,7 @@ import bodyParserImport from 'body-parser'
import { rateLimit } from 'express-rate-limit'
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
import { Profile } from 'doipjs'
import { generateProfileTheme, getMetaFromReq } from '../server/utils.js'
import { generateProfileTheme, getMetaFromReq, escapedParam } from '../server/utils.js'
import logger from '../log.js'
const router = express.Router()
@ -60,90 +60,112 @@ if (process.env.ENABLE_EXPERIMENTAL_RATE_LIMITER) {
{ component: 'profile_rate_limiter', action: 'start' })
}
router.get('/sig', profileRateLimiter, (req, res) => {
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
})
router.post('/sig', profileRateLimiter, bodyParser, async (req, res) => {
const data = await generateSignatureProfile(req.body.signature)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
isSignature: true,
signature: req.body.signature,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
router.get('/sig',
profileRateLimiter,
(req, res) => {
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
})
})
router.get('/wkd/:id', profileRateLimiter, async (req, res) => {
const data = await generateWKDProfile(req.params.id)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
router.post('/sig',
profileRateLimiter,
bodyParser,
async (req, res) => {
const data = await generateSignatureProfile(req.body.signature)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
isSignature: true,
signature: req.body.signature,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
})
})
})
router.get('/hkp/:id', profileRateLimiter, async (req, res) => {
const data = await generateHKPProfile(req.params.id)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
router.get('/wkd/:id',
profileRateLimiter,
escapedParam('id'),
async (req, res) => {
const data = await generateWKDProfile(req.params.id)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
})
})
})
router.get('/hkp/:server/:id', profileRateLimiter, async (req, res) => {
const data = await generateHKPProfile(req.params.id, req.params.server)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
router.get('/hkp/:id',
profileRateLimiter,
escapedParam('id'),
async (req, res) => {
const data = await generateHKPProfile(req.params.id)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
})
})
})
router.get('/keybase/:username/:fingerprint', profileRateLimiter, async (req, res) => {
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
router.get('/hkp/:server/:id',
profileRateLimiter,
escapedParam('server'),
escapedParam('id'),
async (req, res) => {
const data = await generateHKPProfile(req.params.id, req.params.server)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
})
})
})
router.get('/:id', profileRateLimiter, async (req, res) => {
const data = await generateAutoProfile(req.params.id)
const theme = generateProfileTheme(data)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
theme,
meta: getMetaFromReq(req)
router.get('/keybase/:username/:fingerprint',
profileRateLimiter,
escapedParam('username'),
escapedParam('fingerprint'),
async (req, res) => {
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
})
})
router.get('/:id',
profileRateLimiter,
escapedParam('id'),
async (req, res) => {
const data = await generateAutoProfile(req.params.id)
const theme = generateProfileTheme(data)
const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.identifier)
res.render('profile', {
title,
data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false,
enable_signature_verification: false,
theme,
meta: getMetaFromReq(req)
})
})
})
export default router

View file

@ -28,7 +28,7 @@ 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 express from 'express'
import { getMetaFromReq } from '../server/utils.js'
import { escapedParam, getMetaFromReq } from '../server/utils.js'
const router = express.Router()
@ -38,43 +38,55 @@ router.get('/', function (req, res) {
router.get('/profile-url', function (req, res) {
res.render('util/profile-url', { meta: getMetaFromReq(req) })
})
router.get('/profile-url/:input', function (req, res) {
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/profile-url/:input',
escapedParam('input'),
function (req, res) {
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qr', function (req, res) {
res.render('util/qr', { meta: getMetaFromReq(req) })
})
router.get('/qr/:input', function (req, res) {
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qr/:input',
escapedParam('input'),
function (req, res) {
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qrfp', function (req, res) {
res.render('util/qrfp', { meta: getMetaFromReq(req) })
})
router.get('/qrfp/:input', function (req, res) {
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qrfp/:input',
escapedParam('input'),
function (req, res) {
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/wkd', function (req, res) {
res.render('util/wkd', { meta: getMetaFromReq(req) })
})
router.get('/wkd/:input', function (req, res) {
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/wkd/:input',
escapedParam('input'),
function (req, res) {
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/argon2', function (req, res) {
res.render('util/argon2', { meta: getMetaFromReq(req) })
})
router.get('/argon2/:input', function (req, res) {
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/argon2/:input',
escapedParam('input'),
function (req, res) {
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/bcrypt', function (req, res) {
res.render('util/bcrypt', { meta: getMetaFromReq(req) })
})
router.get('/bcrypt/:input', function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/bcrypt/:input',
escapedParam('input'),
function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
})
export default router

View file

@ -30,6 +30,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import { webcrypto as crypto } from 'crypto'
import { Profile } from 'doipjs'
import Color from 'colorjs.io'
import { param } from 'express-validator'
export async function computeWKDLocalPart (localPart) {
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
@ -152,3 +153,26 @@ export function generateProfileTheme (/** @type {Profile} */ profile) {
}
}
}
const reEmailLike = /(<[^\s@<>]+@[^\s@<>]+>)/
export function escapedParam (/** @type {String} */ name) {
return param(name).customSanitizer(value => {
return value.split(reEmailLike).map(token => {
if (reEmailLike.test(token)) return token
return escapeString(token)
}).join('')
})
}
// Copied from https://github.com/validatorjs/validator.js/blob/b958bd7d1026a434ad3bf90064d3dcb8b775f1a9/src/lib/escapeString.js
function escapeString (/** @type {String} */ input) {
return (input.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#x27;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\//g, '&#x2F;')
.replace(/\\/g, '&#x5C;')
.replace(/`/g, '&#96;'))
}

13738
yarn.lock

File diff suppressed because it is too large Load diff