Compare commits

..

No commits in common. "dev" and "redesign-2023" have entirely different histories.

53 changed files with 5641 additions and 9136 deletions

7
.gitea/issue_template.md Normal file
View file

@ -0,0 +1,7 @@
<!-- Please search existing issues to avoid duplicates -->
<!-- If you'd like to propose a new service provider,
please do so over at https://community.keyoxide.org -->
<!-- Feel free to remove these comments -->

View file

@ -1,16 +0,0 @@
---
name: 'Bug'
about: 'Report a bug'
title: '[BUG] '
ref: 'dev'
labels:
- 'Status/Needs Triage'
- Type/Bug
---
### What happened
### Proposed solutions

View file

@ -1,8 +0,0 @@
name: Claim verification bug
about: Report a claim no longer verifying, or not verifying as it should
title: ''
body:
- type: markdown
attributes:
value: |
Please open this issue in the [doip-js repo](https://codeberg.org/keyoxide/doipjs/issues/new/choose) instead.

View file

@ -1,12 +0,0 @@
---
name: 'Enhancement'
about: 'Suggest a new feature or improve an existing one'
title: ''
ref: 'dev'
labels:
- 'Status/Needs Triage'
- Type/Enhancement
---
### Proposal

View file

@ -1,8 +0,0 @@
name: New claim
about: Suggest a new service provider or website for identity verification
title: ''
body:
- type: markdown
attributes:
value: |
Please open this issue in the [doip-js repo](https://codeberg.org/keyoxide/doipjs/issues/new/choose) instead.

11
.gitignore vendored
View file

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

View file

@ -18,9 +18,6 @@ steps:
from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web
tags: latest
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
build-tag-container:
when:
@ -35,9 +32,6 @@ steps:
from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web
auto_tag: true
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
build-dev-container:
when:
@ -51,7 +45,4 @@ steps:
password:
from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web
tags: dev
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
tags: dev

View file

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

View file

@ -6,83 +6,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [4.2.7] - 2024-02-01
### Added
- Server version HTTP endpoint
- Server version in footer
### Changed
- Update doipjs to 1.2.9
### Notes
- The version of keyoxide-web that the server is running can now be requested at the `/.well-known/keyoxide/version` HTTP endpoint. It is also found in the footer of every page.
- Version 1.2.9 of doipjs notably adds support for ORCiD claim verification, as well as a couple of performance improvements.
## [4.2.6] - 2024-01-24
### Added
- Support openpgp4fpr: queries in URL
- Proxy routes for openpgp and aspe
### Changed
- Update doipjs to 1.2.8
### Notes
- This version notably adds support for OpenPGP and ASPE claim verification.
## [4.2.5] - 2023-10-09
### Changed
- Update doipjs to 1.2.7
## [4.2.4] - 2023-10-09
### Changed
- Update doipjs to 1.2.6
## [4.2.3] - 2023-10-05
### Added
- Themeable profile pages
- Apps page
### Changed
- Update doipjs to 1.2.5
- Make Dicebear API domain configurable
### Fixed
- Catch errors potentially thrown by function
- Update JSON schemas
- Icon URL generation in profile view
### Notes
- ASP profiles use the Dicebear API to generate avatars. By default, Keyoxide
uses the official api.dicebear.com instance. To use a custom Dicebear instance,
set the DICEBEAR_API_HOSTNAME environment variable to its hostname.
## [4.2.2] - 2023-10-03
### Changed
- Update doipjs to 1.2.2
## [4.2.1] - 2023-09-23
### Fixed
- Tweak the rate limiter parameters
## [4.2.0] - 2023-09-23
### Added
- Profile request rate limiter (experimental; opt-in)
### Changed
- Update doipjs to 1.2.1
- Add logging to OpenPGP profile creation
- Add debug data to logs
### Fixed
- Make hash utils aware of ASPE
## [4.1.1] - 2023-09-21
### Changed
- Update doipjs to 1.1.0
### Fixed
- Missing rel=me for ambiguous claims
- OpenPGP cache logic
## [4.1.0] - 2023-09-18
### Changed
- Redesign
- Update doipjs to 1.0.1
- Update node to 20
- Make https scheme for proxy calls optional
- Display site version in footer
## [4.0.2] - 2023-09-12
### Fixed
- Handle doip promise rejection

View file

@ -3,20 +3,14 @@ FROM node:20-alpine as builder
WORKDIR /app
COPY . .
RUN corepack enable
RUN yarn install --immutable
RUN yarn run build:server && yarn run build:static
RUN yarn --pure-lockfile
RUN yarn run build:server
RUN yarn run build:static
###
FROM node:20-alpine
ARG CI_COMMIT_SHA
ARG CI_COMMIT_BRANCH
ENV COMMIT_SHA=$CI_COMMIT_SHA
ENV COMMIT_BRANCH=$CI_COMMIT_BRANCH
WORKDIR /app
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/dist /app/dist

View file

@ -1,15 +1,15 @@
# keyoxide-web
# Keyoxide
[![status-badge](https://ci.codeberg.org/api/badges/5919/status.svg)](https://ci.codeberg.org/repos/5919)
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=flat)](https://codeberg.org/keyoxide/keyoxide-web/src/branch/main/LICENSE)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=flat)](https://fosstodon.org/@keyoxide)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
[![Drone (self-hosted) with branch](https://img.shields.io/drone/build/keyoxide/keyoxide-web/main?server=https%3A%2F%2Fdrone.keyoxide.org&style=for-the-badge)](https://drone.keyoxide.org/keyoxide/keyoxide-web)
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=for-the-badge)](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=for-the-badge)](https://fosstodon.org/@keyoxide)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=for-the-badge)](https://opencollective.com/keyoxide)
`keyoxide-web` is the web client that powers [keyoxide.org](https://keyoxide.org) and which you can freely host on your own infrastructure.
[Keyoxide](https://keyoxide.org) is a modern, secure and decentralized platform to prove your online identity.
## Hosting keyoxide-web
## Self-hosting
Self-hosting Keyoxide is an important aspect of the project. Users need to trust the Keyoxide instance they're using to reliably verify identities. Making Keyoxide itself decentralized means no one needs to trust a central server. If a friend or family member is hosting a Keyoxide instance, it becomes much easier to trust the instance!
@ -23,44 +23,34 @@ docker run -d -p 3000:3000 codeberg.org/keyoxide/keyoxide-web:latest
Keyoxide will now be available by visiting http://localhost:3000.
More information available in the [documentation](https://docs.keyoxide.org/guides/self-hosting/).
## Local development
Install `node` in one of the following ways:
- [nix](https://nixos.org/guides/install-nix.html) with [direnv](https://direnv.net/)
- using [fnm](https://github.com/Schniz/fnm)
- using [nvm](https://github.com/nvm-sh/nvm)
- directly from their [website](https://nodejs.org/)
Install dependencies with `npm install` or `yarn`.
Run the server with `npm dev` or `yarn dev`. The Keyoxide web client will now be available at [https://localhost:3000](https://localhost:3000).
More information available in the [documentation](docs.keyoxide.org/self-hosting).
## Contributing
Anyone can contribute!
Anyone can contribute if they'd like! No need to be a programmer or technically-oriented for that matter.
Developers are invited to:
Contributing to Keyoxide can happen in many forms:
- fork the repository and play around
- submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/keyoxide-web/issues)
If you are new to contributing to open source software, we'd love to help you! To get started, here's a [list of "good first issues"](https://codeberg.org/keyoxide/keyoxide-web/issues?q=&type=all&state=open&labels=183598) that you could look into.
Everyone is invited to:
- find and [report bugs](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
- suggesting [new features](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
- [help with translations](https://translate.codeberg.org/projects/keyoxide/)
- [improve documentation](https://codeberg.org/keyoxide/keyoxide-docs)
- start using open source software and promote it
- Finding and reporting bugs
- Suggesting new features
- Improving documentation
- Writing code to fix bugs and features
- Promoting decentralized identity and web3.0
Please note that this project has a [Code of Conduct](https://codeberg.org/keyoxide/web/src/branch/main/CODE_OF_CONDUCT.md) that all contributors agree to abide when participating.
## About the Keyoxide project
### Local development
The Keyoxide project strives for a healthier internet for all and has made its efforts fully [open source](https://codeberg.org/keyoxide). Our [community](https://docs.keyoxide.org/community/) is open and welcoming, feel free to say hi!
To run Keyoxide locally on your machine for development:
Funding for the project comes from the [NLnet foundation](https://nlnet.nl/), [NGI0](https://www.ngi.eu/) and the people supporting our [OpenCollective](https://opencollective.com/keyoxide). The project is grateful for all your support.
- install either
- NodeJS
- directly from their [website](https://nodejs.org/en/), or
- using [nvm](https://github.com/nvm-sh/nvm): `nvm install --lts; nvm use --lts`
- [yarn](https://yarnpkg.com/)
- [nix](https://nixos.org/guides/install-nix.html) with
[direnv](https://direnv.net/) will install yarn and other dependencies.
- install dependencies with `npm install` or `yarn`
- run the server with `npm dev` or `yarn dev`
Keyoxide will now be available at [https://localhost:3000](https://localhost:3000)

View file

@ -1,15 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.2.2/schema.json",
"organizeImports": {
"enabled": false
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": false
}
}

View file

@ -12,6 +12,9 @@ services:
- 3000:3000
environment:
- DOMAIN=
# - KX_HIGHLIGHTS_1_NAME=
# - KX_HIGHLIGHTS_1_DESCRIPTION=
# - KX_HIGHLIGHTS_1_FINGERPRINT=
## The hostname to reach the doip_proxy container below
# - PROXY_HOSTNAME=
## The onion URL to advertise in the HTTP response headers

View file

@ -1,16 +0,0 @@
[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

@ -1,7 +1,6 @@
{
"env": {
"NODE_ENV": "development",
"LOG_LEVEL": "debug"
"NODE_ENV": "development"
},
"ext": "js,json,css,pug,md"
}

View file

@ -1,35 +1,31 @@
{
"name": "keyoxide-web",
"version": "4.2.7",
"version": "4.0.2",
"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": "npm:@myriation/doipjs@1.2.9+myriation.1",
"dialog-polyfill": "^0.5.6",
"doipjs": "^1.0.0",
"dotenv": "^16.0.3",
"express": "^4.17.1",
"express-http-context2": "^1.0.0",
"express-rate-limit": "^7.0.1",
"express-validator": "^6.13.0",
"fork-awesome": "^1.2.0",
"got": "^11.8.2",
"hash-wasm": "^4.9.0",
"jstransformer-markdown-it": "^3.0.0",
"keyv": "^4.5.0",
"libravatar": "^3.0.0",
"nanoid": "^5.0.1",
"openpgp": "^5.5.0",
"pug": "^3.0.2",
"pug": "^3.0.0",
"qrcode": "^1.4.4",
"string-replace-middleware": "^1.0.2",
"winston": "^3.8.2"
},
"devDependencies": {
"@biomejs/biome": "1.2.2",
"@vercel/ncc": "^0.34.0",
"chai": "^4.3.6",
"copy-webpack-plugin": "^11.0.0",
@ -38,7 +34,8 @@
"license-check-and-add": "^4.0.5",
"mini-css-extract-plugin": "^2.5.3",
"mocha": "^10.1.0",
"nodemon": "^3.0.3",
"nodemon": "^2.0.20",
"rome": "^12.1",
"sass": "^1.67.0",
"sass-loader": "^13.3.2",
"standard": "^17.0.0",
@ -51,19 +48,19 @@
"start": "node ./",
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev",
"test": "yarn run lint && mocha",
"watch": "nodemon --config nodemon.json ./",
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
"build": "yarn run build:server && yarn run build:static",
"build:server": "ncc build ./src/index.js -o dist",
"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",
"lint": "yarn run standard:check && yarn run biome:check",
"biome:check": "biome check ./src && biome lint ./src",
"biome:fix": "biome check --apply ./src && biome lint --apply ./src",
"standard:check": "standard ./src",
"standard:fix": "standard --fix ./src",
"license:check": "license-check-and-add check",
"license:add": "license-check-and-add add",
"license:remove": "license-check-and-add remove"
"lint": "yarn run standard:check && yarn run rome:check",
"standard:check": "./node_modules/.bin/standard ./src",
"standard:fix": "./node_modules/.bin/standard --fix ./src",
"rome:check": "./node_modules/.bin/rome check ./src",
"rome:fix": "./node_modules/.bin/rome check --apply ./src",
"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"
},
"repository": {
"type": "git",

View file

@ -128,13 +128,7 @@ router.get('/fetch',
data = await doVerification(data)
}
try {
data = data.toJSON()
} catch (error) {
data = {
errors: [error.message]
}
}
data = data.toJSON()
try {
// Validate JSON
@ -168,13 +162,7 @@ router.get('/verify',
// Do verification
let data = await doVerification(profile)
try {
data = data.toJSON()
} catch (error) {
data = {
errors: [error.message]
}
}
data = data.toJSON()
try {
// Validate JSON

View file

@ -42,12 +42,7 @@ const opts = {
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
},
irc: {
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)}`]
}))
nick: process.env.IRC_NICK || null
},
matrix: {
instance: process.env.MATRIX_INSTANCE || null,
@ -97,40 +92,6 @@ router.get(
}
)
// ASPE route
router.get('/aspe', query('aspeUri').isString(), (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.aspe
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
})
// OpenPGP route
router.get('/openpgp', query('url').isURL(), query('protocol').isString(), (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.openpgp
.fn(req.query, opts)
.then((data) => {
return res.status(200).send(data)
})
.catch((err) => {
return res.status(400).json({ errors: err.message ? err.message : err })
})
})
// DNS route
router.get('/dns', query('domain').isFQDN(), (req, res) => {
const errors = validationResult(req)

View file

@ -28,10 +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 * as httpContext from 'express-http-context2'
import { nanoid } from 'nanoid'
import { readFileSync } from 'fs'
import { execSync } from 'child_process'
import { stringReplace } from 'string-replace-middleware'
import * as pug from 'pug'
import * as dotenv from 'dotenv'
@ -44,20 +41,6 @@ import staticRoute from './routes/static.js'
import utilRoute from './routes/util.js'
dotenv.config()
// Get information about the last git commit
let gitBranch = process.env.CI_COMMIT_BRANCH ?? process.env.COMMIT_BRANCH
if (!gitBranch) {
try {
gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
} catch (_) {}
}
let gitHash = process.env.CI_COMMIT_SHA ?? process.env.COMMIT_SHA
if (!gitHash) {
try {
gitHash = execSync('git rev-parse HEAD').toString().trim()
} catch (_) {}
}
const app = express()
const packageData = JSON.parse(readFileSync('./package.json'))
@ -66,23 +49,12 @@ app.engine('pug', pug.__express).set('view engine', 'pug')
app.set('port', process.env.PORT || 3000)
app.set('domain', process.env.DOMAIN)
app.set('scheme', process.env.SCHEME || 'https')
app.set('keyoxide_name', 'keyoxide-web')
app.set('keyoxide_version', packageData.version)
app.set('git_branch', gitBranch)
app.set('git_hash', gitHash)
app.set('onion_url', process.env.ONION_URL)
// Middlewares
app.use(httpContext.middleware)
app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'interest-cohort=()')
httpContext.set('requestId', nanoid())
httpContext.set('requestPath', req.path)
httpContext.set('requestIp', req.ip)
logger.info('Handle a request',
{ component: 'http_server', action: 'request' })
next()
})

View file

@ -28,7 +28,6 @@ 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 { createLogger, format, transports } from 'winston'
import * as httpContext from 'express-http-context2'
import * as dotenv from 'dotenv'
dotenv.config()
@ -38,23 +37,13 @@ const anonymize = format((info, opts) => {
info.keyserver_domain = undefined
info.username = undefined
info.fingerprint = undefined
info.request_path = undefined
info.request_ip = undefined
}
return info
})
const addRequestData = format((info, opts) => {
if (httpContext.get('requestId')) info.request_id = httpContext.get('requestId')
if (httpContext.get('requestPath')) info.request_path = httpContext.get('requestPath')
if (httpContext.get('requestIp')) info.request_ip = httpContext.get('requestIp')
return info
})
const logger = createLogger({
level: process.env.LOG_LEVEL || 'info',
format: format.combine(
addRequestData(),
anonymize(),
format.timestamp(),
format.json()

View file

@ -36,11 +36,19 @@ const router = express.Router()
const md = markdownImport({ typographer: true })
router.get('/', (req, res) => {
res.render('index', { meta: getMetaFromReq(req) })
})
const highlights = []
for (let index = 1; index < 4; index++) {
if (process.env[`KX_HIGHLIGHTS_${index}_NAME`] &&
process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) {
highlights.push({
name: process.env[`KX_HIGHLIGHTS_${index}_NAME`],
description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`],
fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]
})
}
}
router.get('/apps', (req, res) => {
res.render('apps', { title: 'Apps', meta: getMetaFromReq(req) })
res.render('index', { highlights, meta: getMetaFromReq(req) })
})
router.get('/privacy', (req, res) => {
@ -67,12 +75,6 @@ router.get('/.well-known/webfinger', (req, res) => {
res.json(body)
})
router.get('/.well-known/keyoxide/version', async (req, res) => {
// TODO Support responding with JSON object when requested
const meta = getMetaFromReq(req)
return res.status(200).contentType('text/plain').send(meta.keyoxide.semver)
})
router.get('/users/keyoxide', (req, res) => {
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')

View file

@ -29,143 +29,95 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/
import express from 'express'
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, escapedParam } from '../server/utils.js'
import logger from '../log.js'
import { getMetaFromReq } from '../server/utils.js'
const router = express.Router()
const bodyParser = bodyParserImport.urlencoded({ extended: false })
let profileRateLimiter = (req, res, next) => {
next()
}
router.get('/sig', (req, res) => {
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
})
if (process.env.ENABLE_EXPERIMENTAL_RATE_LIMITER) {
profileRateLimiter = rateLimit({
windowMs: 5000,
limit: 20,
standardHeaders: 'draft-7',
legacyHeaders: false,
handler: (req, res, next, options) => {
logger.debug('Rate-limiting a profile request',
{ component: 'profile_rate_limiter', action: 'block' })
res.status(options.statusCode).render('429', { meta: getMetaFromReq(req) })
}
router.post('/sig', 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)
})
})
logger.debug('Starting the profile request 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.get('/wkd/: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.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', 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', 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', 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('/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)
})
router.get('/:id', async (req, res) => {
const data = await generateAutoProfile(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)
})
})
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 { escapedParam, getMetaFromReq } from '../server/utils.js'
import { getMetaFromReq } from '../server/utils.js'
const router = express.Router()
@ -38,55 +38,43 @@ 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',
escapedParam('input'),
function (req, res) {
res.render('util/profile-url', { input: req.params.input, 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('/qr', function (req, res) {
res.render('util/qr', { 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('/qr/: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',
escapedParam('input'),
function (req, res) {
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qrfp/: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',
escapedParam('input'),
function (req, res) {
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/wkd/: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',
escapedParam('input'),
function (req, res) {
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/argon2/: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',
escapedParam('input'),
function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/bcrypt/:input', function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
})
export default router

View file

@ -157,10 +157,6 @@ export const personaSchema = {
description: 'URL to an avatar image',
type: ['string', 'null']
},
themeColor: {
description: 'Profile page theme color',
type: ['string', 'null']
},
isRevoked: {
type: 'boolean'
},
@ -219,25 +215,17 @@ export const claimSchema = {
display: {
type: 'object',
properties: {
profileName: {
name: {
type: 'string',
description: 'Account name to display in the user interface'
},
profileUrl: {
url: {
type: ['string', 'null'],
description: 'Profile URL to link to in the user interface'
},
proofUrl: {
type: ['string', 'null'],
description: 'Proof URL to link to in the user interface'
description: 'URL to link to in the user interface'
},
serviceProviderName: {
type: ['string', 'null'],
description: 'Name of the service provider to display in the user interface'
},
serviceProviderId: {
type: ['string', 'null'],
description: 'Id of the service provider'
}
}
}

View file

@ -38,9 +38,7 @@ const generateAspeProfile = async (id) => {
return doipjs.asp.fetchASPE(id)
.then(profile => {
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
}
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
profile = processAspProfile(profile)
return profile
})
@ -60,9 +58,7 @@ const generateWKDProfile = async (id) => {
return fetchWKD(id)
.then(async profile => {
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
}
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
profile = processOpenPgpProfile(profile)
logger.debug('Generating a WKD profile',
@ -93,9 +89,7 @@ const generateHKPProfile = async (id, keyserverDomain) => {
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
}
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', keyoxideUrl)
}
profile.addVerifier('keyoxide', keyoxideUrl)
profile = processOpenPgpProfile(profile)
logger.debug('Generating a HKP profile',
@ -117,7 +111,6 @@ const generateAutoProfile = async (id) => {
let result
const aspeRe = /aspe:(.*):(.*)/
const openpgpRe = /openpgp4fpr:(.*)/
if (aspeRe.test(id)) {
result = await generateAspeProfile(id)
@ -127,15 +120,6 @@ const generateAutoProfile = async (id) => {
}
}
if (openpgpRe.test(id)) {
const match = id.match(openpgpRe)
result = await generateHKPProfile(match[1])
if (result && !('errors' in result)) {
return result
}
}
if (id.includes('@')) {
result = await generateWKDProfile(id)
@ -184,9 +168,7 @@ const generateKeybaseProfile = async (username, fingerprint) => {
return fetchKeybase(username, fingerprint)
.then(async profile => {
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
}
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
profile = processOpenPgpProfile(profile)
logger.debug('Generating a Keybase profile',
@ -233,8 +215,7 @@ const processAspProfile = async (/** @type {import('doipjs').Profile */ profile)
// Overwrite avatarUrl
// TODO: don't overwrite avatarUrl once it's fully supported
profile.personas[profile.primaryPersonaIndex].avatarUrl =
`https://${process.env.DICEBEAR_API_HOSTNAME || 'api.dicebear.com'}/7.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
profile.personas[profile.primaryPersonaIndex].avatarUrl = `https://api.dicebear.com/6.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
return profile
}

View file

@ -27,7 +27,6 @@ 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 logger from '../log.js'
import got from 'got'
import * as doipjs from 'doipjs'
import { readKey } from 'openpgp'
@ -35,20 +34,11 @@ import { computeWKDLocalPart } from './utils.js'
import { createHash } from 'crypto'
import Keyv from 'keyv'
let c = null
if (process.env.ENABLE_EXPERIMENTAL_CACHE) {
c = new Keyv()
logger.debug('OpenPGP cache started',
{ component: 'openpgp_cache', action: 'start' })
}
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
const fetchWKD = (id) => {
return new Promise((resolve, reject) => {
(async () => {
logger.debug('Fetching an OpenPGP profile via WKD',
{ component: 'wkd_profile_fetcher', action: 'start', profile_id: id })
let publicKey = null
let profile = null
let fetchURL = null
@ -68,12 +58,7 @@ const fetchWKD = (id) => {
const hash = createHash('md5').update(id).digest('hex')
if (c && await c.get(hash)) {
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
logger.debug('WKD profile retrieved from OpenPGP cache',
{ component: 'openpgp_cache', action: 'retrieve_wkd' })
return resolve(profile)
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
}
if (!profile) {
@ -86,10 +71,7 @@ const fetchWKD = (id) => {
return null
}
})
} catch (errorAdvanced) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (advanced URL)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorAdvanced.message })
} catch (e) {
try {
plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) {
@ -99,10 +81,7 @@ const fetchWKD = (id) => {
return null
}
})
} catch (errorDirect) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (direct URL)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorDirect.message })
} catch (error) {
return reject(new Error('No public keys could be fetched using WKD'))
}
}
@ -116,9 +95,6 @@ const fetchWKD = (id) => {
binaryKey: plaintext
})
} catch (error) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (reading key)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
return reject(new Error('No public keys could be read from the data fetched using WKD'))
}
@ -129,9 +105,6 @@ const fetchWKD = (id) => {
try {
profile = await doipjs.openpgp.parsePublicKey(publicKey)
} catch (error) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (parsing key)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
return reject(new Error('No public keys could be fetched using WKD'))
}
@ -142,14 +115,8 @@ const fetchWKD = (id) => {
if (c && plaintext instanceof Uint8Array) {
await c.set(hash, JSON.stringify(profile), 60 * 1000)
logger.debug('WKD profile stored in OpenPGP cache',
{ component: 'openpgp_cache', action: 'store_wkd' })
}
logger.debug('Fetched an OpenPGP profile via WKD',
{ component: 'wkd_profile_fetcher', action: 'done', profile_id: id })
resolve(profile)
})()
})
@ -158,9 +125,6 @@ const fetchWKD = (id) => {
const fetchHKP = (id, keyserverDomain) => {
return new Promise((resolve, reject) => {
(async () => {
logger.debug('Fetching an OpenPGP profile via HKP',
{ component: 'hkp_profile_fetcher', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
let profile = null
let fetchURL = null
@ -183,21 +147,13 @@ const fetchHKP = (id, keyserverDomain) => {
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
if (c && await c.get(hash)) {
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
logger.debug('HKP profile retrieved from OpenPGP cache',
{ component: 'openpgp_cache', action: 'store_hkp' })
return resolve(profile)
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
}
if (!profile) {
try {
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
} catch (error) {
logger.debug('Failed to fetch an OpenPGP profile via HKP',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, keyserver_domain: keyserverDomain || '', error: error.message })
profile = null
}
}
@ -212,14 +168,8 @@ const fetchHKP = (id, keyserverDomain) => {
if (c && profile instanceof doipjs.Profile) {
await c.set(hash, JSON.stringify(profile), 60 * 1000)
logger.debug('HKP profile stored in OpenPGP cache',
{ component: 'openpgp_cache', action: 'store_hkp' })
}
logger.debug('Fetched an OpenPGP profile via HKP',
{ component: 'hkp_profile_fetcher', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
resolve(profile)
})()
})

View file

@ -28,9 +28,6 @@ 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 { 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())
@ -83,96 +80,10 @@ export function encodeZBase32 (data) {
}
export function getMetaFromReq (req) {
const versionDetails = (req.app.get('git_hash'))
? `+${req.app.get('git_hash').substring(0, 10)}`
: ''
const semver = `${req.app.get('keyoxide_name')}/${req.app.get('keyoxide_version')}${versionDetails}`
const sourceUrl = req.app.get('git_hash')
? `https://codeberg.org/keyoxide/keyoxide-web/src/commit/${req.app.get('git_hash')}`
: 'https://codeberg.org/keyoxide/keyoxide-web'
return {
env: req.app.get('env'),
keyoxide: {
name: req.app.get('keyoxide_name'),
version: req.app.get('keyoxide_version'),
branch: req.app.get('git_branch'),
hash: req.app.get('git_hash'),
semver,
sourceUrl
version: req.app.get('keyoxide_version')
}
}
}
export function generateProfileTheme (/** @type {Profile} */ profile) {
if (!(profile && profile instanceof Profile)) return null
if (!profile.personas[profile.primaryPersonaIndex].themeColor) return null
let base
try {
base = new Color(profile.personas[profile.primaryPersonaIndex].themeColor)
} catch (_) {
return null
}
if (base.to('hsl').hsl[0].isNaN) return null
if (base.to('hsl').hsl[2] === 0) return null
const primaryLight = base.to('hsl')
primaryLight.hsl[2] = 40
const primaryDark = base.to('hsl')
primaryDark.hsl[2] = 80
const primarySubtleLight = base.to('hsl')
primarySubtleLight.hsl[2] = 50
const primarySubtleDark = base.to('hsl')
primarySubtleDark.hsl[2] = 70
const backgroundLight = base.to('hsl')
backgroundLight.hsl[2] = 98
const backgroundDark = base.to('hsl')
backgroundDark.hsl[1] = 20
backgroundDark.hsl[2] = 5
return {
base: base.toString({ format: 'hex' }),
primary: {
light: primaryLight.toString(),
dark: primaryDark.toString()
},
primarySubtle: {
light: primarySubtleLight.toString(),
dark: primarySubtleDark.toString()
},
background: {
light: backgroundLight.toString(),
dark: backgroundDark.toString()
}
}
}
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;'))
}

View file

@ -1,7 +0,0 @@
<svg role="image" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m14.662 2.7734c-0.86914 8.5e-6 -1.5742 0.70509-1.5742 1.5742 0 0.52528 0.26095 1.006 0.67773 1.2949 0.12007 0.083138 0.18912 0.092562 0.20508 0.22266 0.01604 0.13001-0.16211 0.25-0.16211 0.25l-3.3242 2.5859v-0.5293c0.14724-1.4561-0.83223-2.9405-2.2754-3.2793-1.719-0.502-3.7931 1.0298-3.7931 2.6452 0 1.9182 0.00778 8.9247 0.00211 13.429-0.00211 1.6772 1.3577 3.0332 3.0332 3.0332s3.0352-1.3576 3.0352-3.0332v-0.39648c1.365 0.90898 2.7303 1.8174 4.0938 2.7285 1.1299 0.93021 2.9078 0.93859 3.9902-0.07422 1.3712-1.1518 1.323-3.4999-0.0918-4.5957-1.935-1.3219-3.8897-2.6181-5.8418-3.916l5.7734-4.4883c1.1632-0.90466 1.4954-2.5005 0.83398-3.7832-0.04938-0.095755-0.06511-0.12998-0.16797-0.16992-0.10277-0.039938-0.18425 0.00439-0.28711 0.042969-0.14138 0.053062-0.29257 0.080078-0.44531 0.080078-0.69894 0-1.2656-0.56662-1.2656-1.2656 9.4e-5 -0.094853 0.01636-0.14996-0.01172-0.20898-0.02807-0.059023-0.10233-0.097092-0.17578-0.10547-0.12191-0.013926-0.24437-0.020476-0.36719-0.019531-0.12049 8.587e-4 -0.21169-0.00952-0.26367-0.097656-0.05189-0.088142-0.02344-0.18975-0.02344-0.34961 0-0.86915-0.70504-1.5742-1.5742-1.5742z"/>
<path d="m12.806 3.085a1.0735 1.0735 0 0 1-1.0735 1.0735 1.0735 1.0735 0 0 1-1.0735-1.0735 1.0735 1.0735 0 0 1 1.0735-1.0735 1.0735 1.0735 0 0 1 1.0735 1.0735z"/>
<path d="m13.458 1.0033a0.70038 0.70038 0 0 1-0.70038 0.70038 0.70038 0.70038 0 0 1-0.70038-0.70038 0.70038 0.70038 0 0 1 0.70038-0.70038 0.70038 0.70038 0 0 1 0.70038 0.70038z"/>
<path d="m11.339 0.48902a0.48902 0.48902 0 0 1-0.48902 0.48902 0.48902 0.48902 0 0 1-0.48902-0.48902 0.48902 0.48902 0 0 1 0.48902-0.48902 0.48902 0.48902 0 0 1 0.48902 0.48902z"/>
<path d="m19.203 5.1296a0.85797 0.85797 0 0 1-0.85797 0.85797 0.85797 0.85797 0 0 1-0.85797-0.85797 0.85797 0.85797 0 0 1 0.85797-0.85797 0.85797 0.85797 0 0 1 0.85797 0.85797z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View file

@ -1,3 +0,0 @@
<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m10.89 0c-0.5512 2.702e-7 -1.008 0.4571-1.008 1.008 0 0.5512 0.4571 1.008 1.008 1.008 0.2431 0 0.4661-0.0887 0.6414-0.2354 0.01238 0.05092 0.0265 0.1007 0.04512 0.1491-0.7892 0.07774-1.414 0.7551-1.414 1.563 0 0.8594 0.706 1.563 1.565 1.563 0.2774 0 0.5392-0.07366 0.767-0.202 0.05714 0.4898 0.3658 0.8914 0.716 1.232l-2.219 1.72c-0.1251-1.779-1.54-3.225-3.35-3.225-1.891 0-3.441 1.542-3.441 3.433v6.724c-3.902e-4 -0.0156 3.8e-6 -0.026 0 0.02942v5.791c0 1.893 1.548 3.441 3.441 3.441 1.699 0 2.96-1.32 3.233-2.944l3.545 2.366c1.574 1.049 3.718 0.6164 4.767-0.9573 1.049-1.574 0.6242-3.718-0.9494-4.767l-4.716-3.137 4.916-3.825c1.293-1.006 1.683-2.776 0.9828-4.217 0.2337-0.2976 0.3747-0.6704 0.3747-1.075 0-0.9606-0.7891-1.75-1.75-1.75-0.6534 1e-7 -1.228 0.3646-1.528 0.9004-0.01712-6.411e-4 -0.03382-0.00323-0.051-0.00391-0.08719-1.045-0.87-1.942-1.936-1.942-0.4834 4.6e-6 -0.9293 0.1707-1.281 0.4551-0.04123-0.1579-0.1067-0.3062-0.1922-0.4414 0.4963-0.1534 0.8651-0.622 0.8651-1.163 0-0.6626-0.5516-1.208-1.214-1.208-0.3405 0-0.6503 0.1447-0.8709 0.3747-0.1427-0.3849-0.5149-0.665-0.9455-0.665zm0 0.9357c0.03545 0 0.07258 0.03713 0.07258 0.07258 0 0.03545-0.03712 0.07454-0.07258 0.07454-0.03545 0-0.08239-0.03909-0.08239-0.07454 0-0.03545 0.04694-0.07258 0.08239-0.07258zm1.816 0.4374c0.076 0 0.1255 0.04954 0.1255 0.1255 0 0.076-0.04954 0.1255-0.1255 0.1255-0.076 0-0.1275-0.04954-0.1275-0.1255 0-0.076 0.05151-0.1255 0.1275-0.1255zm-0.9788 1.638c0.2727 1e-7 0.4806 0.2098 0.4806 0.4826 0 0.2727-0.2078 0.4806-0.4806 0.4806-0.2727 0-0.4826-0.2078-0.4826-0.4806 0-0.2727 0.2098-0.4826 0.4826-0.4826zm2.801 0.7258c0.5365-1.55e-5 0.9573 0.4208 0.9573 0.9573 0 0.0449-0.0071 0.09728-0.0079 0.1922-7.64e-4 0.09495 0.01177 0.2592 0.104 0.4159 0.09067 0.1538 0.2755 0.2826 0.4159 0.3256 0.1403 0.04299 0.2351 0.03781 0.3099 0.03728 0.1106 0.8569 0.8508 1.528 1.736 1.528 0.1167 3e-7 0.2308-0.01313 0.3413-0.0353 0.3731 0.951 0.2233 2.066-0.6159 2.719l-5.508 4.284a0.5428 0.5428 0 0 0 0.02942 0.8827l5.337 3.558c1.085 0.7232 1.376 2.177 0.6532 3.262-0.7232 1.085-2.177 1.376-3.262 0.6532l-4.182-2.787a0.5428 0.5428 0 0 0-0.8376 0.4512v0.3786c0 1.306-1.051 2.358-2.358 2.358-1.306 0-2.35-1.051-2.35-2.358v-5.791c-3e-6 0.04601-8.017e-4 0.04336 0-0.02156a0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3 0.5428 0.5428 0 0 0 0-2e-3v-6.724c0-1.304 1.046-2.35 2.35-2.35 1.304 0 2.35 1.046 2.35 2.35v0.8376a0.5428 0.5428 0 0 0 0.8749 0.4296l3.182-2.468-0.02942 0.01374s0.08749-0.05788 0.1765-0.155c0.08906-0.09708 0.2481-0.2676 0.2079-0.5924-0.01894-0.1544-0.1333-0.3798-0.2452-0.4747-0.1119-0.0949-0.1512-0.1021-0.1765-0.1197-0.2538-0.1759-0.4159-0.4705-0.4159-0.7925 0-0.5365 0.4266-0.9572 0.9631-0.9573zm3.515 1.038c0.374 0 0.6669 0.2949 0.6669 0.6689 0 0.2634-0.1462 0.4853-0.3629 0.5944-5.26e-4 2.647e-4 -0.0014-2.637e-4 -2e-3 0-0.03386 0.01188-0.0579 0.02194-0.07258 0.02744-0.03712 0.01393-0.0737 0.0257-0.1118 0.03337-0.03811 0.00763-0.07742 0.01177-0.1177 0.01177-0.04676 0-0.09349-0.00497-0.1373-0.01374-0.04382-0.00873-0.0855-0.02255-0.1255-0.03923-0.04004-0.01666-0.07833-0.03705-0.1138-0.06081-0.03545-0.02376-0.06807-0.05041-0.09807-0.08042-0.03003-0.03003-0.05667-0.06264-0.08042-0.09808-0.02376-0.03545-0.04415-0.07373-0.06081-0.1138-0.01667-0.04004-0.03048-0.08173-0.03923-0.1255-0.0087-0.04381-0.01177-0.0886-0.01177-0.1354 1e-5 -0.01184 0.0038-0.04005 0.0059-0.0922 2.62e-4 -0.00664-1.63e-4 -0.00398 0-0.01177 0.04808-0.3236 0.3221-0.5649 0.6611-0.5649z"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -45,7 +45,7 @@ export class Claim extends HTMLElement {
}
async verify() {
const claim = doipjs.Claim.fromJSON(JSON.parse(this.getAttribute('data-claim')));
const claim = doipjs.Claim.fromJson(JSON.parse(this.getAttribute('data-claim')));
await claim.verify({
proxy: {
policy: 'adaptive',
@ -59,23 +59,25 @@ export class Claim extends HTMLElement {
updateContent(value) {
const root = this;
const claimJson = JSON.parse(value);
const claim = doipjs.Claim.fromJSON(claimJson);
const claim = doipjs.Claim.fromJson(claimJson);
root.querySelector('.info .title').innerText = claimJson.display.profileName;
console.log(claimJson);
root.querySelector('.info .title').innerText = claimJson.display.name;
root.querySelector('.info .subtitle').innerText = claimJson.display.serviceProviderName ??
(claim.status < 300 ? '???' : '---');
root.querySelector('.info img').setAttribute('src',
`https://design.keyoxide.org/brands/service-providers/${claimJson.display.serviceProviderId
? claimJson.display.serviceProviderId : '_'}/icon.svg`);
try {
if (claim.status >= 200) {
root.setAttribute('data-status', claim.status < 300 ? 'success' : 'failed');
// root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.status < 300 ? 'success' : 'failed');
} else {
root.setAttribute('data-status', 'running');
// root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
}
} catch (error) {
root.setAttribute('data-status', 'failed');
// root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed');
}
const elContent = root.querySelector('.content');
@ -108,15 +110,15 @@ export class Claim extends HTMLElement {
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
if (claimJson.display.profileUrl) {
profile_link.innerHTML = `Profile link: <a rel="me" href="${claimJson.display.profileUrl}" aria-label="link to profile">${claimJson.display.profileUrl}</a>`;
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 (claimJson.display.proofUrl) {
proof_link.innerHTML = `Proof link: <a href="${claimJson.display.proofUrl}" aria-label="link to profile">${claimJson.display.proofUrl}</a>`;
if (claim.matches[0].proof.request.uri) {
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.request.uri}" aria-label="link to profile">${claim.matches[0].proof.request.uri}</a>`;
} else {
proof_link.innerHTML = `Proof link: not accessible from browser`;
}

View file

@ -58,9 +58,8 @@ kx-claim {
}
hr {
margin: 8px 0;
border: none;
border-top: 2px solid var(--header-background-color);
border-top: 2px solid var(--background-color);
}
.content {
@ -94,9 +93,6 @@ kx-claim {
}
.info {
display: flex;
align-items: baseline;
gap: 8px;
flex: 1;
}
@ -104,17 +100,6 @@ kx-claim {
color: var(--text-color);
}
.info img {
width: 16px;
height: 16px;
opacity: 0.5;
transform: translateY(3px);
@media (prefers-color-scheme: dark) {
filter: invert(1);
}
}
.claim__links {
p {
@ -214,15 +199,15 @@ kx-claim {
text-transform: uppercase;
color: var(--button-text-color);
background-color: var(--button-background-color);
border: solid 1px var(--button-border-color);
border: solid 2px var(--button-border-color);
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: var(--button-background-color-hover);
border-color: var(--button-border-color-hover);
color: var(--button-text-color-hover);
background-color: var(--button-hover-background-color);
border-color: var(--button-hover-border-color);
color: var(--button-hover-text-color);
}
&[data-status="running"] {

View file

@ -32,12 +32,12 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
@use './styles/typography.scss';
@use './styles/forms.scss';
@import '../node_modules/fork-awesome/css/fork-awesome.css';
@import '../node_modules/dialog-polyfill/dist/dialog-polyfill.css';
* {
box-sizing: border-box;
}
:focus {
z-index: 100;
}
/* HELPERS */
.spacer {
@ -72,16 +72,14 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
/* DIALOGS */
dialog {
max-width: 480px;
width: 100% !important;
max-width: 800px !important;
padding: 0 !important;
word-wrap: anywhere;
background-color: var(--section-background-color);
border: 0;
border-radius: 16px;
}
&::backdrop {
background-color: #000;
opacity: 0.8;
}
dialog>div {
padding: 1em;
}
dialog form[method="Dialog"] {

View file

@ -73,8 +73,7 @@ form textarea {
margin: 8px 0;
}
input,
textarea {
input {
color: var(--input-text-color);
background-color: var(--input-background-color);
border: solid 1px var(--input-border-color);
@ -108,22 +107,6 @@ a.button {
border-color: var(--button-border-color-hover);
color: var(--button-text-color-hover);
}
&.themed {
padding: 8px 12px;
color: var(--text-color-inverse);
background-color: var(--primary-color);
border: 0;
&:hover {
color: var(--text-color-inverse);
background-color: var(--primary-color-subtle);
}
}
}
button {
margin-right: 8px;
}
button.inline {
@ -132,21 +115,15 @@ button.inline {
padding: 2px 8px;
}
#search {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50vh;
gap: 48px;
background: transparent;
border: 0;
& > svg {
width: 96px;
fill: var(--primary-color);
}
form {
display: flex;
flex-direction: row;

View file

@ -27,10 +27,6 @@ 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/>.
*/
hr {
margin: 3em 0;
color: var(--line-color-subtle);
}
body {
display: flex;
flex-direction: column;
@ -60,12 +56,14 @@ header {
}
a.logo {
flex: 1;
display: flex;
align-items: center;
gap: 16px;
font-size: 1.8rem;
text-decoration: none;
font-weight: bold;
color: var(--primary-color);
@media screen and (max-width: 480px) {
gap: 8px;
@ -83,7 +81,7 @@ header {
}
}
a {
a.text {
margin: 0;
color: var(--text-color);
@ -192,29 +190,4 @@ section {
vertical-align: sub;
}
}
}
.screenshots {
display: flex;
gap: 16px;
width: 100%;
padding: 8px;
background-color: #fafafa;
overflow-y: scroll;
img {
height: 400px;
}
}
.banners {
display: flex;
flex-wrap: wrap;
gap: 8px;
a {
img {
height: 32px;
}
}
}

View file

@ -35,7 +35,7 @@ h1 {
cursor: default;
}
h2 {
margin: 1em 0 0.5em;
margin: 2em 0 0.5em;
font-size: 1.2em;
font-weight: bold;
color: var(--h2-color);

View file

@ -64,25 +64,14 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
}
:root {
--primary-color-light: var(--purple-700);
--primary-color-dark: var(--purple-300);
--primary-color: var(--purple-700);
--primary-color-subtle: var(--purple-400);
--primary-color-subtle-light: var(--purple-400);
--primary-color-subtle-dark: var(--purple-500);
--background-color-light: #fafafa;
--background-color-dark: #0a0a0a;
}
:root {
--primary-color: var(--primary-color-light);
--primary-color-subtle: var(--primary-color-subtle-light);
--body-background-color: var(--background-color-light);
--body-background-color: #fafafa;
--section-background-color: var(--white);
--text-color: var(--grey-800);
--text-color-subtle: var(--grey-500);
--text-color-inverse: var(--white);
--h1-color: var(--text-color);
--h2-color: var(--text-color);
--h2-small-color: var(--primary-color-subtle);
@ -93,9 +82,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
--link-color: var(--blue-700);
--link-color-subtle: var(--text-color);
--link-color-hover: var(--primary-color);
--line-color-subtle: var(--grey-200);
--link-color-hover: var(--purple-700);
--button-text-color: var(--text-color);
--button-text-color-hover: var(--text-color);
@ -114,17 +101,14 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
--footer-text-color: var(--text-color-subtle);
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--primary-color: var(--purple-300);
--primary-color-subtle: var(--purple-500);
--primary-color: var(--primary-color-dark);
--primary-color-subtle: var(--primary-color-subtle-dark);
--body-background-color: var(--background-color-dark);
--body-background-color: #0a0a0a;
--section-background-color: var(--grey-900);
--text-color: var(--grey-50);
--text-color-subtle: var(--grey-300);
--text-color-inverse: var(--grey-800);
--h1-color: var(--text-color);
--h2-color: var(--text-color);
--h2-small-color: var(--primary-color-subtle);
@ -137,8 +121,6 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
--link-color-subtle: var(--text-color);
--link-color-hover: var(--primary-color);
--line-color-subtle: var(--grey-700);
--button-text-color: var(--text-color);
--button-text-color-hover: var(--text-color);
--button-border-color: var(--grey-700);

View file

@ -27,6 +27,7 @@ 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.js'
@ -50,6 +51,17 @@ const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-
// 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()
@ -337,8 +349,8 @@ const runArgon2GenerationUtility = () => {
elFeedback.innerHTML = "";
} else {
let feedbackContent = "";
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
}
if (!(elInput.value === elInput.value.toLowerCase())) {
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
@ -379,7 +391,7 @@ window.kx__fixArgon2Input = () => {
const elInput = document.querySelector('#form-util-argon2-generate .input');
elInput.value = elInput.value.toLowerCase();
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
elInput.value = `openpgp4fpr:${elInput.value}`;
}
@ -402,8 +414,8 @@ const runBcryptGenerationUtility = () => {
elFeedback.innerHTML = "";
} else {
let feedbackContent = "";
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
}
if (!(elInput.value === elInput.value.toLowerCase())) {
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
@ -444,7 +456,7 @@ window.kx__fixBcryptInput = () => {
const elInput = document.querySelector('#form-util-bcrypt-generate .input');
elInput.value = elInput.value.toLowerCase();
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
elInput.value = `openpgp4fpr:${elInput.value}`;
}

View file

@ -12,22 +12,28 @@
# $ while read -r line; do echo -nE "$line\n" ; done < public.pem > public-oneline.pem
#ACTIVITYPUB_PUBLIC_KEY=
# Domain for Keyoxide Proxy server
# To host a Keyoxide Proxy server, refer to https://docs.keyoxide.org/self-hosting/
# Domain for DOIP Proxy server
# Source code for the server can be found here https://codeberg.org/keyoxide/doip-proxy
#PROXY_HOSTNAME=
# Domain for Dicebear API server
# Defaults to: api.dicebear.com
#DICEBEAR_API_HOSTNAME=
# Tor Onion URL
# The full http:// onion url to add as an 'Onion-Location' header
#ONION_URL=
# Highlights
# Highlight up to three profiles on the homepage
#KX_HIGHLIGHTS_1_NAME=
#KX_HIGHLIGHTS_1_DESCRIPTION=
#KX_HIGHLIGHTS_1_FINGERPRINT=
#KX_HIGHLIGHTS_2_NAME=
#KX_HIGHLIGHTS_2_DESCRIPTION=
#KX_HIGHLIGHTS_2_FINGERPRINT=
#KX_HIGHLIGHTS_3_NAME=
#KX_HIGHLIGHTS_3_DESCRIPTION=
#KX_HIGHLIGHTS_3_FINGERPRINT=
# Enable caching of keys (experimental)
# Opt-in; to disable, omit the environment variable
#ENABLE_EXPERIMENTAL_CACHE=true
# Enable profile request rate limiting (experimental)
# Opt-in; to disable, omit the environment variable
#ENABLE_EXPERIMENTAL_RATE_LIMITER=true
#ENABLE_EXPERIMENTAL_CACHE=

View file

@ -1,8 +0,0 @@
extends templates/base.pug
block content
h1 429 TOO MANY REQUESTS
p
| Too many requests from this IP, please try again later.
br
| Limit: 3 profile requests per second.

View file

@ -1,48 +0,0 @@
extends templates/base.pug
block content
section
h1 Apps
h2 Keyoxide mobile
p The app allows you to verify the online identities of Keyoxide profiles.
.screenshots
img(src="/static/img/keyoxide_mobile_dark_home.jpg"
alt="Screenshot of Keyoxide mobile app")
img(src="/static/img/keyoxide_mobile_dark_profile.jpg"
alt="Screenshot of Keyoxide mobile app")
img(src="/static/img/keyoxide_mobile_light_home.jpg"
alt="Screenshot of Keyoxide mobile app")
h3 Download
.banners
a(href="https://f-droid.org/packages/org.keyoxide.keyoxide/")
img(src="https://img.shields.io/badge/F--Droid-1976D2?style=for-the-badge&logo=f-droid&logoColor=white",
alt="Get it on F-Droid")
a(href="https://play.google.com/store/apps/details?id=org.keyoxide.keyoxide&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1")
img(src="https://img.shields.io/badge/Google_Play-414141?style=for-the-badge&logo=google-play&logoColor=white",
alt="Get it on Google Play")
a(href="https://apps.apple.com/us/app/keyoxide/id1670664318")
img(src="https://img.shields.io/badge/App_Store-0D96F6?style=for-the-badge&logo=app-store&logoColor=white",
alt="Get it on App Store")
a(href="https://codeberg.org/Berker/keyoxide-flutter/releases")
img(src="https://img.shields.io/badge/Codeberg.org-2185d0?style=for-the-badge&logo=codeberg&logoColor=white",
alt="Get it on Codeberg.org")
p
| Developer:
a(href="https://keyoxide.org/aspe:keyoxide.org:WHM3OC7UFRARIVEXDXUV4GVXNQ") Berker Sen
br
| Source code:
a(href="https://codeberg.org/Berker/keyoxide-flutter") Codeberg.org
hr
h2 Keyoxide ASP web tool
p The web tool lets you create and maintain Keyoxide profiles using the ASP method.
.screenshots
img(src="/static/img/keyoxide_asp_web_home.jpg"
alt="Screenshot of Keyoxide ASP web tool")
p
| Homepage:
a(href="https://asp.keyoxide.org") asp.keyoxide.org
br
| Source code:
a(href="https://codeberg.org/keyoxide/kx-aspe-web") Codeberg.org

View file

@ -3,4 +3,4 @@ extends templates/base.pug
block content
section
h1= title
| !{ content }
!{ content }

View file

@ -2,9 +2,6 @@ extends templates/base.pug
block content
section#search.form-wrapper
<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="m10.87 0c-0.4551 0-0.832 0.3769-0.832 0.832 0 0.4551 0.3769 0.832 0.832 0.832 0.4551 0 0.832-0.3769 0.832-0.832 0-0.4551-0.3769-0.832-0.832-0.832zm1.85 0.293c-0.5683 0-1.037 0.4688-1.037 1.037 0 0.2368 0.0807 0.4573 0.2168 0.6328-0.05687-0.007139-0.1151-0.01172-0.1738-0.01172-0.7683 0-1.398 0.6302-1.398 1.398 0 0.7683 0.6302 1.4 1.398 1.4 0.3773 0 0.7222-0.1523 0.9746-0.3984-0.00891 0.07397-0.01562 0.1483-0.01562 0.2246 0 0.5999 0.3126 1.131 0.7734 1.48l-2.586 2.012v-0.1152c0-1.817-1.482-3.299-3.299-3.299-1.817 0-3.299 1.482-3.299 3.299v6.828c-1.699e-4 -0.01435-0.001953-0.009604-0.001953 0.03516v5.883c-3.1e-6 1.819 1.482 3.301 3.301 3.301 1.73 0 3.115-1.354 3.25-3.051l3.744 2.494c1.512 1.008 3.566 0.5976 4.574-0.9141 1.008-1.512 0.5976-3.566-0.9141-4.574l-5.014-3.344 5.209-4.053c1.263-0.9824 1.63-2.72 0.9102-4.115-0.006296-0.01221-0.0141-0.02849-0.02148-0.04297 0.2572-0.2824 0.4141-0.657 0.4141-1.066 0-0.8712-0.7128-1.584-1.584-1.584-0.6345 0-1.187 0.3779-1.439 0.9199-0.08446-0.006921-0.1692-0.01237-0.2539-0.01367 0.0014-0.03013 0.003906-0.04019 0.003906-0.08008 0-1.036-0.8484-1.885-1.885-1.885-0.579 5.6e-6 -1.097 0.2653-1.443 0.6797 1.08e-4 -0.007109 0-0.01435 0-0.02148 0-0.3811-0.1542-0.7293-0.4043-0.9824 0.5684 0 1.037-0.4688 1.037-1.037 0-0.5683-0.4688-1.037-1.037-1.037zm-1.85 0.4219c0.06898 0 0.1172 0.04821 0.1172 0.1172s-0.04821 0.1172-0.1172 0.1172c-0.06898 0-0.1172-0.04821-0.1172-0.1172s0.0482-0.1172 0.1172-0.1172zm1.85 0.293c0.1822 0 0.3223 0.14 0.3223 0.3223 0 0.1822-0.14 0.3223-0.3223 0.3223-0.1822 0-0.3223-0.14-0.3223-0.3223 0-0.1822 0.14-0.3223 0.3223-0.3223zm-0.9941 1.658c0.3822 0 0.6836 0.3014 0.6836 0.6836 0 0.3822-0.3014 0.6855-0.6836 0.6855-0.3822 0-0.6836-0.3034-0.6836-0.6855 0-0.3822 0.3014-0.6836 0.6836-0.6836zm2.842 0.7402c0.6502-1.88e-5 1.17 0.5197 1.17 1.17 0 0.05687-0.005159 0.1097-0.005859 0.1914-6.61e-4 0.08173 0.01051 0.21 0.08008 0.3281 0.06857 0.1163 0.1972 0.2073 0.3047 0.2402 0.1075 0.03295 0.1919 0.03178 0.2617 0.03125 0.0602-4.678e-4 0.1197 0.005507 0.1797 0.009766 0.0172 0.6402 0.4194 1.188 0.9824 1.422 0.08721 0.03618 0.1774 0.06577 0.2715 0.08594 0.003893 8.346e-4 0.007816 0.001148 0.01172 0.001953 0.00711 0.001489 0.01434 0.002515 0.02148 0.003906 0.04462 0.008569 0.08895 0.01678 0.1348 0.02148 0.001285 1.341e-4 0.00262-1.31e-4 0.003906 0 0.05184 0.00519 0.1051 0.007812 0.1582 0.007812 0.09398 0 0.1858-0.009457 0.2754-0.02539 0.002495-4.438e-4 0.005321 4.557e-4 0.007812 0 0.003259-5.976e-4 0.006512-0.001335 0.009766-0.001953 0.08604-0.01629 0.1694-0.04058 0.25-0.07031 0.004443-0.001639 0.009247-0.002228 0.01367-0.003906 0.0038-0.00142 0.002259-6.138e-4 0.005859-0.001953 0.5338 1.086 0.2618 2.415-0.7188 3.178l-5.604 4.357a0.3573 0.3573 0 0 0 0.02148 0.5801l5.428 3.617c1.19 0.7933 1.51 2.394 0.7168 3.584-0.7933 1.19-2.394 1.51-3.584 0.7168l-4.25-2.832a0.3573 0.3573 0 0 0-0.5547 0.2969v0.3848c0 1.433-1.153 2.586-2.586 2.586-1.433 0-2.586-1.153-2.586-2.586v-5.883c0 0.02601 0.001373 0.01561 0.001953-0.03125a0.3573 0.3573 0 0 0 0-0.001953 0.3573 0.3573 0 0 0 0-0.001953v-6.828c0-1.43 1.154-2.584 2.584-2.584 1.43 0 2.584 1.154 2.584 2.584v0.8457a0.3573 0.3573 0 0 0 0.5762 0.2832l3.207-2.496s0.07527-0.04914 0.1484-0.1289c0.07317-0.07976 0.1932-0.2171 0.1641-0.4531-0.01519-0.1239-0.1002-0.2811-0.1855-0.3535-0.08537-0.07239-0.1288-0.08753-0.166-0.1133-0.3095-0.2145-0.502-0.5695-0.502-0.9609 0-0.6502 0.5177-1.17 1.168-1.17zm3.574 1.057c0.4851 0 0.8691 0.386 0.8691 0.8711 0 0.3632-0.2156 0.671-0.5273 0.8027-5.5e-4 2.323e-4 -0.001402-2.313e-4 -0.001953 0-0.008517 0.003475-0.0281 0.009073-0.03516 0.01172-0.04666 0.01751-0.09382 0.03145-0.1426 0.04102-0.002029 3.979e-4 -0.003828 0.00157-0.00586 0.001953-0.003817 6.812e-4 -0.007883-6.32e-4 -0.01172 0-0.04722 0.008255-0.09583 0.01367-0.1445 0.01367-0.06064 0-0.1188-0.006076-0.1758-0.01758s-0.1119-0.0289-0.1641-0.05078c-0.0522-0.02188-0.1021-0.04893-0.1484-0.08008s-0.08962-0.06619-0.1289-0.1055-0.07432-0.0826-0.1055-0.1289-0.0582-0.09624-0.08008-0.1484c-0.02188-0.0522-0.03928-0.1071-0.05078-0.1641-0.0115-0.05698-0.01758-0.1151-0.01758-0.1758 1e-5 -0.01046 0.001317-0.032 0.001954-0.04492 7.45e-4 -0.01496 4.8e-4 -0.03018 0.001953-0.04492 0.04418-0.4421 0.4124-0.7812 0.8672-0.7812z"/>
</svg>
form(action="post")
input#query(type="search" name="query" required placeholder="Search for a profile")
input(type="submit" value="Search")

View file

@ -25,9 +25,10 @@ footer
a(href="https://ariadne.id") ariadne.id
p
| Version
a(href=meta.keyoxide.sourceUrl)= meta.keyoxide.semver
| Version #{meta.keyoxide.version}
if (meta.env === "development")
| -dev
br
| &copy; 2020-2024 Keyoxide project contributors
| &copy; 2023 Keyoxide project contributors

View file

@ -1,10 +1,10 @@
header
.container
nav
a.logo(href='/' aria-label='Home') Keyoxide
.spacer
a.logo(href='/' aria-label='Home')
img(src='/static/img/logo_circle.png' alt='Keyoxide' aria-hidden='true')
| Keyoxide
.links
a.text(href='https://docs.keyoxide.org/getting-started') Getting started
a.text(href='/apps') Apps
a.text(href='https://docs.keyoxide.org') Docs
a.text(href='https://blog.keyoxide.org') Blog

View file

@ -1,48 +1,54 @@
extends templates/base.pug
mixin generatePersona(persona, isPrimary)
if persona.claims.length > 0
h2
if persona.email
| Identity claims (
span.p-email #{persona.email}
| )
else
| Identity claims
if isPrimary
small.primary primary
if persona.description
span.persona__description.p-comment
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
| #{persona.description}
each claim in persona.claims
if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim,data-status='running')
details(aria-label="Claim")
summary
.info
img(src=`https://design.keyoxide.org/brands/service-providers/_/icon.svg` onerror="this.src='https://design.keyoxide.org/brands/service-providers/_/icon.svg'")
p
span.title= claim.display.profileName
span.subtitle-wrapper
| [
span.subtitle= claim.display.serviceproviderName
| ]
.icons
.verificationStatus
.inProgress
<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"></path></svg>
.success
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
.failure
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
h2
if persona.email
| Identity claims (
span.p-email #{persona.email}
| )
else
| Identity claims
if isPrimary
small.primary primary
if persona.description
span.persona__description.p-comment
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
| #{persona.description}
each claim in persona.claims
if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim,data-status='running')
details(aria-label="Claim")
summary
.info
p
span.title= claim.display.name
span.subtitle-wrapper
| [
span.subtitle= claim.display.serviceproviderName
| ]
.icons
.verificationStatus
.inProgress
<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"></path></svg>
.success
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
.failure
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
.content
.subsection
img(src='/static/img/link.png')
div
p Claim link:
a(rel="me" href=claim.uri aria-label="Link to claim")= claim.uri
.content
.subsection
img(src='/static/img/link.png')
div
if (claim.display.url)
p Profile link:
a(rel='me' href=claim.display.url aria-label="Link to profile")= claim.display.url
else
p Profile link: not accessible from browser
if (claim.matches.length === 1 && claim.matches[0].proof.uri)
p Proof link:
a(href=claim.matches[0].proof.uri aria-label="Link to proof")= claim.matches[0].proof.uri
else
p Proof link: not accessible from browser
block content
if (data && 'publicKey' in data)
@ -111,16 +117,6 @@ block content
input(type='submit', name='submit', value='Generate profile')
unless (isSignature && !signature)
if (theme)
style
| :root {
| --primary-color-light: #{theme.primary.light};
| --primary-color-dark: #{theme.primary.dark};
| --primary-color-subtle-light: #{theme.primarySubtle.light};
| --primary-color-subtle-dark: #{theme.primarySubtle.dark};
| --background-color-light: #{theme.background.light};
| --background-color-dark: #{theme.background.dark};
| }
.profile__header
img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
@ -140,28 +136,29 @@ block content
each persona, index in data.personas
unless index == data.primaryPersonaIndex
+generatePersona(persona, false)
h2 Profile
if data.verifiers.length > 0
button.themed(onClick=`showQR('${data.verifiers[0].url}', 'profile_verifier_url')` aria-label='Show profile ID QR') Keyoxide profile QR
button.themed(onClick=`showQR('${data.identifier}', 'profile_identifier')` aria-label='Show profile ID QR') Profile ID QR
section
h2 Profile information
if (data && data.publicKey)
h3 Public key
kx-key.kx-item(data-keydata=data.publicKey)
details(aria-label="Key")
summary
.info
p
span.title= data.identifier
span.subtitle-wrapper
| [
span.subtitle= data.publicKey.fetch.method
| ]
.content
h3 Public key
kx-key.kx-item(data-keydata=data.publicKey)
details(aria-label="Key")
summary
.info
p
span.title= data.identifier
span.subtitle-wrapper
| [
span.subtitle= data.publicKey.fetch.method
| ]
.content
.subsection
img(src='/static/img/link.png')
div
p Key link:
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
hr
if (data.profileType === 'openpgp')
.subsection
img(src='/static/img/link.png')
img(src='/static/img/qrcode.png')
div
p Key link:
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
button(onClick=`showQR('${data.publicKey.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR

View file

@ -6,7 +6,6 @@ html(lang='en')
meta(name='theme-color' content='#fff')
meta(name='description' content='Modern and secure platform to manage a decentralized identity based on cryptographic keys')
link(rel='shortcut icon' href='/favicon.svg')
link(rel='stylesheet' href='/static/main.css')
title= (title ? title : 'Keyoxide')
include ../partials/header.pug
@ -17,6 +16,7 @@ html(lang='en')
include ../partials/footer.pug
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/doipFetchers.js' charset='utf-8')
script(type='application/javascript' defer src='/static/doip.js' charset='utf-8')

View file

@ -11,7 +11,7 @@ block content
a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
| hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
| . Be sure to include "openpgp4fpr:" for a valid proof!
h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash

View file

@ -11,7 +11,7 @@ block content
a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt
| hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
| . Be sure to include "openpgp4fpr:" for a valid proof!
h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash

13469
yarn.lock

File diff suppressed because it is too large Load diff