Compare commits

...

101 commits

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
Yarmo Mackenbach
785647bbb8
chore: release 4.2.7 2024-02-01 18:02:04 +01:00
Yarmo Mackenbach
3fb5fb860f
chore: bump doipjs to 1.2.9 2024-02-01 17:17:16 +01:00
Yarmo Mackenbach
e7c1a878ff
fix: use const 2024-01-30 01:23:36 +01:00
Yarmo Mackenbach
ed4c265dad
fix: use only hash in semver build metadata 2024-01-30 01:21:13 +01:00
André Jaenisch
912f3619eb
fix: use PATH expansion for run-scripts
Yarn is including `./node_modules/.bin` in $PATH when evaluating
run-commands. In other words, they don't have to be included for
executables inside of `package.json`.

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
2024-01-29 10:17:00 +01:00
Yarmo Mackenbach
9e19a622d9
fix: typo 2024-01-28 13:54:50 +01:00
Yarmo Mackenbach
5f1b800a42
feat: get info about last git commit 2024-01-28 13:53:47 +01:00
Yarmo Mackenbach
7f671e74b6
chore: update README [SKIP CI] 2024-01-28 10:15:26 +01:00
Yarmo Mackenbach
18841c41af
chore: update README [SKIP CI] 2024-01-28 10:02:05 +01:00
Yarmo Mackenbach
827969f9a1
chore: update README [SKIP CI] 2024-01-28 10:01:06 +01:00
Yarmo Mackenbach
3726642211
chore: update README [SKIP CI] 2024-01-28 10:00:09 +01:00
Yarmo Mackenbach
0d5f6aaf3e
feat: add source URL to footer 2024-01-27 22:35:51 +01:00
Yarmo Mackenbach
4b764583d9
feat: optimize version number generation 2024-01-27 22:35:21 +01:00
Yarmo Mackenbach
fe96ab2c35
fix: remove obsolete references to highlights 2024-01-27 22:16:37 +01:00
Yarmo Mackenbach
e52d965d02
fix: move version endpoint 2024-01-27 22:12:43 +01:00
Yarmo Mackenbach
6950170cc9
fix: handling of arg and env 2024-01-27 22:00:06 +01:00
Yarmo Mackenbach
759f7bd79e
fix: change arg to env 2024-01-27 21:57:06 +01:00
Yarmo Mackenbach
7416912197
fix: add env args to Dockerfile 2024-01-27 21:51:59 +01:00
Yarmo Mackenbach
1f3c47af2b
fix: add necessary env args to CI build 2024-01-27 21:48:51 +01:00
Yarmo Mackenbach
de2a938ccc
fix: simplify version API endpoint 2024-01-27 21:44:11 +01:00
Yarmo Mackenbach
e55a0b2b77
feat: add version API endpoint 2024-01-27 21:31:32 +01:00
Yarmo Mackenbach
0061149aa5
fix: change default label to "Needs Triage" 2024-01-26 23:10:03 +01:00
Yarmo Mackenbach
4873d8bc8c
chore: improve issue templates [SKIP CI] 2024-01-26 20:52:25 +01:00
Yarmo Mackenbach
875df78dfe chore: change issue template [SKIP CI] 2024-01-26 16:15:20 +01:00
Yarmo Mackenbach
1607ca684b
chore: release 4.2.6 2024-01-24 19:32:59 +01:00
Yarmo Mackenbach
607fee17f9
fix: remove obsolete config 2024-01-24 19:29:58 +01:00
Yarmo Mackenbach
9be8cab435
feat: support openpgp4fpr in URL 2024-01-23 20:16:48 +01:00
Yarmo Mackenbach
a2857d1a3e
feat: update footer year 2024-01-23 20:11:31 +01:00
Yarmo Mackenbach
ee55c50a1a
fix: URL query validation 2024-01-23 19:47:33 +01:00
Yarmo Mackenbach
e0d47e5247
feat: add proxy routes 2024-01-23 19:34:17 +01:00
Yarmo Mackenbach
ee60af395a
chore: bump deps 2024-01-23 19:26:28 +01:00
Yarmo Mackenbach
ce9ffce84d
chore: bump deps 2024-01-23 19:24:24 +01:00
Yarmo Mackenbach
d661e3d61f
chore: release 4.2.5 2023-10-09 19:46:26 +02:00
Yarmo Mackenbach
520165115a
feat: update doipjs to 1.2.7 2023-10-09 19:42:16 +02:00
Yarmo Mackenbach
1a789ab90d
chore: release 4.2.4 2023-10-09 19:19:52 +02:00
Yarmo Mackenbach
836e6d2077
feat: update doipjs to 1.2.6 2023-10-09 19:15:05 +02:00
Yarmo Mackenbach
28852c74fb
chore: release 4.2.3 2023-10-05 17:35:33 +02:00
Yarmo Mackenbach
4d49df981b
fix: update schemas 2023-10-05 17:10:03 +02:00
Yarmo Mackenbach
94471b4b8e
fix: fix CSS vars 2023-10-05 13:15:23 +02:00
Yarmo Mackenbach
99217f6a3d
feat: support profile theming 2023-10-05 13:06:35 +02:00
Yarmo Mackenbach
df48b92732
feat: improve apps page 2023-10-05 10:51:54 +02:00
Yarmo Mackenbach
c6932f8b98
feat: add apps page 2023-10-04 20:39:14 +02:00
Yarmo Mackenbach
6f688d5caf
feat: change logo weight 2023-10-04 18:42:14 +02:00
Yarmo Mackenbach
fafa3ad58d
fix: fix styling 2023-10-04 15:37:37 +02:00
Yarmo Mackenbach
1b36959d17
fix: fix formatting 2023-10-04 14:53:11 +02:00
Yarmo Mackenbach
5200ad3611
feat: make Dicebear API domain configurable 2023-10-04 14:49:53 +02:00
Yarmo Mackenbach
96983a67df
fix: update schemas 2023-10-04 14:46:40 +02:00
Yarmo Mackenbach
d8c7ed8e8a
feat: replace rome with biome 2023-10-04 13:53:58 +02:00
Yarmo Mackenbach
f83eb78f80
fix: fix styling 2023-10-04 13:04:07 +02:00
Yarmo Mackenbach
d91fdfc1bd
feat: remove logo from header, add to index 2023-10-04 12:57:41 +02:00
Yarmo Mackenbach
b0a93dcf91
feat: add Keyoxide profile QR button 2023-10-04 11:20:44 +02:00
Yarmo Mackenbach
be97ff4246
fix: only add profile verifier when appropriate 2023-10-04 11:19:54 +02:00
Yarmo Mackenbach
b250392326
fix: typo 2023-10-04 10:21:54 +02:00
Yarmo Mackenbach
995e4c73f7
feat: add QR button for profiles 2023-10-04 10:06:21 +02:00
Yarmo Mackenbach
91244b992b
fix: references to display object 2023-10-04 10:01:09 +02:00
Yarmo Mackenbach
f137c6aa4c
feat: update doipjs to 1.2.4 2023-10-04 09:22:30 +02:00
Yarmo Mackenbach
c0ee28d778
feat: update doipjs to 1.2.3 2023-10-03 13:53:54 +02:00
Yarmo Mackenbach
5928e4d28d
fix: allow toJSON to fail 2023-10-03 13:11:03 +02:00
Yarmo Mackenbach
cf78f90251
fix: update schemas 2023-10-03 13:10:17 +02:00
Yarmo Mackenbach
86b2b35462
fix: use correct property to generate icon URL 2023-10-03 12:57:39 +02:00
Yarmo Mackenbach
b1bc1328eb
chore: release 4.2.2 2023-10-03 12:34:16 +02:00
Yarmo Mackenbach
88ecf73d11
feat: update doipjs to 1.2.2 2023-10-03 12:30:29 +02:00
Yarmo Mackenbach
652bfe227e
fix: avoid calling function on null 2023-09-25 17:24:26 +02:00
Yarmo Mackenbach
73dd948a33
fix: improve claim image layout 2023-09-25 17:13:06 +02:00
Yarmo Mackenbach
8e39b11cec
fix: make icons light in dark mode 2023-09-25 17:05:45 +02:00
Yarmo Mackenbach
9163b45525
feat: add updating icons to claims 2023-09-25 16:12:20 +02:00
Yarmo Mackenbach
526e69809c
chore: release 4.2.1 2023-09-23 22:16:32 +02:00
Yarmo Mackenbach
72e18ac7dd
fix: tweak rate limiter parameters 2023-09-23 22:10:17 +02:00
Yarmo Mackenbach
cd629fabb9
chore: release 4.2.0 2023-09-23 10:44:39 +02:00
Yarmo Mackenbach
4fb8302cc9
feat: update doipjs to 1.2.1 2023-09-23 10:43:40 +02:00
Yarmo Mackenbach
af1bdd872c
feat: update template.env 2023-09-22 12:20:16 +02:00
Yarmo Mackenbach
b333365730
feat: add profile request rate limiter 2023-09-22 12:15:10 +02:00
Yarmo Mackenbach
bccd5d298f
feat: update doipjs to 1.1.1 2023-09-22 11:07:14 +02:00
Yarmo Mackenbach
3d7f1ce11a
fix: make hash utils aware of ASPE 2023-09-22 10:22:35 +02:00
Yarmo Mackenbach
09d2292557
feat: add logging to openpgp profiles 2023-09-22 09:48:55 +02:00
Yarmo Mackenbach
a812bb0866
feat: add request data to logs 2023-09-22 09:26:07 +02:00
Yarmo Mackenbach
5f5e039a2c
fix: fix CHANGELOG 2023-09-21 15:56:26 +02:00
Yarmo Mackenbach
c272fa7d44
chore: release 4.1.1 2023-09-21 15:50:32 +02:00
Yarmo Mackenbach
36ff58576b
feat: add logging to OpenPGP cache component 2023-09-21 15:33:27 +02:00
Yarmo Mackenbach
fc10aeba1c
fix: use correct fromJSON() for Profiles and Claims 2023-09-21 15:30:35 +02:00
Yarmo Mackenbach
7ce2287894
feat: update doipjs to 1.1.0 2023-09-21 15:27:44 +02:00
Yarmo Mackenbach
1756a37ab2
fix: missing rel=me link for ambiguous claims 2023-09-20 08:32:50 +02:00
Yarmo Mackenbach
57da895ae5
feat: update doipjs to 1.0.4 2023-09-19 15:42:54 +02:00
Yarmo Mackenbach
b103be7897
feat: update doipjs to 1.0.3 2023-09-19 13:42:34 +02:00
Yarmo Mackenbach
bed7a7ee77
feat: update doipjs to 1.0.2 2023-09-19 13:03:37 +02:00
Yarmo Mackenbach
78ec2a6bc6
fix: clean up code 2023-09-19 13:00:28 +02:00
Yarmo Mackenbach
a28f1bba96
chore: release 4.1.0 2023-09-18 18:04:08 +02:00
Yarmo Mackenbach
494b93bf5c
fix: fix dark theme issues 2023-09-17 14:31:56 +02:00
Yarmo Mackenbach
e2ed828f9d
fix: make public key section optional 2023-09-17 14:21:21 +02:00
Yarmo Mackenbach
d74aa0854a
feat: remove obsolete dependencies 2023-09-15 21:14:01 +02:00
Yarmo Mackenbach
4cdbe1783b
fix: fix styles 2023-09-15 21:07:00 +02:00
Ty
d34d3027ee
Update to yarn modern, patch doip for SASL auth 2023-08-03 21:09:21 -06:00
53 changed files with 9138 additions and 5643 deletions

View file

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

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

View file

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

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

View file

@ -0,0 +1,8 @@
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.

9
.gitignore vendored
View file

@ -34,3 +34,12 @@ ignore
dist dist
static static
logs logs
# yarn
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

View file

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

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

@ -6,6 +6,83 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [4.0.2] - 2023-09-12
### Fixed ### Fixed
- Handle doip promise rejection - Handle doip promise rejection

View file

@ -3,14 +3,20 @@ FROM node:20-alpine as builder
WORKDIR /app WORKDIR /app
COPY . . COPY . .
RUN yarn --pure-lockfile RUN corepack enable
RUN yarn run build:server RUN yarn install --immutable
RUN yarn run build:static RUN yarn run build:server && yarn run build:static
### ###
FROM node:20-alpine 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 WORKDIR /app
COPY --from=builder /app/package.json /app/package.json COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/dist /app/dist COPY --from=builder /app/dist /app/dist

View file

@ -1,15 +1,15 @@
# Keyoxide # keyoxide-web
[![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) [![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=for-the-badge)](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE) [![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=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide) [![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=for-the-badge)](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=for-the-badge)](https://fosstodon.org/@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=for-the-badge)](https://opencollective.com/keyoxide) [![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
[Keyoxide](https://keyoxide.org) is a modern, secure and decentralized platform to prove your online identity. `keyoxide-web` is the web client that powers [keyoxide.org](https://keyoxide.org) and which you can freely host on your own infrastructure.
## Self-hosting ## Hosting keyoxide-web
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! 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,34 +23,44 @@ docker run -d -p 3000:3000 codeberg.org/keyoxide/keyoxide-web:latest
Keyoxide will now be available by visiting http://localhost:3000. Keyoxide will now be available by visiting http://localhost:3000.
More information available in the [documentation](docs.keyoxide.org/self-hosting). 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).
## Contributing ## Contributing
Anyone can contribute if they'd like! No need to be a programmer or technically-oriented for that matter. Anyone can contribute!
Contributing to Keyoxide can happen in many forms: Developers are invited to:
- Finding and reporting bugs - fork the repository and play around
- Suggesting new features - submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/keyoxide-web/issues)
- Improving documentation
- Writing code to fix bugs and features 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.
- Promoting decentralized identity and web3.0
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
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. 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.
### Local development ## About the Keyoxide project
To run Keyoxide locally on your machine for 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!
- install either 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.
- 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)

15
biome.json Normal file
View file

@ -0,0 +1,15 @@
{
"$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,9 +12,6 @@ services:
- 3000:3000 - 3000:3000
environment: environment:
- DOMAIN= - DOMAIN=
# - KX_HIGHLIGHTS_1_NAME=
# - KX_HIGHLIGHTS_1_DESCRIPTION=
# - KX_HIGHLIGHTS_1_FINGERPRINT=
## The hostname to reach the doip_proxy container below ## The hostname to reach the doip_proxy container below
# - PROXY_HOSTNAME= # - PROXY_HOSTNAME=
## The onion URL to advertise in the HTTP response headers ## The onion URL to advertise in the HTTP response headers

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

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

View file

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

View file

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

View file

@ -42,7 +42,12 @@ const opts = {
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
}, },
irc: { 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: { matrix: {
instance: process.env.MATRIX_INSTANCE || null, instance: process.env.MATRIX_INSTANCE || null,
@ -92,6 +97,40 @@ 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 // DNS route
router.get('/dns', query('domain').isFQDN(), (req, res) => { router.get('/dns', query('domain').isFQDN(), (req, res) => {
const errors = validationResult(req) const errors = validationResult(req)

View file

@ -28,7 +28,10 @@ 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/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
import express from 'express' import express from 'express'
import * as httpContext from 'express-http-context2'
import { nanoid } from 'nanoid'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import { execSync } from 'child_process'
import { stringReplace } from 'string-replace-middleware' import { stringReplace } from 'string-replace-middleware'
import * as pug from 'pug' import * as pug from 'pug'
import * as dotenv from 'dotenv' import * as dotenv from 'dotenv'
@ -41,6 +44,20 @@ import staticRoute from './routes/static.js'
import utilRoute from './routes/util.js' import utilRoute from './routes/util.js'
dotenv.config() 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 app = express()
const packageData = JSON.parse(readFileSync('./package.json')) const packageData = JSON.parse(readFileSync('./package.json'))
@ -49,12 +66,23 @@ app.engine('pug', pug.__express).set('view engine', 'pug')
app.set('port', process.env.PORT || 3000) app.set('port', process.env.PORT || 3000)
app.set('domain', process.env.DOMAIN) app.set('domain', process.env.DOMAIN)
app.set('scheme', process.env.SCHEME || 'https') app.set('scheme', process.env.SCHEME || 'https')
app.set('keyoxide_name', 'keyoxide-web')
app.set('keyoxide_version', packageData.version) app.set('keyoxide_version', packageData.version)
app.set('git_branch', gitBranch)
app.set('git_hash', gitHash)
app.set('onion_url', process.env.ONION_URL) app.set('onion_url', process.env.ONION_URL)
// Middlewares // Middlewares
app.use(httpContext.middleware)
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader('Permissions-Policy', 'interest-cohort=()') 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() next()
}) })

View file

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

View file

@ -36,19 +36,11 @@ const router = express.Router()
const md = markdownImport({ typographer: true }) const md = markdownImport({ typographer: true })
router.get('/', (req, res) => { router.get('/', (req, res) => {
const highlights = [] res.render('index', { meta: getMetaFromReq(req) })
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`]
})
}
}
res.render('index', { highlights, meta: getMetaFromReq(req) }) router.get('/apps', (req, res) => {
res.render('apps', { title: 'Apps', meta: getMetaFromReq(req) })
}) })
router.get('/privacy', (req, res) => { router.get('/privacy', (req, res) => {
@ -75,6 +67,12 @@ router.get('/.well-known/webfinger', (req, res) => {
res.json(body) 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) => { router.get('/users/keyoxide', (req, res) => {
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) { if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>') res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')

View file

@ -29,95 +29,143 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/ */
import express from 'express' import express from 'express'
import bodyParserImport from 'body-parser' import bodyParserImport from 'body-parser'
import { rateLimit } from 'express-rate-limit'
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js' import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
import { Profile } from 'doipjs' import { Profile } from 'doipjs'
import { getMetaFromReq } from '../server/utils.js' import { generateProfileTheme, getMetaFromReq, escapedParam } from '../server/utils.js'
import logger from '../log.js'
const router = express.Router() const router = express.Router()
const bodyParser = bodyParserImport.urlencoded({ extended: false }) const bodyParser = bodyParserImport.urlencoded({ extended: false })
router.get('/sig', (req, res) => { let profileRateLimiter = (req, res, next) => {
res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) }) next()
}) }
router.post('/sig', bodyParser, async (req, res) => { if (process.env.ENABLE_EXPERIMENTAL_RATE_LIMITER) {
const data = await generateSignatureProfile(req.body.signature) profileRateLimiter = rateLimit({
const title = utils.generatePageTitle('profile', data) windowMs: 5000,
res.set('ariadne-identity-proof', data.identifier) limit: 20,
res.render('profile', { standardHeaders: 'draft-7',
title, legacyHeaders: false,
data: data instanceof Profile ? data.toJSON() : data, handler: (req, res, next, options) => {
isSignature: true, logger.debug('Rate-limiting a profile request',
signature: req.body.signature, { component: 'profile_rate_limiter', action: 'block' })
enable_message_encryption: false,
enable_signature_verification: false,
meta: getMetaFromReq(req)
})
})
router.get('/wkd/:id', async (req, res) => { res.status(options.statusCode).render('429', { meta: getMetaFromReq(req) })
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/:id', async (req, res) => { logger.debug('Starting the profile request rate limiter',
const data = await generateHKPProfile(req.params.id) { component: 'profile_rate_limiter', action: 'start' })
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) => { router.get('/sig',
const data = await generateHKPProfile(req.params.id, req.params.server) profileRateLimiter,
const title = utils.generatePageTitle('profile', data) (req, res) => {
res.set('ariadne-identity-proof', data.identifier) res.render('profile', { isSignature: true, signature: null, meta: getMetaFromReq(req) })
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) => { router.post('/sig',
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint) profileRateLimiter,
const title = utils.generatePageTitle('profile', data) bodyParser,
res.set('ariadne-identity-proof', data.identifier) async (req, res) => {
res.render('profile', { const data = await generateSignatureProfile(req.body.signature)
title, const title = utils.generatePageTitle('profile', data)
data: data instanceof Profile ? data.toJSON() : data, res.set('ariadne-identity-proof', data.identifier)
enable_message_encryption: false, res.render('profile', {
enable_signature_verification: false, title,
meta: getMetaFromReq(req) 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('/:id', async (req, res) => { router.get('/wkd/:id',
const data = await generateAutoProfile(req.params.id) profileRateLimiter,
const title = utils.generatePageTitle('profile', data) escapedParam('id'),
res.set('ariadne-identity-proof', data.identifier) async (req, res) => {
res.render('profile', { const data = await generateWKDProfile(req.params.id)
title, const title = utils.generatePageTitle('profile', data)
data: data instanceof Profile ? data.toJSON() : data, res.set('ariadne-identity-proof', data.identifier)
enable_message_encryption: false, res.render('profile', {
enable_signature_verification: false, title,
meta: getMetaFromReq(req) 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('/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)
})
}) })
})
export default router 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/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
import express from 'express' import express from 'express'
import { getMetaFromReq } from '../server/utils.js' import { escapedParam, getMetaFromReq } from '../server/utils.js'
const router = express.Router() const router = express.Router()
@ -38,43 +38,55 @@ router.get('/', function (req, res) {
router.get('/profile-url', function (req, res) { router.get('/profile-url', function (req, res) {
res.render('util/profile-url', { meta: getMetaFromReq(req) }) res.render('util/profile-url', { meta: getMetaFromReq(req) })
}) })
router.get('/profile-url/:input', function (req, res) { router.get('/profile-url/:input',
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) }) escapedParam('input'),
}) function (req, res) {
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qr', function (req, res) { router.get('/qr', function (req, res) {
res.render('util/qr', { meta: getMetaFromReq(req) }) res.render('util/qr', { meta: getMetaFromReq(req) })
}) })
router.get('/qr/:input', function (req, res) { router.get('/qr/:input',
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) }) escapedParam('input'),
}) function (req, res) {
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qrfp', function (req, res) { router.get('/qrfp', function (req, res) {
res.render('util/qrfp', { meta: getMetaFromReq(req) }) res.render('util/qrfp', { meta: getMetaFromReq(req) })
}) })
router.get('/qrfp/:input', function (req, res) { router.get('/qrfp/:input',
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) }) escapedParam('input'),
}) function (req, res) {
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/wkd', function (req, res) { router.get('/wkd', function (req, res) {
res.render('util/wkd', { meta: getMetaFromReq(req) }) res.render('util/wkd', { meta: getMetaFromReq(req) })
}) })
router.get('/wkd/:input', function (req, res) { router.get('/wkd/:input',
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) }) escapedParam('input'),
}) function (req, res) {
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/argon2', function (req, res) { router.get('/argon2', function (req, res) {
res.render('util/argon2', { meta: getMetaFromReq(req) }) res.render('util/argon2', { meta: getMetaFromReq(req) })
}) })
router.get('/argon2/:input', function (req, res) { router.get('/argon2/:input',
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) }) escapedParam('input'),
}) function (req, res) {
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/bcrypt', function (req, res) { router.get('/bcrypt', function (req, res) {
res.render('util/bcrypt', { meta: getMetaFromReq(req) }) res.render('util/bcrypt', { meta: getMetaFromReq(req) })
}) })
router.get('/bcrypt/:input', function (req, res) { router.get('/bcrypt/:input',
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) }) escapedParam('input'),
}) function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
})
export default router export default router

View file

@ -157,6 +157,10 @@ export const personaSchema = {
description: 'URL to an avatar image', description: 'URL to an avatar image',
type: ['string', 'null'] type: ['string', 'null']
}, },
themeColor: {
description: 'Profile page theme color',
type: ['string', 'null']
},
isRevoked: { isRevoked: {
type: 'boolean' type: 'boolean'
}, },
@ -215,17 +219,25 @@ export const claimSchema = {
display: { display: {
type: 'object', type: 'object',
properties: { properties: {
name: { profileName: {
type: 'string', type: 'string',
description: 'Account name to display in the user interface' description: 'Account name to display in the user interface'
}, },
url: { profileUrl: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'URL to link to in the user interface' description: 'Profile URL to link to in the user interface'
},
proofUrl: {
type: ['string', 'null'],
description: 'Proof URL to link to in the user interface'
}, },
serviceProviderName: { serviceProviderName: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Name of the service provider to display in the user interface' 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,7 +38,9 @@ const generateAspeProfile = async (id) => {
return doipjs.asp.fetchASPE(id) return doipjs.asp.fetchASPE(id)
.then(profile => { .then(profile => {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
}
profile = processAspProfile(profile) profile = processAspProfile(profile)
return profile return profile
}) })
@ -58,7 +60,9 @@ const generateWKDProfile = async (id) => {
return fetchWKD(id) return fetchWKD(id)
.then(async profile => { .then(async profile => {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
}
profile = processOpenPgpProfile(profile) profile = processOpenPgpProfile(profile)
logger.debug('Generating a WKD profile', logger.debug('Generating a WKD profile',
@ -89,7 +93,9 @@ const generateHKPProfile = async (id, keyserverDomain) => {
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}` keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
} }
profile.addVerifier('keyoxide', keyoxideUrl) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', keyoxideUrl)
}
profile = processOpenPgpProfile(profile) profile = processOpenPgpProfile(profile)
logger.debug('Generating a HKP profile', logger.debug('Generating a HKP profile',
@ -111,6 +117,7 @@ const generateAutoProfile = async (id) => {
let result let result
const aspeRe = /aspe:(.*):(.*)/ const aspeRe = /aspe:(.*):(.*)/
const openpgpRe = /openpgp4fpr:(.*)/
if (aspeRe.test(id)) { if (aspeRe.test(id)) {
result = await generateAspeProfile(id) result = await generateAspeProfile(id)
@ -120,6 +127,15 @@ 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('@')) { if (id.includes('@')) {
result = await generateWKDProfile(id) result = await generateWKDProfile(id)
@ -168,7 +184,9 @@ const generateKeybaseProfile = async (username, fingerprint) => {
return fetchKeybase(username, fingerprint) return fetchKeybase(username, fingerprint)
.then(async profile => { .then(async profile => {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`) if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
}
profile = processOpenPgpProfile(profile) profile = processOpenPgpProfile(profile)
logger.debug('Generating a Keybase profile', logger.debug('Generating a Keybase profile',
@ -215,7 +233,8 @@ const processAspProfile = async (/** @type {import('doipjs').Profile */ profile)
// Overwrite avatarUrl // Overwrite avatarUrl
// TODO: don't overwrite avatarUrl once it's fully supported // TODO: don't overwrite avatarUrl once it's fully supported
profile.personas[profile.primaryPersonaIndex].avatarUrl = `https://api.dicebear.com/6.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128` profile.personas[profile.primaryPersonaIndex].avatarUrl =
`https://${process.env.DICEBEAR_API_HOSTNAME || 'api.dicebear.com'}/7.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
return profile return profile
} }

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

View file

@ -28,6 +28,9 @@ 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/>. 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 { webcrypto as crypto } from 'crypto'
import { Profile } from 'doipjs'
import Color from 'colorjs.io'
import { param } from 'express-validator'
export async function computeWKDLocalPart (localPart) { export async function computeWKDLocalPart (localPart) {
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase()) const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
@ -80,10 +83,96 @@ export function encodeZBase32 (data) {
} }
export function getMetaFromReq (req) { 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 { return {
env: req.app.get('env'), env: req.app.get('env'),
keyoxide: { keyoxide: {
version: req.app.get('keyoxide_version') 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
} }
} }
} }
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

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

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

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

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

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

View file

@ -58,8 +58,9 @@ kx-claim {
} }
hr { hr {
margin: 8px 0;
border: none; border: none;
border-top: 2px solid var(--background-color); border-top: 2px solid var(--header-background-color);
} }
.content { .content {
@ -93,6 +94,9 @@ kx-claim {
} }
.info { .info {
display: flex;
align-items: baseline;
gap: 8px;
flex: 1; flex: 1;
} }
@ -100,6 +104,17 @@ kx-claim {
color: var(--text-color); 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 { .claim__links {
p { p {
@ -199,15 +214,15 @@ kx-claim {
text-transform: uppercase; text-transform: uppercase;
color: var(--button-text-color); color: var(--button-text-color);
background-color: var(--button-background-color); background-color: var(--button-background-color);
border: solid 2px var(--button-border-color); border: solid 1px var(--button-border-color);
border-radius: 4px; border-radius: 4px;
cursor: pointer; cursor: pointer;
} }
button:hover { button:hover {
background-color: var(--button-hover-background-color); background-color: var(--button-background-color-hover);
border-color: var(--button-hover-border-color); border-color: var(--button-border-color-hover);
color: var(--button-hover-text-color); color: var(--button-text-color-hover);
} }
&[data-status="running"] { &[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/typography.scss';
@use './styles/forms.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; box-sizing: border-box;
} }
:focus {
z-index: 100;
}
/* HELPERS */ /* HELPERS */
.spacer { .spacer {
@ -72,14 +72,16 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
/* DIALOGS */ /* DIALOGS */
dialog { dialog {
width: 100% !important; max-width: 480px;
max-width: 800px !important;
padding: 0 !important;
word-wrap: anywhere; word-wrap: anywhere;
} background-color: var(--section-background-color);
border: 0;
border-radius: 16px;
dialog>div { &::backdrop {
padding: 1em; background-color: #000;
opacity: 0.8;
}
} }
dialog form[method="Dialog"] { dialog form[method="Dialog"] {

View file

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

View file

@ -27,6 +27,10 @@ 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 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/>. 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 { body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -56,14 +60,12 @@ header {
} }
a.logo { a.logo {
flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
font-size: 1.8rem; font-size: 1.8rem;
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
color: var(--primary-color);
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
gap: 8px; gap: 8px;
@ -81,7 +83,7 @@ header {
} }
} }
a.text { a {
margin: 0; margin: 0;
color: var(--text-color); color: var(--text-color);
@ -191,3 +193,28 @@ section {
} }
} }
} }
.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; cursor: default;
} }
h2 { h2 {
margin: 2em 0 0.5em; margin: 1em 0 0.5em;
font-size: 1.2em; font-size: 1.2em;
font-weight: bold; font-weight: bold;
color: var(--h2-color); color: var(--h2-color);

View file

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

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 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/>. 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 QRCode from 'qrcode'
import * as openpgp from 'openpgp' import * as openpgp from 'openpgp'
import * as utils from './utils.js' import * as utils from './utils.js'
@ -51,17 +50,6 @@ const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-
// Initialize UI elements and event listeners // Initialize UI elements and event listeners
export function init() { 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 // Run context-dependent scripts
if (elFormEncrypt) { if (elFormEncrypt) {
runEncryptionForm() runEncryptionForm()
@ -349,8 +337,8 @@ const runArgon2GenerationUtility = () => {
elFeedback.innerHTML = ""; elFeedback.innerHTML = "";
} else { } else {
let feedbackContent = ""; let feedbackContent = "";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[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>"; feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
} }
if (!(elInput.value === elInput.value.toLowerCase())) { if (!(elInput.value === elInput.value.toLowerCase())) {
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>"; feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
@ -391,7 +379,7 @@ window.kx__fixArgon2Input = () => {
const elInput = document.querySelector('#form-util-argon2-generate .input'); const elInput = document.querySelector('#form-util-argon2-generate .input');
elInput.value = elInput.value.toLowerCase(); elInput.value = elInput.value.toLowerCase();
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
elInput.value = `openpgp4fpr:${elInput.value}`; elInput.value = `openpgp4fpr:${elInput.value}`;
} }
@ -414,8 +402,8 @@ const runBcryptGenerationUtility = () => {
elFeedback.innerHTML = ""; elFeedback.innerHTML = "";
} else { } else {
let feedbackContent = ""; let feedbackContent = "";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[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>"; feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
} }
if (!(elInput.value === elInput.value.toLowerCase())) { if (!(elInput.value === elInput.value.toLowerCase())) {
feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>"; feedbackContent += "❗ Valid proofs must be lowercase. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
@ -456,7 +444,7 @@ window.kx__fixBcryptInput = () => {
const elInput = document.querySelector('#form-util-bcrypt-generate .input'); const elInput = document.querySelector('#form-util-bcrypt-generate .input');
elInput.value = elInput.value.toLowerCase(); elInput.value = elInput.value.toLowerCase();
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) { if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
elInput.value = `openpgp4fpr:${elInput.value}`; elInput.value = `openpgp4fpr:${elInput.value}`;
} }

View file

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

8
views/429.pug Normal file
View file

@ -0,0 +1,8 @@
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.

48
views/apps.pug Normal file
View file

@ -0,0 +1,48 @@
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 block content
section section
h1= title h1= title
!{ content } | !{ content }

View file

@ -2,6 +2,9 @@ extends templates/base.pug
block content block content
section#search.form-wrapper 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") form(action="post")
input#query(type="search" name="query" required placeholder="Search for a profile") input#query(type="search" name="query" required placeholder="Search for a profile")
input(type="submit" value="Search") input(type="submit" value="Search")

View file

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

View file

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

View file

@ -1,54 +1,48 @@
extends templates/base.pug extends templates/base.pug
mixin generatePersona(persona, isPrimary) mixin generatePersona(persona, isPrimary)
h2 if persona.claims.length > 0
if persona.email h2
| Identity claims ( if persona.email
span.p-email #{persona.email} | Identity claims (
| ) span.p-email #{persona.email}
else | )
| Identity claims else
if isPrimary | Identity claims
small.primary primary if isPrimary
if persona.description small.primary primary
span.persona__description.p-comment if persona.description
<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> span.persona__description.p-comment
| #{persona.description} <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>
each claim in persona.claims | #{persona.description}
if claim.matches.length > 0 each claim in persona.claims
kx-claim.kx-item(data-claim=claim,data-status='running') if claim.matches.length > 0
details(aria-label="Claim") kx-claim.kx-item(data-claim=claim,data-status='running')
summary details(aria-label="Claim")
.info summary
p .info
span.title= claim.display.name img(src=`https://design.keyoxide.org/brands/service-providers/_/icon.svg` onerror="this.src='https://design.keyoxide.org/brands/service-providers/_/icon.svg'")
span.subtitle-wrapper p
| [ span.title= claim.display.profileName
span.subtitle= claim.display.serviceproviderName span.subtitle-wrapper
| ] | [
.icons span.subtitle= claim.display.serviceproviderName
.verificationStatus | ]
.inProgress .icons
<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> .verificationStatus
.success .inProgress
<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> <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>
.failure .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-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> <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 .content
.subsection .subsection
img(src='/static/img/link.png') img(src='/static/img/link.png')
div div
if (claim.display.url) p Claim link:
p Profile link: a(rel="me" href=claim.uri aria-label="Link to claim")= claim.uri
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 block content
if (data && 'publicKey' in data) if (data && 'publicKey' in data)
@ -117,6 +111,16 @@ block content
input(type='submit', name='submit', value='Generate profile') input(type='submit', name='submit', value='Generate profile')
unless (isSignature && !signature) 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 .profile__header
img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar") img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
@ -136,29 +140,28 @@ block content
each persona, index in data.personas each persona, index in data.personas
unless index == data.primaryPersonaIndex unless index == data.primaryPersonaIndex
+generatePersona(persona, false) +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 section
h2 Profile information h2 Profile information
h3 Public key if (data && data.publicKey)
kx-key.kx-item(data-keydata=data.publicKey) h3 Public key
details(aria-label="Key") kx-key.kx-item(data-keydata=data.publicKey)
summary details(aria-label="Key")
.info summary
p .info
span.title= data.identifier p
span.subtitle-wrapper span.title= data.identifier
| [ span.subtitle-wrapper
span.subtitle= data.publicKey.fetch.method | [
| ] span.subtitle= data.publicKey.fetch.method
.content | ]
.subsection .content
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 .subsection
img(src='/static/img/qrcode.png') img(src='/static/img/link.png')
div div
button(onClick=`showQR('${data.publicKey.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR p Key link:
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl

View file

@ -6,6 +6,7 @@ html(lang='en')
meta(name='theme-color' content='#fff') meta(name='theme-color' content='#fff')
meta(name='description' content='Modern and secure platform to manage a decentralized identity based on cryptographic keys') 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='shortcut icon' href='/favicon.svg')
link(rel='stylesheet' href='/static/main.css')
title= (title ? title : 'Keyoxide') title= (title ? title : 'Keyoxide')
include ../partials/header.pug include ../partials/header.pug
@ -16,7 +17,6 @@ html(lang='en')
include ../partials/footer.pug 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/openpgp.js' charset='utf-8')
script(type='application/javascript' defer src='/static/doipFetchers.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') 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 a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
| hashes useful to | hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid proof! | . Be sure to include "openpgp4fpr:" for a valid OpenPGP proof! For ASP, enter the entire URI beginning with "aspe:".
h3 Input h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input) input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash h3 Hash

View file

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

13469
yarn.lock

File diff suppressed because it is too large Load diff