Compare commits

...

171 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
Yarmo Mackenbach
139ab487a5
feat: Update privacy policy 2023-09-15 18:11:41 +02:00
Yarmo Mackenbach
c5ea6c3024
feat: rewrite templates 2023-09-15 18:11:41 +02:00
Yarmo Mackenbach
4cb94fd40e
feat: rewrite styles 2023-09-15 18:11:37 +02:00
Yarmo Mackenbach
2afcb240d3
feat: support scss 2023-09-15 15:23:30 +02:00
Yarmo Mackenbach
1dc3a08ccc Merge pull request 'configurable-scheme' (#164) from aspensmonster/keyoxide-web:configurable-scheme into dev
Reviewed-on: https://codeberg.org/keyoxide/keyoxide-web/pulls/164
2023-09-15 13:18:57 +00:00
Preston Maness
211d30b2c1 Include yarn.lock with updated esmock 2023-09-14 11:33:10 -05:00
Yarmo Mackenbach
31d9fb541e
fix: apply linting fix 2023-09-14 12:37:29 +02:00
Yarmo Mackenbach
339e0e9e0e
fix: wrong color 2023-09-14 12:35:39 +02:00
Yarmo Mackenbach
8eb90beda2
feat: display version 2023-09-14 12:31:44 +02:00
Yarmo Mackenbach
aeee377ea3
fix: remove obsolete argument 2023-09-14 12:30:12 +02:00
Yarmo Mackenbach
75709a1168
fix: avoid adding script if data is missing 2023-09-14 11:55:45 +02:00
Yarmo Mackenbach
bad5af6770
feat: update to node 20 2023-09-14 11:04:31 +02:00
Yarmo Mackenbach
6ec95cb659
fix: replace funding badge 2023-09-14 10:36:22 +02:00
Yarmo Mackenbach
49568b9422
fix: docker container reference 2023-09-14 10:31:11 +02:00
Preston Maness
4907f094ea Use latest esmock 2.5.0, remove --loader
--loader=esmock is required for Node versions less than v20.6.0.
2023-09-12 16:40:57 -05:00
Yarmo Mackenbach
0dd591c2c3
chore: release 4.0.2 2023-09-12 18:43:47 +02:00
Preston Maness
8793b2e15d Merge branch 'dev' into configurable-scheme 2023-09-10 22:17:02 -05:00
Preston Maness
cc343198b4 return from rejects 2023-09-08 21:19:56 +00:00
Yarmo Mackenbach
ae6a3b0fdb
fix: yarn script calls 2023-09-01 13:16:53 +02:00
Preston Maness
b9b7b33c83 Merge branch 'dev' into configurable-scheme 2023-08-31 20:32:20 -05:00
Preston Maness
563718ed51 Revert "Temporary fork while working on new providers"
This reverts commit ba62125b18.

Accidentally committed this on the wrong branch.
2023-08-31 20:28:05 -05:00
Yarmo Mackenbach
35c1dbc13d
fix: syntax 2023-08-31 14:50:50 +02:00
Yarmo Mackenbach
1373f14587
fix: handle doip promise rejection 2023-08-31 14:38:03 +02:00
Yarmo Mackenbach
44f9664300
chore: release 4.0.1 2023-08-28 14:48:42 +02:00
Yarmo Mackenbach
7f432d40ec
debug: debug Dockerfile 2023-08-28 14:44:06 +02:00
Yarmo Mackenbach
4e0562a2db
chore: release 4.0.0 2023-08-28 13:24:57 +02:00
Yarmo Mackenbach
0ea467ab3b
fix: fix ci copy error 2023-08-26 10:52:46 +02:00
Yarmo Mackenbach
876959dd7c
fix: fix ci copy error 2023-08-25 16:32:07 +02:00
Yarmo Mackenbach
fef0e11052
fix: fix ci copy error 2023-08-25 15:55:54 +02:00
Yarmo Mackenbach
bc2f2b6a86
fix: fix ci copy error 2023-08-25 15:53:42 +02:00
Yarmo Mackenbach
7cc54d9fa2
fix: fix ci copy error 2023-08-25 15:49:55 +02:00
Yarmo Mackenbach
3285d43be4
chore: update ci syntax 2023-08-25 15:20:11 +02:00
Preston Maness
ba62125b18 Temporary fork while working on new providers 2023-08-19 15:49:53 -05:00
Ty
d34d3027ee
Update to yarn modern, patch doip for SASL auth 2023-08-03 21:09:21 -06:00
Preston Maness
03d7bf3446 Test adjustments for doipjs 1.0 and v3 API 2023-07-14 18:38:06 -05:00
Preston Maness
0b9a00c69e Merge branch 'dev' into configurable-scheme initial merge conflict
resolution

Tests not updated yet
2023-07-14 17:03:30 -05:00
Yarmo Mackenbach
b8114bad9e
fix: fix linting issues 2023-07-13 11:19:48 +02:00
Yarmo Mackenbach
2760adf5f8
feat: update project 2023-07-13 11:12:07 +02:00
Yarmo Mackenbach
348df9f17f
feat: update templates 2023-07-13 11:11:33 +02:00
Yarmo Mackenbach
9864a5fb66
feat: update client code 2023-07-13 11:11:16 +02:00
Yarmo Mackenbach
59fc51c407
feat: update server code 2023-07-13 11:10:58 +02:00
Yarmo Mackenbach
6676c78961
feat: introduce API v3, obsolete older versions 2023-07-13 11:00:56 +02:00
Preston Maness
ceb743081f Merge branch 'esmock-tests' into configurable-scheme 2023-07-04 10:44:11 -05:00
Preston Maness
f74260f79b Use esmock for mocking dependencies 2023-07-04 10:43:41 -05:00
Yarmo Mackenbach
3ff82ff46d
fix: fix linting issues 2023-07-04 12:49:11 +02:00
Yarmo Mackenbach
375173ecdf
feat: add lint script to project 2023-07-04 12:44:29 +02:00
Yarmo Mackenbach
5398e1d89a
chore: update deps 2023-07-04 12:43:55 +02:00
Preston Maness
f5ef4b6623 Initial testing of configurable scheme code with mocks 2023-07-04 00:06:33 -05:00
Preston Maness
ba9ae78d7d Update and add tests for configurable scheme 2023-07-03 15:02:15 -05:00
Preston Maness
f620684f35 Revert "Quickstart guidance updates"
This reverts commit 5e0561df7e.
2023-07-03 14:53:15 -05:00
Preston Maness
59e4281b43 Revert "Add SCHEME environment variable to vscodium launcher"
This reverts commit 9196aeaad6.
2023-07-03 14:53:06 -05:00
Preston Maness
939f118931 Revert "fix expected URL with SCHEME env var added"
This reverts commit 21bae8df69.
2023-07-03 14:52:54 -05:00
Preston Maness
55847461dc Revert "Add empty matrix env vars to launch.json"
This reverts commit 916dfcc6d3.
2023-07-03 14:52:42 -05:00
Preston Maness
c75291c201 Revert "Update tests to pass in scheme, update dev quick start instructions"
This reverts commit 0dfc0cbe10.
2023-07-03 14:52:28 -05:00
Preston Maness
0dfc0cbe10 Update tests to pass in scheme, update dev quick start instructions 2023-07-03 14:33:33 -05:00
Preston Maness
916dfcc6d3 Add empty matrix env vars to launch.json 2023-07-03 14:33:33 -05:00
Preston Maness
cf11a3f343 Linting change 2023-07-03 14:33:33 -05:00
Preston Maness
223e39209c Make sure default scheme is set here too 2023-07-03 14:33:33 -05:00
Preston Maness
21bae8df69 fix expected URL with SCHEME env var added 2023-07-03 14:33:33 -05:00
Preston Maness
9196aeaad6 Add SCHEME environment variable to vscodium launcher 2023-07-03 14:33:33 -05:00
Preston Maness
ecc789a4a9 first stab at it from keyoxide-web end 2023-07-03 14:33:33 -05:00
Preston Maness
5e0561df7e Quickstart guidance updates 2023-07-03 14:33:33 -05:00
Yarmo Mackenbach
04180468ba
fix: make sure primaryUserIndex exists 2023-06-19 10:37:31 +02:00
Yarmo Mackenbach
786e9cdd3e
chore: Apply linting & formatting 2023-03-28 10:37:43 +02:00
Yarmo Mackenbach
99fe8edd71
feat: Add CI for testing 2023-03-28 10:35:14 +02:00
Yarmo Mackenbach
939b620e8e
feat: Add rome tool 2023-03-28 10:25:48 +02:00
Yarmo Mackenbach
2be4e304a1
chore: Release 3.6.4 2023-03-27 19:36:22 +02:00
Yarmo Mackenbach
eadf5c14c3
feat: Add graphql proxy endpoint 2023-03-27 19:27:07 +02:00
Yarmo Mackenbach
8a311f5dc6 chore: Release 3.6.3 2023-03-27 19:03:46 +02:00
Yarmo Mackenbach
d7fce5d236 feat: Add OC donation button 2023-03-27 19:03:46 +02:00
Yarmo Mackenbach
ae32f05197
chore: Update doipjs dep 2023-03-27 18:48:20 +02:00
73 changed files with 11384 additions and 7817 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.

11
.gitignore vendored
View file

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

View file

@ -1,4 +1,10 @@
pipeline:
steps:
test:
image: node
commands:
- yarn --pure-lockfile
- yarn run test
build-latest-container:
when:
branch: main
@ -12,6 +18,9 @@ pipeline:
from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web
tags: latest
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
build-tag-container:
when:
@ -26,6 +35,9 @@ pipeline:
from_secret: codeberg_password
repo: codeberg.org/keyoxide/keyoxide-web
auto_tag: true
build_args_from_env:
- CI_COMMIT_SHA
- CI_COMMIT_BRANCH
build-dev-container:
when:
@ -39,4 +51,7 @@ pipeline:
password:
from_secret: codeberg_password
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,115 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [4.2.7] - 2024-02-01
### Added
- Server version HTTP endpoint
- Server version in footer
### Changed
- Update doipjs to 1.2.9
### Notes
- The version of keyoxide-web that the server is running can now be requested at the `/.well-known/keyoxide/version` HTTP endpoint. It is also found in the footer of every page.
- Version 1.2.9 of doipjs notably adds support for ORCiD claim verification, as well as a couple of performance improvements.
## [4.2.6] - 2024-01-24
### Added
- Support openpgp4fpr: queries in URL
- Proxy routes for openpgp and aspe
### Changed
- Update doipjs to 1.2.8
### Notes
- This version notably adds support for OpenPGP and ASPE claim verification.
## [4.2.5] - 2023-10-09
### Changed
- Update doipjs to 1.2.7
## [4.2.4] - 2023-10-09
### Changed
- Update doipjs to 1.2.6
## [4.2.3] - 2023-10-05
### Added
- Themeable profile pages
- Apps page
### Changed
- Update doipjs to 1.2.5
- Make Dicebear API domain configurable
### Fixed
- Catch errors potentially thrown by function
- Update JSON schemas
- Icon URL generation in profile view
### Notes
- ASP profiles use the Dicebear API to generate avatars. By default, Keyoxide
uses the official api.dicebear.com instance. To use a custom Dicebear instance,
set the DICEBEAR_API_HOSTNAME environment variable to its hostname.
## [4.2.2] - 2023-10-03
### Changed
- Update doipjs to 1.2.2
## [4.2.1] - 2023-09-23
### Fixed
- Tweak the rate limiter parameters
## [4.2.0] - 2023-09-23
### Added
- Profile request rate limiter (experimental; opt-in)
### Changed
- Update doipjs to 1.2.1
- Add logging to OpenPGP profile creation
- Add debug data to logs
### Fixed
- Make hash utils aware of ASPE
## [4.1.1] - 2023-09-21
### Changed
- Update doipjs to 1.1.0
### Fixed
- Missing rel=me for ambiguous claims
- OpenPGP cache logic
## [4.1.0] - 2023-09-18
### Changed
- Redesign
- Update doipjs to 1.0.1
- Update node to 20
- Make https scheme for proxy calls optional
- Display site version in footer
## [4.0.2] - 2023-09-12
### Fixed
- Handle doip promise rejection
- yarn script calls
## [4.0.1] - 2023-08-28
### Fixed
- CI docker builds
## [4.0.0] - 2023-08-28
### Added
- ASPE support
- Rome linting and formatting
- API v3
### Changed
- Updated doipjs to 1.0.0
### Fixed
- Missing primaryUserIndex
### Removed
- API v0, v1, v2
## [3.6.4] - 2023-03-27
### Fixed
- Missing /graphql proxy API endpoint
## [3.6.3] - 2023-03-27
### Added
- Basic logging
### Changed
- Replaced liberapay & kofi with opencollective
- Updated doipjs to 0.18.3
## [3.6.2] - 2023-03-08
### Added
- Dark theme

View file

@ -1,14 +1,21 @@
FROM node:16-alpine as builder
FROM node:20-alpine as builder
WORKDIR /app
COPY . .
RUN yarn --pure-lockfile
RUN yarn run build
RUN corepack enable
RUN yarn install --immutable
RUN yarn run build:server && yarn run build:static
###
FROM node:16-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
COPY --from=builder /app/package.json /app/package.json
@ -19,4 +26,4 @@ COPY --from=builder /app/static /app/static
EXPOSE 3000
CMD node --experimental-fetch ./dist/
CMD node ./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)
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=for-the-badge)](https://codeberg.org/keyoxide/web/src/branch/main/LICENSE)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=for-the-badge)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=for-the-badge)](https://fosstodon.org/@keyoxide)
[![Liberapay receiving](https://img.shields.io/liberapay/receives/keyoxide?style=for-the-badge)](https://liberapay.com/Keyoxide)
[![status-badge](https://ci.codeberg.org/api/badges/5919/status.svg)](https://ci.codeberg.org/repos/5919)
[![License](https://img.shields.io/badge/license-AGPL--v3-blue?style=flat)](https://codeberg.org/keyoxide/keyoxide-web/src/branch/main/LICENSE)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/keyoxide/keyoxide?sort=semver&style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Docker Pulls](https://img.shields.io/docker/pulls/keyoxide/keyoxide?style=flat)](https://hub.docker.com/r/keyoxide/keyoxide)
[![Mastodon Follow](https://img.shields.io/mastodon/follow/247838?domain=https%3A%2F%2Ffosstodon.org&style=flat)](https://fosstodon.org/@keyoxide)
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/keyoxide?style=flat)](https://opencollective.com/keyoxide)
[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!
@ -17,38 +17,50 @@ Self-hosting Keyoxide is an important aspect of the project. Users need to trust
The Docker container allows you to easily self-host the [Keyoxide](https://keyoxide.org) project. To get started, simply run:
`docker run -d -p 3000:3000 keyoxide/keyoxide:stable`
```sh
docker run -d -p 3000:3000 codeberg.org/keyoxide/keyoxide-web:latest
```
Keyoxide will now be available by visiting http://localhost:3000.
More information available in the [documentation](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
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
- Suggesting new features
- Improving documentation
- Writing code to fix bugs and features
- Promoting decentralized identity and web3.0
- fork the repository and play around
- submit PRs to [implement new features or fix bugs](https://codeberg.org/keyoxide/keyoxide-web/issues)
If you are new to contributing to open source software, we'd love to help you! To get started, here's a [list of "good first issues"](https://codeberg.org/keyoxide/keyoxide-web/issues?q=&type=all&state=open&labels=183598) that you could look into.
Everyone is invited to:
- find and [report bugs](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
- suggesting [new features](https://codeberg.org/keyoxide/keyoxide-web/issues/new/choose)
- [help with translations](https://translate.codeberg.org/projects/keyoxide/)
- [improve documentation](https://codeberg.org/keyoxide/keyoxide-docs)
- start using open source software and promote it
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
- 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)
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.

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

@ -8,21 +8,21 @@ There are no accounts on Keyoxide. Never does Keyoxide need to know any of your
## Profile pages
To generate a profile page, Keyoxide will look at the URL and fetch the associated OpenPGP key. The OpenPGP key is parsed on the server and the claims are rendered unverified then sent to the browser. The browser will then attempt to verify the claims by fetching the required proofs, usually JSON documents.
To generate a profile page, Keyoxide will look at the URL and fetch the associated OpenPGP key or ASP profile. The fetched document is parsed on the server, the claims are rendered unverified then sent to the browser. The browser will then attempt to verify the claims by fetching the required proofs, usually JSON documents.
If a browser cannot fetch the proof (for example, due to CORS restraints), it will ask a proxy server to fetch the proof instead if a proxy is configured by the instance administrator.
OpenPGP keys, profile pages and claim verifications are not cached on the server.
Profile pages and claim verifications are not cached on the server. OpenPGP keys may be cached for a minute to alleviate strain on keyservers.
## Donations
Donations are handled by Stripe. Keyoxide does not store personal or payment-related information.
Donations are handled by OpenCollective. Keyoxide does not store personal or payment-related information.
How Stripe processes the data is covered by the [Stripe privacy policy](https://stripe.com/privacy).
How OpenCollective processes the data is covered by the [OpenCollective Privacy Policy](https://opencollective.com/privacypolicy).
## Keyoxide instances
Each Keyoxide instance is administered by different people using different technologies and different configurations. This document cannot account for the way each particular instance handles/stores/processes the HTTP requests.
Each Keyoxide instance is administered by different people using different infrastructure and different configurations. This document cannot account for the way each particular instance handles/stores/processes the HTTP requests.
## Keyoxide.org instance

View file

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

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,9 +1,7 @@
{
"execArgs": [
"--experimental-fetch"
],
"env": {
"NODE_ENV": "development"
"NODE_ENV": "development",
"LOG_LEVEL": "debug"
},
"ext": "js,json,css,pug,md"
}

View file

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

View file

@ -1,399 +0,0 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import express from 'express'
import { check, validationResult } from 'express-validator'
import Ajv from 'ajv'
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
import * as dotenv from 'dotenv'
dotenv.config()
const router = express.Router()
const ajv = new Ajv({ coerceTypes: true })
const apiProfileSchema = {
type: 'object',
properties: {
keyData: {
type: 'object',
properties: {
fingerprint: {
type: 'string'
},
openpgp4fpr: {
type: 'string'
},
users: {
type: 'array',
items: {
type: 'object',
properties: {
userData: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
comment: { type: 'string' },
isPrimary: { type: 'boolean' },
isRevoked: { type: 'boolean' }
}
},
claims: {
type: 'array',
items: {
type: 'object',
properties: {
claimVersion: { type: 'integer' },
uri: { type: 'string' },
fingerprint: { type: 'string' },
status: { type: 'string' },
matches: {
type: 'array',
items: {
type: 'object',
properties: {
serviceProvider: {
type: 'object',
properties: {
type: { type: 'string' },
name: { type: 'string' }
}
},
match: {
type: 'object',
properties: {
regularExpression: { type: 'object' },
isAmbiguous: { type: 'boolean' }
}
},
profile: {
type: 'object',
properties: {
display: { type: 'string' },
uri: { type: 'string' },
qr: { type: 'string' }
}
},
proof: {
type: 'object',
properties: {
uri: { type: 'string' },
request: {
type: 'object',
properties: {
fetcher: { type: 'string' },
access: { type: 'string' },
format: { type: 'string' },
data: { type: 'object' }
}
}
}
},
claim: {
type: 'object',
properties: {
format: { type: 'string' },
relation: { type: 'string' },
path: {
type: 'array',
items: {
type: 'string'
}
}
}
}
}
}
},
verification: {
type: 'object'
},
summary: {
type: 'object',
properties: {
profileName: { type: 'string' },
profileURL: { type: 'string' },
serviceProviderName: { type: 'string' },
isVerificationDone: { type: 'boolean' },
isVerified: { type: 'boolean' }
}
}
}
}
}
}
}
},
primaryUserIndex: {
type: 'integer'
},
key: {
type: 'object',
properties: {
data: { type: 'object' },
fetchMethod: { type: 'string' },
uri: { type: 'string' }
}
}
}
},
keyoxide: {
type: 'object',
properties: {
url: { type: 'string' }
}
},
extra: {
type: 'object',
properties: {
avatarURL: { type: 'string' }
}
},
errors: {
type: 'array'
}
},
required: ['keyData', 'keyoxide', 'extra', 'errors'],
additionalProperties: false
}
const apiProfileValidate = ajv.compile(apiProfileSchema)
const doVerification = async (data) => {
const promises = []
const results = []
const verificationOptions = {
proxy: {
hostname: process.env.PROXY_HOSTNAME,
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
}
}
// Return early if no users in key
if (!data.keyData.users) {
return data
}
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
const user = data.keyData.users[iUser]
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
const claim = user.claims[iClaim]
promises.push(
new Promise((resolve, reject) => {
(async () => {
await claim.verify(verificationOptions)
results.push([iUser, iClaim, claim])
resolve()
})()
})
)
}
}
await Promise.all(promises)
results.forEach(result => {
data.keyData.users[result[0]].claims[result[1]] = result[2]
})
return data
}
const sanitize = (data) => {
const dataClone = JSON.parse(JSON.stringify(data))
if (dataClone.keyData.users) {
for (let iUser = 0; iUser < dataClone.keyData.users.length; iUser++) {
const user = dataClone.keyData.users[iUser]
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
const claim = user.claims[iClaim]
// TODO Fix upstream
for (let iMatch = 0; iMatch < claim.matches.length; iMatch++) {
const match = claim.matches[iMatch]
if (Array.isArray(match.claim)) {
match.claim = match.claim[0]
}
}
// TODO Fix upstream
if (!claim.verification) {
claim.verification = {}
}
// TODO Fix upstream
claim.matches.forEach(match => {
match.proof.request.access = ['generic', 'nocors', 'granted', 'server'][match.proof.request.access]
match.claim.format = ['uri', 'fingerprint', 'message'][match.claim.format]
match.claim.relation = ['contains', 'equals', 'oneof'][match.claim.relation]
})
data.keyData.users[iUser].claims[iClaim] = claim
}
}
}
const valid = apiProfileValidate(data)
if (!valid) {
throw new Error('Profile data sanitization error')
}
return data
}
const addSummaryToClaims = (data) => {
// Return early if no users in key
if (!data.keyData.users) {
return data
}
// To be removed when data is added by DOIP library
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
const user = data.keyData.users[userIndex]
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
const claim = user.claims[claimIndex]
const isVerificationDone = claim.status === 'verified'
const isVerified = isVerificationDone ? claim.verification.result : false
const isAmbiguous = isVerified
? false
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
data.keyData.users[userIndex].claims[claimIndex].summary = {
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
isVerificationDone,
isVerified
}
}
}
return data
}
router.get('/profile/fetch',
check('query').exists(),
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
check('doVerification').default(false).isBoolean().toBoolean(),
check('returnPublicKey').default(false).isBoolean().toBoolean(),
async (req, res) => {
const valRes = validationResult(req)
if (!valRes.isEmpty()) {
res.status(400).send(valRes)
return
}
// Generate profile
let data
switch (req.query.protocol) {
case 'wkd':
data = await generateWKDProfile(req.query.query)
break
case 'hkp':
data = await generateHKPProfile(req.query.query)
break
default:
data = await generateAutoProfile(req.query.query)
break
}
if (data.errors.length > 0) {
delete data.key
res.status(500).send(data)
}
// Return public key
if (req.query.returnPublicKey) {
data.keyData.key.data = data.key.publicKey
}
delete data.key
// Do verification
if (req.query.doVerification) {
data = await doVerification(data)
}
try {
// Sanitize JSON
data = sanitize(data)
} catch (error) {
data.keyData = {}
data.extra = {}
data.errors = [error.message]
}
// Add missing data
data = addSummaryToClaims(data)
let statusCode = 200
if (data.errors.length > 0) {
statusCode = 500
}
res.status(statusCode).send(data)
}
)
router.get('/profile/verify',
check('data').exists().isJSON(),
async (req, res) => {
const valRes = validationResult(req)
if (!valRes.isEmpty()) {
res.status(400).send(valRes)
return
}
// Do verification
let data = await doVerification(req.query.data)
try {
// Sanitize JSON
data = sanitize(data)
} catch (error) {
data.keyData = {}
data.extra = {}
data.errors = [error.message]
}
// Add missing data
data = addSummaryToClaims(data)
let statusCode = 200
if (data.errors.length > 0) {
statusCode = 500
}
res.status(statusCode).send(data)
}
)
export default router

View file

@ -1,23 +0,0 @@
/*
Copyright 2022 Yarmo Mackenbach
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import express from 'express'
const router = express.Router()
router.get('*', (req, res) => {
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v2 API endpoint')
})
export default router

View file

@ -1,370 +0,0 @@
/*
Copyright (C) 2022 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import express from 'express'
import { check, validationResult } from 'express-validator'
import Ajv from 'ajv'
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
import * as dotenv from 'dotenv'
dotenv.config()
const router = express.Router()
const ajv = new Ajv({ coerceTypes: true })
const apiProfileSchema = {
type: 'object',
properties: {
keyData: {
type: 'object',
properties: {
fingerprint: {
type: 'string'
},
openpgp4fpr: {
type: 'string'
},
users: {
type: 'array',
items: {
type: 'object',
properties: {
userData: {
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
email: { type: 'string' },
comment: { type: 'string' },
isPrimary: { type: 'boolean' },
isRevoked: { type: 'boolean' }
}
},
claims: {
type: 'array',
items: {
type: 'object',
properties: {
claimVersion: { type: 'integer' },
uri: { type: 'string' },
fingerprint: { type: 'string' },
status: { type: 'string' },
matches: {
type: 'array',
items: {
type: 'object',
properties: {
serviceProvider: {
type: 'object',
properties: {
type: { type: 'string' },
name: { type: 'string' }
}
},
match: {
type: 'object',
properties: {
regularExpression: { type: 'object' },
isAmbiguous: { type: 'boolean' }
}
},
profile: {
type: 'object',
properties: {
display: { type: 'string' },
uri: { type: 'string' },
qr: { type: 'string' }
}
},
proof: {
type: 'object',
properties: {
uri: { type: 'string' },
request: {
type: 'object',
properties: {
fetcher: { type: 'string' },
access: { type: 'string' },
format: { type: 'string' },
data: { type: 'object' }
}
}
}
},
claim: {
type: 'array',
items: {
type: 'object',
properties: {
format: { type: 'string' },
relation: { type: 'string' },
path: {
type: 'array',
items: {
type: 'string'
}
}
}
}
}
}
}
},
verification: {
type: 'object'
},
summary: {
type: 'object',
properties: {
profileName: { type: 'string' },
profileURL: { type: 'string' },
serviceProviderName: { type: 'string' },
isVerificationDone: { type: 'boolean' },
isVerified: { type: 'boolean' }
}
}
}
}
}
}
}
},
primaryUserIndex: {
type: 'integer'
},
key: {
type: 'object',
properties: {
data: { type: 'object' },
fetchMethod: { type: 'string' },
uri: { type: 'string' }
}
}
}
},
keyoxide: {
type: 'object',
properties: {
url: { type: 'string' }
}
},
extra: {
type: 'object',
properties: {
avatarURL: { type: 'string' }
}
},
errors: {
type: 'array'
}
},
required: ['keyData', 'keyoxide', 'extra', 'errors'],
additionalProperties: false
}
const apiProfileValidate = ajv.compile(apiProfileSchema)
const doVerification = async (data) => {
const promises = []
const results = []
const verificationOptions = {
proxy: {
hostname: process.env.PROXY_HOSTNAME,
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
}
}
// Return early if no users in key
if (!data.keyData.users) {
return data
}
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
const user = data.keyData.users[iUser]
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
const claim = user.claims[iClaim]
promises.push(
new Promise((resolve, reject) => {
(async () => {
await claim.verify(verificationOptions)
results.push([iUser, iClaim, claim])
resolve()
})()
})
)
}
}
await Promise.all(promises)
results.forEach(result => {
data.keyData.users[result[0]].claims[result[1]] = result[2]
})
return data
}
const sanitize = (data) => {
const valid = apiProfileValidate(data)
if (!valid) {
throw new Error('Profile data sanitization error')
}
return data
}
const addSummaryToClaims = (data) => {
// Return early if no users in key
if (!data.keyData.users) {
return data
}
// To be removed when data is added by DOIP library
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
const user = data.keyData.users[userIndex]
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
const claim = user.claims[claimIndex]
const isVerificationDone = claim.status === 'verified'
const isVerified = isVerificationDone ? claim.verification.result : false
const isAmbiguous = isVerified
? false
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
data.keyData.users[userIndex].claims[claimIndex].summary = {
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
isVerificationDone,
isVerified
}
}
}
return data
}
router.get('/fetch',
check('query').exists(),
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
check('doVerification').default(false).isBoolean().toBoolean(),
check('returnPublicKey').default(false).isBoolean().toBoolean(),
async (req, res) => {
const valRes = validationResult(req)
if (!valRes.isEmpty()) {
res.status(400).send(valRes)
return
}
// Generate profile
let data
switch (req.query.protocol) {
case 'wkd':
data = await generateWKDProfile(req.query.query)
break
case 'hkp':
data = await generateHKPProfile(req.query.query)
break
default:
data = await generateAutoProfile(req.query.query)
break
}
if (data.errors.length > 0) {
delete data.key
res.status(500).send(data)
}
// Return public key
if (req.query.returnPublicKey) {
data.keyData.key.data = data.key.publicKey
}
delete data.key
// Do verification
if (req.query.doVerification) {
data = await doVerification(data)
}
try {
// Sanitize JSON
data = sanitize(data)
} catch (error) {
data.keyData = {}
data.extra = {}
data.errors = [error.message]
}
// Add missing data
data = addSummaryToClaims(data)
let statusCode = 200
if (data.errors.length > 0) {
statusCode = 500
}
res.status(statusCode).send(data)
}
)
router.get('/verify',
check('data').exists().isJSON(),
async (req, res) => {
const valRes = validationResult(req)
if (!valRes.isEmpty()) {
res.status(400).send(valRes)
return
}
// Do verification
let data = await doVerification(req.query.data)
try {
// Sanitize JSON
data = sanitize(data)
} catch (error) {
data.keyData = {}
data.extra = {}
data.errors = [error.message]
}
// Add missing data
data = addSummaryToClaims(data)
let statusCode = 200
if (data.errors.length > 0) {
statusCode = 500
}
res.status(statusCode).send(data)
}
)
export default router

View file

@ -1,5 +1,5 @@
/*
Copyright (C) 2022 Yarmo Mackenbach
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free

View file

@ -0,0 +1,197 @@
/*
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import express from 'express'
import { check, validationResult } from 'express-validator'
import Ajv from 'ajv/dist/2020.js'
import * as dotenv from 'dotenv'
import { Claim } from 'doipjs'
import { generateAspeProfile, generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
import { claimSchema, personaSchema, profileSchema, serviceProviderSchema } from '../../schemas.js'
dotenv.config()
const router = express.Router()
const ajv = new Ajv({
schemas: [profileSchema, personaSchema, claimSchema, serviceProviderSchema]
})
const apiProfileValidate = ajv.compile(profileSchema)
const doVerification = async (profile) => {
const promises = []
const results = []
const verificationOptions = {
proxy: {
hostname: process.env.PROXY_HOSTNAME,
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
}
}
// Return early if no users in key
if (!profile.personas) {
return profile
}
for (let iUser = 0; iUser < profile.personas.length; iUser++) {
const user = profile.personas[iUser]
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
const claim = user.claims[iClaim]
promises.push(
new Promise((resolve, reject) => {
(async () => {
await claim.verify(verificationOptions)
results.push([iUser, iClaim, claim])
resolve()
})()
})
)
}
}
await Promise.all(promises)
results.forEach(result => {
profile.personas[result[0]].claims[result[1]] = result[2]
})
return profile
}
const validate = (profile) => {
const valid = apiProfileValidate(profile)
if (!valid) {
throw new Error(`Profile data validation error: ${apiProfileValidate.errors.map(x => x.message).join(', ')}`)
}
}
router.get('/fetch',
check('query').exists(),
check('protocol').optional().toLowerCase().isIn(['aspe', 'hkp', 'wkd']),
check('doVerification').default(false).isBoolean().toBoolean(),
async (req, res) => {
const valRes = validationResult(req)
if (!valRes.isEmpty()) {
res.status(400).send(valRes)
return
}
// Generate profile
let data
switch (req.query.protocol) {
case 'aspe':
data = await generateAspeProfile(req.query.query)
break
case 'wkd':
data = await generateWKDProfile(req.query.query)
break
case 'hkp':
data = await generateHKPProfile(req.query.query)
break
default:
data = await generateAutoProfile(req.query.query)
break
}
if ('errors' in data && data.errors.length > 0) {
res.status(500).send(data)
}
// Do verification
if (req.query.doVerification) {
data = await doVerification(data)
}
try {
data = data.toJSON()
} catch (error) {
data = {
errors: [error.message]
}
}
try {
// Validate JSON
validate(data)
} catch (error) {
data = {
errors: [error.message]
}
}
let statusCode = 200
if ('errors' in data && data.errors.length > 0) {
statusCode = 500
}
res.status(statusCode).send(data)
}
)
router.get('/verify',
check('data').exists().isJSON(),
async (req, res) => {
const valRes = validationResult(req)
if (!valRes.isEmpty()) {
res.status(400).send(valRes)
return
}
const profile = Claim.fromJson(req.query.data)
// Do verification
let data = await doVerification(profile)
try {
data = data.toJSON()
} catch (error) {
data = {
errors: [error.message]
}
}
try {
// Validate JSON
validate(data)
} catch (error) {
data = {
errors: [error.message]
}
}
let statusCode = 200
if ('errors' in data) {
statusCode = 500
}
res.status(statusCode).send(data)
}
)
export default router

View file

@ -42,7 +42,12 @@ const opts = {
privateKey: process.env.ACTIVITYPUB_PRIVATE_KEY || null
},
irc: {
nick: process.env.IRC_NICK || null
nick: process.env.IRC_NICK || null,
sasl: Object.keys(process.env).filter(k => k.startsWith('IRC_SASL_USERNAME_')).map(k => ({
username: process.env[k],
password: process.env[`IRC_SASL_PASSWORD_${k.substring('IRC_SASL_USERNAME_'.length)}`],
domainRegex: process.env[`IRC_SASL_DOMAIN_REGEX_${k.substring('IRC_SASL_USERNAME_'.length)}`]
}))
},
matrix: {
instance: process.env.MATRIX_INSTANCE || null,
@ -51,9 +56,6 @@ const opts = {
telegram: {
token: process.env.TELEGRAM_TOKEN || null
},
twitter: {
bearerToken: process.env.TWITTER_BEARER_TOKEN || null
},
xmpp: {
service: process.env.XMPP_SERVICE || null,
username: process.env.XMPP_USERNAME || null,
@ -95,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
router.get('/dns', query('domain').isFQDN(), (req, res) => {
const errors = validationResult(req)
@ -118,9 +154,7 @@ router.get(
query('id').isEmail(),
async (req, res) => {
if (
!opts.claims.xmpp.service ||
!opts.claims.xmpp.username ||
!opts.claims.xmpp.password
!((opts.claims.xmpp.service && opts.claims.xmpp.username) && opts.claims.xmpp.password)
) {
return res.status(501).json({ errors: 'XMPP not enabled on server' })
}
@ -140,33 +174,13 @@ router.get(
}
)
// Twitter route
router.get('/twitter', query('tweetId').isInt(), async (req, res) => {
if (!opts.claims.twitter.bearerToken) {
return res.status(501).json({ errors: 'Twitter not enabled on server' })
}
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.twitter
.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 })
})
})
// Matrix route
router.get(
'/matrix',
query('roomId').isString(),
query('eventId').isString(),
async (req, res) => {
if (!opts.claims.matrix.instance || !opts.claims.matrix.accessToken) {
if (!(opts.claims.matrix.instance && opts.claims.matrix.accessToken)) {
return res.status(501).json({ errors: 'Matrix not enabled on server' })
}
const errors = validationResult(req)
@ -277,4 +291,26 @@ router.get(
}
)
// GraphQL route
router.get(
'/graphql',
query('url').isURL(),
query('query').isString(),
async (req, res) => {
const errors = validationResult(req)
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() })
}
fetcher.graphql
.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 })
})
}
)
export default router

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

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

View file

@ -28,16 +28,19 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import express from 'express'
import apiRouter0 from '../api/v0/index.js'
import apiRouter1 from '../api/v1/index.js'
import apiRouter2 from '../api/v2/index.js'
import apiRouter3 from '../api/v3/index.js'
const router = express.Router()
if ((process.env.ENABLE_MAIN_MODULE ?? 'true') === 'true') {
router.use('/0', apiRouter0)
}
router.use('/1', apiRouter1)
router.use('/2', apiRouter2)
router.get('/0', (req, res) => {
return res.status(501).send('Proxy v0 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
})
router.get('/1', (req, res) => {
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
})
router.get('/2', (req, res) => {
return res.status(501).send('Proxy v2 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
})
router.use('/3', apiRouter3)
export default router

View file

@ -30,31 +30,23 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import express from 'express'
import markdownImport from 'markdown-it'
import { readFileSync } from 'fs'
import demoData from '../server/demo.js'
import { getMetaFromReq } from '../server/utils.js'
const router = express.Router()
const md = markdownImport({ typographer: true })
router.get('/', (req, res) => {
const highlights = []
for (let index = 1; index < 4; index++) {
if (process.env[`KX_HIGHLIGHTS_${index}_NAME`] &&
process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) {
highlights.push({
name: process.env[`KX_HIGHLIGHTS_${index}_NAME`],
description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`],
fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]
})
}
}
res.render('index', { meta: getMetaFromReq(req) })
})
res.render('index', { highlights, demoData })
router.get('/apps', (req, res) => {
res.render('apps', { title: 'Apps', meta: getMetaFromReq(req) })
})
router.get('/privacy', (req, res) => {
const rawContent = readFileSync('./content/privacy-policy.md', 'utf8')
const content = md.render(rawContent)
res.render('article', { title: 'Privacy policy', content })
res.render('article', { title: 'Privacy policy', content, meta: getMetaFromReq(req) })
})
router.get('/.well-known/webfinger', (req, res) => {
@ -75,6 +67,12 @@ router.get('/.well-known/webfinger', (req, res) => {
res.json(body)
})
router.get('/.well-known/keyoxide/version', async (req, res) => {
// TODO Support responding with JSON object when requested
const meta = getMetaFromReq(req)
return res.status(200).contentType('text/plain').send(meta.keyoxide.semver)
})
router.get('/users/keyoxide', (req, res) => {
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')

View file

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

View file

@ -28,52 +28,65 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import express from 'express'
import { escapedParam, getMetaFromReq } from '../server/utils.js'
const router = express.Router()
router.get('/', function (req, res) {
res.render('util/index')
res.render('util/index', { meta: getMetaFromReq(req) })
})
router.get('/profile-url', function (req, res) {
res.render('util/profile-url')
})
router.get('/profile-url/:input', function (req, res) {
res.render('util/profile-url', { input: req.params.input })
res.render('util/profile-url', { meta: getMetaFromReq(req) })
})
router.get('/profile-url/:input',
escapedParam('input'),
function (req, res) {
res.render('util/profile-url', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qr', function (req, res) {
res.render('util/qr')
})
router.get('/qr/:input', function (req, res) {
res.render('util/qr', { input: req.params.input })
res.render('util/qr', { meta: getMetaFromReq(req) })
})
router.get('/qr/:input',
escapedParam('input'),
function (req, res) {
res.render('util/qr', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/qrfp', function (req, res) {
res.render('util/qrfp')
})
router.get('/qrfp/:input', function (req, res) {
res.render('util/qrfp', { input: req.params.input })
res.render('util/qrfp', { meta: getMetaFromReq(req) })
})
router.get('/qrfp/:input',
escapedParam('input'),
function (req, res) {
res.render('util/qrfp', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/wkd', function (req, res) {
res.render('util/wkd')
})
router.get('/wkd/:input', function (req, res) {
res.render('util/wkd', { input: req.params.input })
res.render('util/wkd', { meta: getMetaFromReq(req) })
})
router.get('/wkd/:input',
escapedParam('input'),
function (req, res) {
res.render('util/wkd', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/argon2', function (req, res) {
res.render('util/argon2')
})
router.get('/argon2/:input', function (req, res) {
res.render('util/argon2', { input: req.params.input })
res.render('util/argon2', { meta: getMetaFromReq(req) })
})
router.get('/argon2/:input',
escapedParam('input'),
function (req, res) {
res.render('util/argon2', { input: req.params.input, meta: getMetaFromReq(req) })
})
router.get('/bcrypt', function (req, res) {
res.render('util/bcrypt')
})
router.get('/bcrypt/:input', function (req, res) {
res.render('util/bcrypt', { input: req.params.input })
res.render('util/bcrypt', { meta: getMetaFromReq(req) })
})
router.get('/bcrypt/:input',
escapedParam('input'),
function (req, res) {
res.render('util/bcrypt', { input: req.params.input, meta: getMetaFromReq(req) })
})
export default router

387
src/schemas.js Normal file
View file

@ -0,0 +1,387 @@
/*
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
export const profileSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/profile.schema.json',
title: 'Profile',
description: 'Keyoxide profile with personas',
type: 'object',
properties: {
profileVersion: {
description: 'The version of the profile',
type: 'integer'
},
profileType: {
description: 'The type of the profile [openpgp, asp]',
type: 'string'
},
identifier: {
description: 'Identifier of the profile (email, fingerprint, URI)',
type: 'string'
},
personas: {
description: 'The personas inside the profile',
type: 'array',
items: {
$ref: 'https://spec.keyoxide.org/2/persona.schema.json'
},
minItems: 1,
uniqueItems: true
},
primaryPersonaIndex: {
description: 'The index of the primary persona',
type: 'integer'
},
publicKey: {
description: 'The cryptographic key associated with the profile',
type: 'object',
properties: {
keyType: {
description: 'The type of cryptographic key [eddsa, es256, openpgp, none]',
type: 'string'
},
encoding: {
description: 'The encoding of the cryptographic key [pem, jwk, armored_pgp, none]',
type: 'string'
},
encodedKey: {
description: 'The encoded cryptographic key (PEM, stringified JWK, ...)',
type: ['string', 'null']
},
fetch: {
description: 'Details on how to fetch the public key',
type: 'object',
properties: {
method: {
description: 'The method to fetch the key [aspe, hkp, wkd, http, none]',
type: 'string'
},
query: {
description: 'The query to fetch the key',
type: ['string', 'null']
},
resolvedUrl: {
description: 'The URL the method eventually resolved to',
type: ['string', 'null']
}
}
}
},
required: [
'keyType',
'fetch'
]
},
verifiers: {
description: 'A list of links to verifiers',
type: 'array',
items: {
type: 'object',
properties: {
name: {
description: 'Name of the verifier site',
type: 'string'
},
url: {
description: 'URL to the profile page on the verifier site',
type: 'string'
}
}
},
uniqueItems: true
}
},
required: [
'profileVersion',
'profileType',
'identifier',
'personas',
'primaryPersonaIndex',
'publicKey',
'verifiers'
],
additionalProperties: false
}
export const personaSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/persona.schema.json',
title: 'Profile',
description: 'Keyoxide persona with identity claims',
type: 'object',
properties: {
identifier: {
description: 'Identifier of the persona',
type: ['string', 'null']
},
name: {
description: 'Name of the persona',
type: 'string'
},
email: {
description: 'Email address of the persona',
type: ['string', 'null']
},
description: {
description: 'Description of the persona',
type: ['string', 'null']
},
avatarUrl: {
description: 'URL to an avatar image',
type: ['string', 'null']
},
themeColor: {
description: 'Profile page theme color',
type: ['string', 'null']
},
isRevoked: {
type: 'boolean'
},
claims: {
description: 'A list of identity claims',
type: 'array',
items: {
$ref: 'https://spec.keyoxide.org/2/claim.schema.json'
},
uniqueItems: true
}
},
required: [
'name',
'claims'
],
additionalProperties: false
}
export const claimSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/claim.schema.json',
title: 'Identity claim',
description: 'Verifiable online identity claim',
type: 'object',
properties: {
claimVersion: {
description: 'The version of the claim',
type: 'integer'
},
uri: {
description: 'The claim URI',
type: 'string'
},
proofs: {
description: 'The proofs that would verify the claim',
type: 'array',
items: {
type: 'string'
},
minItems: 1,
uniqueItems: true
},
matches: {
description: 'Service providers matched to the claim',
type: 'array',
items: {
$ref: 'https://spec.keyoxide.org/2/serviceprovider.schema.json'
},
uniqueItems: true
},
status: {
type: 'integer',
description: 'Claim status code'
},
display: {
type: 'object',
properties: {
profileName: {
type: 'string',
description: 'Account name to display in the user interface'
},
profileUrl: {
type: ['string', 'null'],
description: 'Profile URL to link to in the user interface'
},
proofUrl: {
type: ['string', 'null'],
description: 'Proof URL to link to in the user interface'
},
serviceProviderName: {
type: ['string', 'null'],
description: 'Name of the service provider to display in the user interface'
},
serviceProviderId: {
type: ['string', 'null'],
description: 'Id of the service provider'
}
}
}
},
required: [
'claimVersion',
'uri',
'proofs',
'status',
'display'
],
additionalProperties: false
}
export const serviceProviderSchema = {
$schema: 'https://json-schema.org/draft/2020-12/schema',
$id: 'https://spec.keyoxide.org/2/serviceprovider.schema.json',
title: 'Service provider',
description: 'A service provider that can be matched to identity claims',
type: 'object',
properties: {
about: {
description: 'Details about the service provider',
type: 'object',
properties: {
name: {
description: 'Full name of the service provider',
type: 'string'
},
id: {
description: 'Identifier of the service provider (no whitespace or symbols, lowercase)',
type: 'string'
},
homepage: {
description: 'URL to the homepage of the service provider',
type: ['string', 'null']
}
}
},
profile: {
description: 'What the profile would look like if the match is correct',
type: 'object',
properties: {
display: {
description: 'Profile name to be displayed',
type: 'string'
},
uri: {
description: 'URI or URL for public access to the profile',
type: 'string'
},
qr: {
description: 'URI or URL associated with the profile usually served as a QR code',
type: ['string', 'null']
}
}
},
claim: {
description: 'Details from the claim matching process',
type: 'object',
properties: {
uriRegularExpression: {
description: 'Regular expression used to parse the URI',
type: 'string'
},
uriIsAmbiguous: {
description: 'Whether this match automatically excludes other matches',
type: 'boolean'
}
}
},
proof: {
description: 'Information for the proof verification process',
type: 'object',
properties: {
request: {
description: 'Details to request the potential proof',
type: 'object',
properties: {
uri: {
description: 'Location of the proof',
type: ['string', 'null']
},
accessRestriction: {
description: 'Type of access restriction [none, nocors, granted, server]',
type: 'string'
},
fetcher: {
description: 'Name of the fetcher to use',
type: 'string'
},
data: {
description: 'Data needed by the fetcher or proxy to request the proof',
type: 'object',
additionalProperties: true
}
}
},
response: {
description: 'Details about the expected response',
type: 'object',
properties: {
format: {
description: 'Expected format of the proof [text, json]',
type: 'string'
}
}
},
target: {
description: 'Details about the target located in the response',
type: 'array',
items: {
type: 'object',
properties: {
format: {
description: 'How is the proof formatted [uri, fingerprint]',
type: 'string'
},
encoding: {
description: 'How is the proof encoded [plain, html, xml]',
type: 'string'
},
relation: {
description: 'How are the response and the target related [contains, equals]',
type: 'string'
},
path: {
description: 'Path to the target location if the response is JSON',
type: 'array',
items: {
type: 'string'
}
}
}
}
}
}
}
},
required: [
'about',
'profile',
'claim',
'proof'
],
additionalProperties: false
}

View file

@ -29,45 +29,52 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/
import logger from '../log.js'
import * as doipjs from 'doipjs'
import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './keys.js'
import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './openpgpProfiles.js'
import libravatar from 'libravatar'
const generateAspeProfile = async (id) => {
logger.debug('Generating an ASPE profile',
{ component: 'aspe_profile_generator', action: 'start', profile_id: id })
return doipjs.asp.fetchASPE(id)
.then(profile => {
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/${id}`)
}
profile = processAspProfile(profile)
return profile
})
.catch(err => {
logger.warn('Failed to generate ASPE profile',
{ component: 'aspe_profile_generator', action: 'failure', error: err.message, profile_id: id })
return {
errors: [err.message]
}
})
}
const generateWKDProfile = async (id) => {
logger.debug('Generating a WKD profile',
{ component: 'wkd_profile_generator', action: 'start', profile_id: id })
return fetchWKD(id)
.then(async key => {
let keyData = await doipjs.keys.process(key.publicKey)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
keyData.key.fetchMethod = 'wkd'
keyData.key.uri = key.fetchURL
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
keyoxideData.url = `https://${process.env.DOMAIN}/wkd/${id}`
.then(async profile => {
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
}
profile = processOpenPgpProfile(profile)
logger.debug('Generating a WKD profile',
{ component: 'wkd_profile_generator', action: 'done', profile_id: id })
return {
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
return profile
})
.catch(err => {
logger.warn('Failed to generate WKD profile',
{ component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id })
return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message]
}
})
@ -78,41 +85,29 @@ const generateHKPProfile = async (id, keyserverDomain) => {
{ component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
return fetchHKP(id, keyserverDomain)
.then(async key => {
let keyData = await doipjs.keys.process(key.publicKey)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
keyData.key.fetchMethod = 'hkp'
keyData.key.uri = key.fetchURL
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
.then(async profile => {
let keyoxideUrl
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${id}`
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}`
} else {
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
}
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', keyoxideUrl)
}
profile = processOpenPgpProfile(profile)
logger.debug('Generating a HKP profile',
{ component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
return {
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
return profile
})
.catch(err => {
logger.warn('Failed to generate HKP profile',
{ component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' })
return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message]
}
})
@ -121,25 +116,41 @@ const generateHKPProfile = async (id, keyserverDomain) => {
const generateAutoProfile = async (id) => {
let result
const aspeRe = /aspe:(.*):(.*)/
const openpgpRe = /openpgp4fpr:(.*)/
if (aspeRe.test(id)) {
result = await generateAspeProfile(id)
if (result && !('errors' in result)) {
return result
}
}
if (openpgpRe.test(id)) {
const match = id.match(openpgpRe)
result = await generateHKPProfile(match[1])
if (result && !('errors' in result)) {
return result
}
}
if (id.includes('@')) {
result = await generateWKDProfile(id)
if (result && result.errors.length === 0) {
if (result && !('errors' in result)) {
return result
}
}
result = await generateHKPProfile(id)
if (result && result.errors.length === 0) {
if (result && !('errors' in result)) {
return result
}
return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: ['No public keys could be found']
errors: ['No public profile/keys could be found']
}
}
@ -149,34 +160,19 @@ const generateSignatureProfile = async (signature) => {
return fetchSignature(signature)
.then(async key => {
let keyData = key.keyData
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
delete key.keyData
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
let profile = await doipjs.signatures.parse(key.publicKey)
profile = processOpenPgpProfile(profile)
logger.debug('Generating a signature profile',
{ component: 'signature_profile_generator', action: 'done' })
return {
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
return profile
})
.catch(err => {
logger.warn('Failed to generate a signature profile',
{ component: 'signature_profile_generator', action: 'failure', error: err.message })
return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message]
}
})
@ -187,82 +183,105 @@ const generateKeybaseProfile = async (username, fingerprint) => {
{ component: 'keybase_profile_generator', action: 'start', username, fingerprint })
return fetchKeybase(username, fingerprint)
.then(async key => {
let keyData = await doipjs.keys.process(key.publicKey)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
keyData.key.fetchMethod = 'hkp'
keyData.key.uri = key.fetchURL
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
keyoxideData.url = `https://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`
.then(async profile => {
if (process.env.DOMAIN) {
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
}
profile = processOpenPgpProfile(profile)
logger.debug('Generating a Keybase profile',
{ component: 'keybase_profile_generator', action: 'done', username, fingerprint })
return {
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
return profile
})
.catch(err => {
logger.warn('Failed to generate a Keybase profile',
{ component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint })
return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message]
}
})
}
const processKeyData = (keyData) => {
keyData.users.forEach(user => {
const processAspProfile = async (/** @type {import('doipjs').Profile */ profile) => {
profile.personas.forEach(persona => {
// Remove faulty claims
user.claims = user.claims.filter(claim => {
persona.claims = persona.claims.filter(claim => {
return claim instanceof doipjs.Claim
})
// Match claims
user.claims.forEach(claim => {
persona.claims.forEach(claim => {
claim.match()
})
// Sort claims
user.claims.sort((a, b) => {
persona.claims.sort((a, b) => {
if (a.matches.length === 0) return 1
if (b.matches.length === 0) return -1
if (a.matches[0].serviceprovider.name < b.matches[0].serviceprovider.name) {
if (a.matches[0].about.name < b.matches[0].about.name) {
return -1
}
if (a.matches[0].serviceprovider.name > b.matches[0].serviceprovider.name) {
if (a.matches[0].about.name > b.matches[0].about.name) {
return 1
}
return 0
})
})
return keyData
// Overwrite avatarUrl
// TODO: don't overwrite avatarUrl once it's fully supported
profile.personas[profile.primaryPersonaIndex].avatarUrl =
`https://${process.env.DICEBEAR_API_HOSTNAME || 'api.dicebear.com'}/7.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
return profile
}
const computeExtraData = async (key, keyData) => {
// Get the primary user
const primaryUser = await key.publicKey.getPrimaryUser()
const processOpenPgpProfile = async (/** @type {import('doipjs').Profile */ profile) => {
profile.personas.forEach(persona => {
// Remove faulty claims
persona.claims = persona.claims.filter(claim => {
return claim instanceof doipjs.Claim
})
// Query libravatar to get the avatar url
return {
avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true })
}
// Match claims
persona.claims.forEach(claim => {
claim.match()
})
// Sort claims
persona.claims.sort((a, b) => {
if (a.matches.length === 0) return 1
if (b.matches.length === 0) return -1
if (a.matches[0].about.name < b.matches[0].about.name) {
return -1
}
if (a.matches[0].about.name > b.matches[0].about.name) {
return 1
}
return 0
})
})
// Overwrite avatarUrl
// TODO: don't overwrite avatarUrl once it's fully supported
profile.personas[profile.primaryPersonaIndex].avatarUrl = await libravatar.get_avatar_url({ email: profile.personas[profile.primaryPersonaIndex].email, size: 128, default: 'mm', https: true })
return profile
}
const getScheme = () => {
return process.env.PROXY_SCHEME
? process.env.PROXY_SCHEME
: process.env.SCHEME
? process.env.SCHEME
: 'https'
}
export { generateAspeProfile }
export { generateWKDProfile }
export { generateHKPProfile }
export { generateAutoProfile }

View file

@ -1,243 +0,0 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import got from 'got'
import * as doipjs from 'doipjs'
import { readKey, readCleartextMessage, verify, PublicKey } from 'openpgp'
import { computeWKDLocalPart } from './utils.js'
import { createHash } from 'crypto'
import Keyv from 'keyv'
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
const fetchWKD = (id) => {
return new Promise((resolve, reject) => {
(async () => {
const output = {
publicKey: null,
fetchURL: null
}
if (!id.includes('@')) {
reject(new Error(`The WKD identifier "${id}" is invalid`))
}
const [, localPart, domain] = /([^@]*)@(.*)/.exec(id)
if (!localPart || !domain) {
reject(new Error(`The WKD identifier "${id}" is invalid`))
}
const localEncoded = await computeWKDLocalPart(localPart)
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
let plaintext
const hash = createHash('md5').update(id).digest('hex')
if (c && await c.get(hash)) {
plaintext = Uint8Array.from((await c.get(hash)).split(','))
}
if (!plaintext) {
try {
plaintext = await got(urlAdvanced).then((response) => {
if (response.statusCode === 200) {
output.fetchURL = urlAdvanced
return new Uint8Array(response.rawBody)
} else {
return null
}
})
} catch (e) {
try {
plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) {
output.fetchURL = urlDirect
return new Uint8Array(response.rawBody)
} else {
return null
}
})
} catch (error) {
reject(new Error('No public keys could be fetched using WKD'))
}
}
if (!plaintext) {
reject(new Error('No public keys could be fetched using WKD'))
}
if (c && plaintext instanceof Uint8Array) {
await c.set(hash, plaintext.toString(), 60 * 1000)
}
}
try {
output.publicKey = await readKey({
binaryKey: plaintext
})
} catch (error) {
reject(new Error('No public keys could be read from the data fetched using WKD'))
}
if (!output.publicKey) {
reject(new Error('No public keys could be read from the data fetched using WKD'))
}
resolve(output)
})()
})
}
const fetchHKP = (id, keyserverDomain) => {
return new Promise((resolve, reject) => {
(async () => {
const output = {
publicKey: null,
fetchURL: null
}
keyserverDomain = keyserverDomain || 'keys.openpgp.org'
let query = ''
if (id.includes('@')) {
query = id
} else {
let sanitizedId = id
const whitespaceRegex = /\s/g
if (whitespaceRegex.test(id)) {
sanitizedId = id.replaceAll(whitespaceRegex, '')
}
query = `0x${sanitizedId}`
}
output.fetchURL = `https://${keyserverDomain}/pks/lookup?op=get&options=mr&search=${query}`
const hash = createHash('md5').update(`${query}__${keyserverDomain}`).digest('hex')
if (c && await c.get(hash)) {
output.publicKey = await readKey({
armoredKey: await c.get(hash)
})
} else {
try {
output.publicKey = await doipjs.keys.fetchHKP(query, keyserverDomain)
} catch (error) {
reject(new Error('No public keys could be fetched using HKP'))
}
}
if (!output.publicKey) {
reject(new Error('No public keys could be fetched using HKP'))
}
if (c && output.publicKey instanceof PublicKey) {
await c.set(hash, output.publicKey.armor(), 60 * 1000)
}
resolve(output)
})()
})
}
const fetchSignature = (signature) => {
return new Promise((resolve, reject) => {
(async () => {
const output = {
publicKey: null,
fetchURL: null,
keyData: null
}
// Check validity of signature
let signatureData
try {
signatureData = await readCleartextMessage({
cleartextMessage: signature
})
} catch (error) {
reject(new Error(`Signature could not be properly read (${error.message})`))
}
// Process the signature
try {
output.keyData = await doipjs.signatures.process(signature)
output.publicKey = output.keyData.key.data
// TODO Find the URL to the key
output.fetchURL = null
} catch (error) {
reject(new Error(`Signature could not be properly read (${error.message})`))
}
// Check if a key was fetched
if (!output.publicKey) {
reject(new Error('No public keys could be fetched'))
}
// Check validity of signature
const verified = await verify({
message: signatureData,
verificationKeys: output.publicKey
})
if (!await verified.signatures[0].verified) {
reject(new Error('Signature was invalid'))
}
resolve(output)
})()
})
}
const fetchKeybase = (username, fingerprint) => {
return new Promise((resolve, reject) => {
(async () => {
const output = {
publicKey: null,
fetchURL: null
}
try {
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint)
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
} catch (error) {
reject(new Error('No public keys could be fetched from Keybase'))
}
if (!output.publicKey) {
reject(new Error('No public keys could be fetched from Keybase'))
}
resolve(output)
})()
})
}
export { fetchWKD }
export { fetchHKP }
export { fetchSignature }
export { fetchKeybase }

View file

@ -0,0 +1,279 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import logger from '../log.js'
import got from 'got'
import * as doipjs from 'doipjs'
import { readKey } from 'openpgp'
import { computeWKDLocalPart } from './utils.js'
import { createHash } from 'crypto'
import Keyv from 'keyv'
let c = null
if (process.env.ENABLE_EXPERIMENTAL_CACHE) {
c = new Keyv()
logger.debug('OpenPGP cache started',
{ component: 'openpgp_cache', action: 'start' })
}
const fetchWKD = (id) => {
return new Promise((resolve, reject) => {
(async () => {
logger.debug('Fetching an OpenPGP profile via WKD',
{ component: 'wkd_profile_fetcher', action: 'start', profile_id: id })
let publicKey = null
let profile = null
let fetchURL = null
if (!id.includes('@')) {
return reject(new Error(`The WKD identifier "${id}" is invalid`))
}
const [, localPart, domain] = /([^@]*)@(.*)/.exec(id)
if (!(localPart && domain)) {
return reject(new Error(`The WKD identifier "${id}" is invalid`))
}
const localEncoded = await computeWKDLocalPart(localPart)
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
let plaintext
const hash = createHash('md5').update(id).digest('hex')
if (c && await c.get(hash)) {
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
logger.debug('WKD profile retrieved from OpenPGP cache',
{ component: 'openpgp_cache', action: 'retrieve_wkd' })
return resolve(profile)
}
if (!profile) {
try {
plaintext = await got(urlAdvanced).then((response) => {
if (response.statusCode === 200) {
fetchURL = urlAdvanced
return new Uint8Array(response.rawBody)
} else {
return null
}
})
} catch (errorAdvanced) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (advanced URL)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorAdvanced.message })
try {
plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) {
fetchURL = urlDirect
return new Uint8Array(response.rawBody)
} else {
return null
}
})
} catch (errorDirect) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (direct URL)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: errorDirect.message })
return reject(new Error('No public keys could be fetched using WKD'))
}
}
if (!plaintext) {
return reject(new Error('No public keys could be fetched using WKD'))
}
try {
publicKey = await readKey({
binaryKey: plaintext
})
} catch (error) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (reading key)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
return reject(new Error('No public keys could be read from the data fetched using WKD'))
}
if (!publicKey) {
return reject(new Error('No public keys could be read from the data fetched using WKD'))
}
try {
profile = await doipjs.openpgp.parsePublicKey(publicKey)
} catch (error) {
logger.debug('Failed to fetch an OpenPGP profile via WKD (parsing key)',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, error: error.message })
return reject(new Error('No public keys could be fetched using WKD'))
}
profile.publicKey.fetch.method = 'wkd'
profile.publicKey.fetch.query = id
profile.publicKey.fetch.resolvedUrl = fetchURL
}
if (c && plaintext instanceof Uint8Array) {
await c.set(hash, JSON.stringify(profile), 60 * 1000)
logger.debug('WKD profile stored in OpenPGP cache',
{ component: 'openpgp_cache', action: 'store_wkd' })
}
logger.debug('Fetched an OpenPGP profile via WKD',
{ component: 'wkd_profile_fetcher', action: 'done', profile_id: id })
resolve(profile)
})()
})
}
const fetchHKP = (id, keyserverDomain) => {
return new Promise((resolve, reject) => {
(async () => {
logger.debug('Fetching an OpenPGP profile via HKP',
{ component: 'hkp_profile_fetcher', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
let profile = null
let fetchURL = null
const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org'
let query = ''
if (id.includes('@')) {
query = id
} else {
let sanitizedId = id
const whitespaceRegex = /\s/g
if (whitespaceRegex.test(id)) {
sanitizedId = id.replaceAll(whitespaceRegex, '')
}
query = `0x${sanitizedId}`
}
fetchURL = `https://${keyserverDomainNormalized}/pks/lookup?op=get&options=mr&search=${query}`
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
if (c && await c.get(hash)) {
profile = doipjs.Profile.fromJSON(JSON.parse(await c.get(hash)))
logger.debug('HKP profile retrieved from OpenPGP cache',
{ component: 'openpgp_cache', action: 'store_hkp' })
return resolve(profile)
}
if (!profile) {
try {
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
} catch (error) {
logger.debug('Failed to fetch an OpenPGP profile via HKP',
{ component: 'hkp_profile_fetcher', action: 'failure', profile_id: id, keyserver_domain: keyserverDomain || '', error: error.message })
profile = null
}
}
if (!profile) {
return reject(new Error('No public keys could be fetched using HKP'))
}
profile.publicKey.fetch.method = 'hkp'
profile.publicKey.fetch.query = id
profile.publicKey.fetch.resolvedUrl = fetchURL
if (c && profile instanceof doipjs.Profile) {
await c.set(hash, JSON.stringify(profile), 60 * 1000)
logger.debug('HKP profile stored in OpenPGP cache',
{ component: 'openpgp_cache', action: 'store_hkp' })
}
logger.debug('Fetched an OpenPGP profile via HKP',
{ component: 'hkp_profile_fetcher', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
resolve(profile)
})()
})
}
const fetchSignature = (signature) => {
return new Promise((resolve, reject) => {
(async () => {
let profile = null
// Process the signature
try {
profile = await doipjs.signatures.parse(signature)
// TODO Find the URL to the key
} catch (error) {
return reject(new Error(`Signature could not be properly read (${error.message})`))
}
// Check if a key was fetched
if (!profile) {
return reject(new Error('No profile could be fetched'))
}
resolve(profile)
})()
})
}
const fetchKeybase = (username, fingerprint) => {
return new Promise((resolve, reject) => {
(async () => {
let profile = null
let fetchURL = null
try {
profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
} catch (error) {
return reject(new Error('No public keys could be fetched from Keybase'))
}
if (!profile) {
return reject(new Error('No public keys could be fetched from Keybase'))
}
profile.publicKey.fetch.method = 'http'
profile.publicKey.fetch.resolvedUrl = fetchURL
resolve(profile)
})()
})
}
export { fetchWKD }
export { fetchHKP }
export { fetchSignature }
export { fetchKeybase }

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/>.
*/
import { webcrypto as crypto } from 'crypto'
import { Profile } from 'doipjs'
import Color from 'colorjs.io'
import { param } from 'express-validator'
export async function computeWKDLocalPart (localPart) {
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
@ -39,7 +42,7 @@ export function generatePageTitle (type, data) {
switch (type) {
case 'profile':
try {
return `${data.keyData.users[data.keyData.primaryUserIndex].userData.name} - Keyoxide`
return `${data.personas[data.primaryPersonaIndex].name} - Keyoxide`
} catch (error) {
return 'Profile - Keyoxide'
}
@ -78,3 +81,98 @@ export function encodeZBase32 (data) {
}
return result
}
export function getMetaFromReq (req) {
const versionDetails = (req.app.get('git_hash'))
? `+${req.app.get('git_hash').substring(0, 10)}`
: ''
const semver = `${req.app.get('keyoxide_name')}/${req.app.get('keyoxide_version')}${versionDetails}`
const sourceUrl = req.app.get('git_hash')
? `https://codeberg.org/keyoxide/keyoxide-web/src/commit/${req.app.get('git_hash')}`
: 'https://codeberg.org/keyoxide/keyoxide-web'
return {
env: req.app.get('env'),
keyoxide: {
name: req.app.get('keyoxide_name'),
version: req.app.get('keyoxide_version'),
branch: req.app.get('git_branch'),
hash: req.app.get('git_hash'),
semver,
sourceUrl
}
}
}
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

@ -35,8 +35,8 @@ import * as ui from './ui.js'
import * as utils from './utils.js'
// Import CSS files
import './styles.css'
import './kx-styles.css'
import './styles.scss'
import './kx-styles.scss'
// Add functions to window
window.showQR = utils.showQR

View file

@ -45,11 +45,12 @@ export class Claim extends HTMLElement {
}
async verify() {
const claim = new doipjs.Claim(JSON.parse(this.getAttribute('data-claim')));
const claim = doipjs.Claim.fromJSON(JSON.parse(this.getAttribute('data-claim')));
await claim.verify({
proxy: {
policy: 'adaptive',
hostname: 'PLACEHOLDER__PROXY_HOSTNAME'
hostname: 'PLACEHOLDER__PROXY_HOSTNAME',
scheme: 'PLACEHOLDER__PROXY_SCHEME'
}
});
this.setAttribute('data-claim', JSON.stringify(claim));
@ -57,36 +58,31 @@ export class Claim extends HTMLElement {
updateContent(value) {
const root = this;
const claim = new doipjs.Claim(JSON.parse(value));
const claimJson = JSON.parse(value);
const claim = doipjs.Claim.fromJSON(claimJson);
switch (claim.matches[0].serviceprovider.name) {
case 'dns':
case 'xmpp':
case 'irc':
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name.toUpperCase();
break;
default:
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name;
break;
}
root.querySelector('.info .title').innerText = claim.matches[0].profile.display;
root.querySelector('.info .title').innerText = claimJson.display.profileName;
root.querySelector('.info .subtitle').innerText = claimJson.display.serviceProviderName ??
(claim.status < 300 ? '???' : '---');
root.querySelector('.info img').setAttribute('src',
`https://design.keyoxide.org/brands/service-providers/${claimJson.display.serviceProviderId
? claimJson.display.serviceProviderId : '_'}/icon.svg`);
try {
if (claim.status === 'verified') {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.verification.result ? 'success' : 'failed');
if (claim.status >= 200) {
root.setAttribute('data-status', claim.status < 300 ? 'success' : 'failed');
} else {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
root.setAttribute('data-status', 'running');
}
} catch (error) {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'failed');
root.setAttribute('data-status', 'failed');
}
const elContent = root.querySelector('.content');
elContent.innerHTML = ``;
// Handle failed ambiguous claim
if (claim.status === 'verified' && !claim.verification.result && claim.isAmbiguous()) {
if (claim.status >= 300 && claim.isAmbiguous()) {
root.querySelector('.info .subtitle').innerText = '---';
const subsection_alert = elContent.appendChild(document.createElement('div'));
@ -112,15 +108,15 @@ export class Claim extends HTMLElement {
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].profile.uri) {
profile_link.innerHTML = `Profile link: <a rel="me" href="${claim.matches[0].profile.uri}" aria-label="link to profile">${claim.matches[0].profile.uri}</a>`;
if (claimJson.display.profileUrl) {
profile_link.innerHTML = `Profile link: <a rel="me" href="${claimJson.display.profileUrl}" aria-label="link to profile">${claimJson.display.profileUrl}</a>`;
} else {
profile_link.innerHTML = `Profile link: not accessible from browser`;
}
const proof_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].proof.uri) {
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.uri}" aria-label="link to profile">${claim.matches[0].proof.uri}</a>`;
if (claimJson.display.proofUrl) {
proof_link.innerHTML = `Proof link: <a href="${claimJson.display.proofUrl}" aria-label="link to profile">${claimJson.display.proofUrl}</a>`;
} else {
proof_link.innerHTML = `Proof link: not accessible from browser`;
}
@ -155,7 +151,7 @@ export class Claim extends HTMLElement {
const subsection_status_text = subsection_status.appendChild(document.createElement('div'));
const verification = subsection_status_text.appendChild(document.createElement('p'));
if (claim.status === 'verified') {
if (claim.status >= 200) {
verification.innerHTML = `Claim verification has completed.`;
subsection_status_icon.setAttribute('src', '/static/img/check-decagram.svg');
subsection_status_icon.setAttribute('alt', '');
@ -177,10 +173,10 @@ export class Claim extends HTMLElement {
const subsection_result_text = subsection_result.appendChild(document.createElement('div'));
const result = subsection_result_text.appendChild(document.createElement('p'));
result.innerHTML = `The claim <strong>${claim.verification.result ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
result.innerHTML = `The claim <strong>${claim.status >= 200 && claim.status < 300 ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
// Additional info
if (claim.verification.proof.viaProxy) {
if (claim.status === 201) {
elContent.appendChild(document.createElement('hr'));
const subsection_info = elContent.appendChild(document.createElement('div'));
@ -192,7 +188,7 @@ export class Claim extends HTMLElement {
const subsection_info_text = subsection_info.appendChild(document.createElement('div'));
const result_proxyUsed = subsection_info_text.appendChild(document.createElement('p'));
result_proxyUsed.innerHTML = `A proxy was used to fetch the proof: <a href="https://PLACEHOLDER__PROXY_HOSTNAME" aria-label="Link to proxy server">PLACEHOLDER__PROXY_HOSTNAME</a>`;
result_proxyUsed.innerHTML = `A proxy was used to fetch the proof: <a href="PLACEHOLDER__PROXY_SCHEME://PLACEHOLDER__PROXY_HOSTNAME" aria-label="Link to proxy server">PLACEHOLDER__PROXY_HOSTNAME</a>`;
}
// TODO Display errors
@ -217,4 +213,4 @@ export class Claim extends HTMLElement {
// });
// }
}
}
}

View file

@ -46,7 +46,7 @@ export class Key extends HTMLElement {
const root = this;
const data = JSON.parse(value);
root.querySelector('.info .subtitle').innerText = data.key.fetchMethod;
root.querySelector('.info .subtitle').innerText = `${data.keyType} / ${data.fetch.method}`;
root.querySelector('.info .title').innerText = data.fingerprint;
const elContent = root.querySelector('.content');
@ -62,22 +62,24 @@ export class Key extends HTMLElement {
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
profile_link.innerHTML = `Key link: <a class="u-key" rel="pgpkey" href="${data.key.uri}" aria-label="Link to cryptographic key">${data.key.uri}</a>`;
profile_link.innerHTML = `Key link: <a class="u-key" rel="pgpkey" href="${data.fetch.resolvedUrl}" aria-label="Link to cryptographic key">${data.fetch.resolvedUrl}</a>`;
elContent.appendChild(document.createElement('hr'));
// QR Code
const subsection_qr = elContent.appendChild(document.createElement('div'));
subsection_qr.setAttribute('class', 'subsection');
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.svg');
subsection_qr_icon.setAttribute('alt', '');
subsection_qr_icon.setAttribute('aria-hidden', 'true');
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
if (data.keyType === 'openpgp') {
elContent.appendChild(document.createElement('hr'));
// QR Code
const subsection_qr = elContent.appendChild(document.createElement('div'));
subsection_qr.setAttribute('class', 'subsection');
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.svg');
subsection_qr_icon.setAttribute('alt', '');
subsection_qr_icon.setAttribute('aria-hidden', 'true');
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
}
}
}

View file

@ -1,264 +0,0 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
kx-claim {
display: block;
margin: 12px 0;
}
/* KX-ITEM */
.kx-item details {
width: 100%;
border-radius: 8px;
}
.kx-item details p {
margin: 0;
word-break: break-word;
font-size: 1rem;
}
.kx-item details a {
color: var(--link-color);
}
.kx-item details hr {
border: none;
border-top: 2px solid var(--claim-background-color);
}
.kx-item details .content {
padding: 12px;
border: solid 3px var(--claim-background-color);
border-top: 0px;
border-radius: 0px 0px 8px 8px;
}
.kx-item details summary {
display: flex;
align-items: center;
padding: 8px 12px;
background-color: var(--claim-background-color);
border: solid 3px var(--claim-background-color);
border-radius: 8px;
list-style: none;
cursor: pointer;
}
.kx-item details summary::-webkit-details-marker {
display: none;
}
.kx-item details summary:hover, summary:focus {
border-color: var(--claim-border-accent-color);
}
details[open] summary {
border-radius: 8px 8px 0px 0px;
}
.kx-item details summary .info {
flex: 1;
}
.kx-item details summary .info .title {
font-size: 1.1em;
}
.kx-item details summary .claim__description p {
font-size: 1.4rem;
line-height: 2rem;
}
.kx-item details summary .claim__links p, p.subtle-links {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 1rem;
color: var(--link-subtle-color);
}
.kx-item details summary .claim__links a, summary .claim__links span, p.subtle-links a {
font-size: 1rem;
margin: 0 10px 0 0;
color: var(--link-subtle-color);
}
.kx-item details summary .subtitle {
color: var(--claim-title-text-color);
}
.kx-item details summary .verificationStatus {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: 100%;
color: #fff;
font-size: 2rem;
user-select: none;
}
.kx-item details summary .verificationStatus::after {
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
}
.kx-item details summary .verificationStatus .inProgress {
opacity: 0;
transition: opacity 0.4s ease;
pointer-events: none;
}
.kx-item details summary .verificationStatus[data-value="success"] {
content: "v";
background-color: var(--success-color);
}
.kx-item details summary .verificationStatus[data-value="success"]::after {
content: "✔";
}
.kx-item details summary .verificationStatus[data-value="failed"] {
background-color: var(--failure-color);
}
.kx-item details summary .verificationStatus[data-value="failed"]::after {
content: "✕";
}
.kx-item details summary .verificationStatus[data-value="running"] .inProgress {
opacity: 1;
}
.kx-item details .subsection {
display: flex;
align-items: center;
gap: 16px;
}
.kx-item details .subsection > img {
width: 24px;
height: 24px;
opacity: 0.4;
}
@media (prefers-color-scheme: dark) {
.kx-item details .subsection > img {
filter: invert(1);
}
}
.kx-item details .inProgress {
font-size: 10px;
margin: 50px auto;
text-indent: -9999em;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--loader-color);
background: -moz-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
background: -webkit-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
background: -o-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
background: -ms-linear-gradient(left, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
background: linear-gradient(to right, var(--loader-color) 10%, rgba(255, 255, 255, 0) 42%);
position: relative;
-webkit-animation: load3 1.4s infinite linear;
animation: load3 1.4s infinite linear;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
}
.kx-item details .inProgress:before {
width: 50%;
height: 50%;
background: var(--loader-color);
border-radius: 100% 0 0 0;
position: absolute;
top: 0;
left: 0;
content: '';
}
.kx-item details .inProgress:after {
background: var(--claim-background-color);
width: 65%;
height: 65%;
border-radius: 50%;
content: '';
margin: auto;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.kx-item details button {
padding: 0.4rem 0.8rem;
margin-right: 8px;
text-decoration: none;
text-transform: uppercase;
color: var(--button-text-color);
background-color: var(--button-background-color);
border: solid 2px var(--button-border-color);
border-radius: 4px;
cursor: pointer;
}
.kx-item details button:hover {
background-color: var(--button-hover-background-color);
border-color: var(--button-hover-border-color);
color: var(--button-hover-text-color);
}
@media screen and (max-width: 640px) {
.kx-item details summary .claim__description p {
font-size: 1.2rem;
}
.kx-item details summary .claim__links a, p.subtle-links a {
font-size: 0.9rem;
}
}
@media screen and (max-width: 480px) {
summary .claim__description p {
font-size: 1rem;
}
.kx-item details summary .verificationStatus {
width: 36px;
height: 36px;
font-size: 1.6rem;
}
.kx-item details .inProgress {
width: 36px;
height: 36px;
}
}
@-webkit-keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes load3 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

284
static-src/kx-styles.scss Normal file
View file

@ -0,0 +1,284 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
kx-claim {
padding: 2px 0;
}
.kx-item {
display: block;
font-size: 0.9rem;
margin-left: -4px;
details {
position: relative;
width: 100%;
border-radius: 4px;
z-index: 0;
&[open] {
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
}
p {
margin: 0;
word-break: break-word;
font-size: 1em;
}
a {
color: var(--link-color);
}
hr {
margin: 8px 0;
border: none;
border-top: 2px solid var(--header-background-color);
}
.content {
font-size: 0.9em;
padding: 12px;
background-color: var(--background-color);
border-top: 0px;
border-radius: 0px 0px 4px 4px;
}
&[open] summary {
border-radius: 4px 4px 0px 0px;
background-color: var(--header-background-color) !important;
}
summary {
display: flex;
align-items: center;
padding: 2px 4px;
border-radius: 4px;
list-style: none;
cursor: pointer;
&::-webkit-details-marker {
display: none;
}
&:hover,
&:focus {
background-color: var(--header-background-color);
}
.info {
display: flex;
align-items: baseline;
gap: 8px;
flex: 1;
}
.info .title {
color: var(--text-color);
}
.info img {
width: 16px;
height: 16px;
opacity: 0.5;
transform: translateY(3px);
@media (prefers-color-scheme: dark) {
filter: invert(1);
}
}
.claim__links {
p {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 1em;
color: var(--link-color-subtle);
}
.a,
span {
font-size: 1em;
margin: 0 10px 0 0;
color: var(--link-color-subtle);
}
}
.subtitle-wrapper {
color: var(--text-color-subtle);
}
}
.subsection {
display: flex;
align-items: center;
gap: 8px;
}
.subsection>img {
width: 20px;
height: 20px;
opacity: 0.4;
}
@media (prefers-color-scheme: dark) {
.subsection>img {
filter: invert(1);
}
}
}
.verificationStatus {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 100%;
color: #fff;
font-size: 1.4em;
user-select: none;
&::after {
position: absolute;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
align-items: center;
justify-content: center;
}
.inProgress,
.success,
.failure {
position: absolute;
top: 0;
left: 0;
opacity: 0;
pointer-events: none;
}
.inProgress {
color: var(--loader-color);
svg {
animation: 1s linear 0s infinite rot360;
}
}
.success {
color: var(--success-color);
}
.failure {
color: var(--failure-color);
}
}
button {
padding: 0.4rem 0.8em;
margin-right: 8px;
text-decoration: none;
text-transform: uppercase;
color: var(--button-text-color);
background-color: var(--button-background-color);
border: solid 1px var(--button-border-color);
border-radius: 4px;
cursor: pointer;
}
button:hover {
background-color: var(--button-background-color-hover);
border-color: var(--button-border-color-hover);
color: var(--button-text-color-hover);
}
&[data-status="running"] {
.title {
color: var(--text-color-subtle) !important;
}
.inProgress {
opacity: 1 !important;
}
}
&[data-status="success"] {
.title {
color: var(--primary-color) !important;
font-weight: bold;
}
.success {
opacity: 1 !important;
}
}
&[data-status="failed"] {
.title {
color: var(--text-color-subtle) !important;
}
.failure {
opacity: 1 !important;
}
}
&[data-status="success"] .verificationStatus>div,
&[data-status="failed"] .verificationStatus>div {
transition: opacity 0.4s ease !important;
}
@-webkit-keyframes rot360 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes rot360 {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
}

View file

@ -1,645 +0,0 @@
/*
Copyright (C) 2021 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
@import '../node_modules/fork-awesome/css/fork-awesome.css';
@import '../node_modules/dialog-polyfill/dist/dialog-polyfill.css';
:root {
--white: hsl(0, 0%, 100%);
--black: hsl(0, 0%, 0%);
--grey-100: hsl(0, 0%, 90%);
--grey-200: hsl(0, 0%, 80%);
--grey-300: hsl(0, 0%, 70%);
--grey-400: hsl(0, 0%, 60%);
--grey-500: hsl(0, 0%, 50%);
--grey-600: hsl(0, 0%, 40%);
--grey-700: hsl(0, 0%, 30%);
--grey-900: hsl(0, 0%, 10%);
--green-300: hsl(110, 45%, 70%);
--green-400: hsl(110, 45%, 60%);
--green-600: hsl(110, 45%, 40%);
--red-400: hsl(10, 60%, 60%);
--blue-500: rgb(67, 176, 234);
--blue-700: hsl(201, 90%, 30%);
--purple-50: rgb(249, 248, 251);
--purple-100: rgb(238, 236, 248);
--purple-200: hsl(250, 48%, 90%);
--purple-300: hsl(250, 48%, 85%);
--purple-400: hsl(250, 48%, 70%);
--purple-500: hsl(250, 48%, 65%);
--purple-600: hsl(250, 48%, 60%);
--purple-700: hsl(250, 48%, 55%);
--purple-900: hsl(250, 38%, 45%);
--yellow-100: hsl(56, 100%, 95%);
--yellow-200: hsl(56, 100%, 90%);
--yellow-500: hsl(56, 100%, 65%);
--loader-color: var(--purple-400);
--success-color: var(--green-600);
--failure-color: var(--red-400);
--text-color: var(--grey-900);
--h1-color: var(--purple-700);
--h2-color: var(--purple-700);
--h2-small-color: var(--purple-600);
--h3-color: var(--grey-700);
--h3-small-color: var(--purple-400);
--h4-color: var(--grey-600);
--h4-small-color: var(--purple-400);
--link-color: var(--blue-700);
--link-subtle-color: var(--grey-700);
--nav-link-color: var(--purple-700);
--button-text-color: var(--text-color);
--button-border-color: var(--purple-500);
--button-background-color: var(--white);
--button-hover-text-color: var(--white);
--button-hover-border-color: var(--purple-500);
--button-hover-background-color: var(--purple-500);
--body-background-color: var(--white);
--footer-background-color: var(--purple-900);
--footer-text-color: var(--purple-200);
--card-background-color: var(--purple-50);
--card-border-color: var(--purple-200);
--claim-background-color: var(--purple-100);
--claim-border-accent-color: var(--purple-400);
--claim-title-text-color: var(--purple-700);
--input-focus-background-color: azure;
--focus-outline-color: lightskyblue;
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: var(--grey-100);
--h1-color: var(--purple-700);
--h2-color: var(--purple-300);
--h2-small-color: var(--purple-600);
--h3-color: var(--grey-300);
--h3-small-color: var(--purple-400);
--h4-color: var(--grey-300);
--h4-small-color: var(--purple-6400);
--link-color: var(--blue-500);
--link-subtle-color: var(--grey-700);
--nav-link-color: var(--purple-100);
--loader-color: var(--purple-600);
--success-color: var(--green-600);
--failure-color: var(--red-400);
--button-text-color: var(--white);
--button-border-color: var(--purple-700);
--button-background-color: var(--purple-900);
--button-hover-text-color: var(--white);
--button-hover-border-color: var(--purple-700);
--button-hover-background-color: var(--purple-700);
--body-background-color: #121212;
--footer-background-color: #191720;
--footer-text-color: var(--purple-200);
--card-background-color: #191720;
--card-border-color: #26203a;
--claim-background-color: #26203a;
--claim-border-accent-color: var(--purple-400);
--claim-title-text-color: var(--purple-300);
--input-focus-background-color: azure;
--focus-outline-color: lightskyblue;
}
}
* {
box-sizing: border-box;
}
:focus {
outline: none;
box-shadow: 0 0 0 3px var(--focus-outline-color);
}
input:focus, textarea:focus {
background: var(--input-focus-background-color);
}
input[type="radio"]:focus + label {
box-shadow: 0 0 0 3px var(--focus-outline-color);
background: var(--input-focus-background-color) !important;
color: var(--text-color) !important;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0;
padding: 1.6rem 0 0;
line-height: 1.4rem;
font-family: sans-serif;
color: var(--text-color);
background-color: var(--body-background-color);
}
/* HELPERS */
.spacer {
flex: 1;
}
.no-margin {
margin: 0 !important;
}
.full-width {
display: block;
width: 100% !important;
}
.half-width {
display: block;
width: 50% !important;
}
.select-all {
user-select: all;
}
/* LAYOUT */
header {
margin: 0 1.6rem 1.6rem;
}
header nav {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 8px;
}
header nav a.logo {
width: 64px;
height: 64px;
font-size: 1.6rem;
text-transform: uppercase;
text-decoration: none;
color: var(--nav-link-color);
}
header nav a.logo img {
width: 100%;
}
nav a.text {
/* font-size: 0.9em; */
margin: 0;
padding: 0.5em 1em;
text-transform: uppercase;
text-decoration: none;
color: var(--nav-link-color);
border-radius: 4px;
}
nav a.text:hover, nav a.text:active {
color: #fff;
background-color: var(--purple-500);
}
main {
flex: 1;
margin: 0 1.6rem;
}
footer {
margin: 4.8rem 0 0;
padding: 0 1.6rem 1.6rem;
background-color: var(--footer-background-color);
color: var(--footer-background-color);
}
.container {
width: 100%;
max-width: 720px;
margin: 0 auto;
}
section.profile p, .demo p {
font-size: 1.2rem;
}
.demo {
margin: 4.8rem auto;
}
.card {
margin: 0 0 1.6rem;
padding: 0 1.2rem;
background-color: #fff;
background-color: var(--card-background-color);
border: 2px solid var(--card-border-color);
border-radius: 4px;
}
.card.card--transparent {
padding-left: 0;
padding-right: 0;
background-color: transparent;
border: 0;
}
.card--profileHeader {
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
gap: 24px;
}
.card--profileHeader p, .card--profileHeader small {
margin: 0;
}
.card--small-profile {
display: flex;
flex-direction: column;
text-align: center;
}
.card--small-profile-dummy {
opacity: 0.5;
border: 0;
}
.card--small-profile .name {
font-size: 1.4em;
}
.card--small-profile p {
margin-top: 0;
}
.card--small-profile p span.fingerprint {
display: inline-block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.8rem;
}
#profileName {
font-size: 1.6rem;
color: var(--text-color);
}
#profileURLFingerprint {
font-size: 1rem;
margin: 0 0 1.2rem;
}
.hcards {
display: grid;
grid-gap: 1.2rem;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
margin-bottom: 1.6rem;
}
.hcards .card {
margin: 0;
}
.hcards.hcards--max-2 {
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
}
.hcards--col-1-2, .hcards--col-2-1 {
grid-template-columns: repeat(auto-fit, minmax(256px, 1fr));
}
.hcards--col-1-2 .card, .hcards--col-2-1 .card {
grid-column: 1 / 2;
}
@media screen and (min-width: 1024px) {
.hcards--max-3 {
grid-template-columns: 1fr 1fr 1fr;
}
.hcards--max-2 {
grid-template-columns: 1fr 1fr;
}
.hcards--col-1-2, .hcards--col-2-1 {
grid-template-columns: repeat(3, 1fr);
}
}
@media screen and (min-width: 720px) {
.hcards--col-2-1 .card:nth-of-type(1) {
grid-column: 1 / -2;
}
.hcards--col-2-1 .card:nth-of-type(2) {
grid-column: -2 / -1;
}
.hcards--col-1-2 .card:nth-of-type(1) {
grid-column: 1 / 2;
}
.hcards--col-1-2 .card:nth-of-type(2) {
grid-column: 2 / -1;
}
}
.warning {
padding: calc(0.8rem - 2px) 0.8rem;
background-color: var(--yellow-200);
border: solid 2px var(--yellow-500);
}
.warning p:first-of-type {
margin-top: 0;
}
.warning p:last-of-type {
margin-bottom: 0;
}
#profileAvatar {
display: inline-block;
min-width: 96px;
max-width: 128px;
line-height: 0;
text-align: center;
border-radius: 50%;
}
/* TYPOGRAPHY */
h1 {
font-size: 1.6em;
margin: 3.2rem 0 1.6rem;
font-weight: normal;
color: var(--h1-color);
cursor: default;
}
h2 {
font-size: 1.4em;
margin: 3.2rem 0 1.6rem;
font-weight: normal;
color: var(--h2-color);
cursor: default;
}
h2 small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--h2-small-color);
color: #fff;
border-radius: 4px;
}
h3 {
margin: 1.6rem 0;
font-size: 1.3em;
line-height: 1.6rem;
color: var(--h3-color);
font-weight: normal;
/* text-align: center; */
cursor: default;
}
h3 small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--h3-small-color);
color: #fff;
border-radius: 4px;
}
h4 {
margin: 1.6rem 0;
font-size: 1em;
line-height: 1.6rem;
color: var(--h4-color);
/* color: var(--purple-700); */
font-weight: bold;
cursor: default;
}
h4 small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--h4-small-color);
color: #fff;
border-radius: 4px;
}
p {
margin: 1.6rem 0;
}
p.warning {
padding: 8px;
background-color: #fffadc;
border: solid 1px #ffeea8;
}
a {
color: var(--link-color);
}
ul {
padding-left: 1em;
list-style: '- ';
}
main h1:first-of-type {
margin-top: 1.6rem;
}
footer h1 {
margin-bottom: 0.8rem;
color: var(--purple-200);
font-size: 1.2rem;
font-weight: bold;
}
footer a {
display: inline-block;
color: var(--purple-100);
height: 32px;
}
code {
padding: 2px 4px;
background-color: var(--purple-100);
border: 1px solid var(--purple-500);
}
pre {
padding: 8px 12px;
background-color: var(--purple-100);
border: 1px solid var(--purple-500);
overflow-x: auto;
line-height: 1.2rem;
font-size: 1rem;
}
pre code {
padding: 0;
background-color: 0px;
border: 0px;
}
#search {
margin-top: 96px;
margin-bottom: 128px;
}
#qr {
display: block;
width: 100% !important;
max-width: 256px !important;
height: auto !important;
margin: 0 auto 16px;
}
/* FORM ELEMENTS */
.form-wrapper {
align-items: center;
padding-top: 1.4rem;
padding-bottom: 1.6rem;
margin-bottom: 48px;
}
.form-wrapper *:last-child {
margin-bottom: 0;
}
.form-wrapper form {
display: flex;
flex-direction: column;
margin: 0;
}
.form-wrapper h2 {
margin-top: 0;
}
form input[type="text"], form input[type="search"] {
margin: 8px 0;
padding: 4px;
border: 1px solid #999;
border-radius: 3px;
font-size: 0.9rem;
}
form textarea {
width: 100%;
height: 128px;
margin: 8px 0;
resize: vertical;
font-size: 0.9rem;
border: 1px solid #999;
}
.button-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0;
}
.radio-wrapper {
display: flex;
flex-wrap: wrap;
margin: 8px 0;
}
.radio-wrapper input[type="radio"] {
position: absolute;
opacity: 0;
z-index: -1;
}
.radio-wrapper input[type="radio"] + label {
margin: 0;
padding: 2px 8px;
background-color: #fff;
border: solid var(--purple-400);
border-width: 2px 1px;
cursor: pointer;
}
.radio-wrapper input[type="radio"]:first-of-type + label {
border-radius: 4px 0 0 4px;
border-left-width: 2px;
}
.radio-wrapper input[type="radio"]:last-of-type + label {
border-radius: 0 4px 4px 0;
border-right-width: 2px;
}
.radio-wrapper input[type="radio"]:focus + label {
z-index: 1;
}
.radio-wrapper input[type="radio"] + label:hover {
background-color: var(--purple-100);
border-color: var(--purple-500);
}
.radio-wrapper input[type="radio"]:checked + label {
color: #fff;
background-color: var(--purple-600);
border-color: var(--purple-600);
}
input[type="button"], input[type="submit"], button, a.button {
display: inline-block;
min-height: 36px;
margin: 8px 0;
padding: 4px 8px;
font-family: sans-serif;
font-size: 0.9rem;
text-decoration: none;
text-transform: uppercase;
color: var(--button-text-color);
background-color: var(--button-background-color);
border: solid 2px var(--button-border-color);
border-radius: 4px;
cursor: pointer;
}
input[type="button"]:focus, input[type="submit"]:focus, button:focus, a.button:focus {
background-color: var(--input-focus-background-color);
}
input[type="button"]:hover, input[type="submit"]:hover, button:hover, a.button:hover {
background-color: var(--button-hover-background-color);
border-color: var(--button-hover-border-color);
color: var(--button-hover-text-color);
}
a.button i {
font-size: 1.4em;
}
a.button--donate {
display: inline-flex;
align-items: center;
gap: 8px;
margin-right: 12px;
padding: 8px 16px;
font-size: 0.95rem;
border: 0;
}
a.button--donate svg {
width: 24px;
height: 24px;
fill: var(--text-color);
}
a.button--donate.button--liberapay {
color: #333;
background-color: #ffee16;
}
a.button--donate.button--liberapay svg {
fill: #333;
}
a.button--donate.button--liberapay:hover {
color: #333;
background-color: #fff463;
}
a.button--donate.button--kofi {
color: #333;
background-color: #1ac0ff;
}
a.button--donate.button--kofi svg {
fill: #333;
}
a.button--donate.button--kofi:hover {
color: #333;
background-color: #66d4ff;
}
button.inline {
min-height: auto;
margin: 0;
padding: 2px 8px;
}
/* DIALOGS */
dialog {
width: 100% !important;
max-width: 800px !important;
padding: 0 !important;
word-wrap: anywhere;
}
dialog > div {
padding: 1em;
}
dialog form[method="Dialog"] {
margin: 1em 0 0 !important;
}
dialog form[method="Dialog"] input {
width: auto;
}
dialog p {
font-size: 1rem !important;
margin: 1rem 0;
}
dialog p:first-of-type {
margin-top: 0;
}

View file

@ -27,55 +27,76 @@ You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
export default {
claimVersion: 1,
uri: 'https://fosstodon.org/@keyoxide',
fingerprint: '9f0048ac0b23301e1f77e994909f6bd6f80f485d',
status: 'verified',
matches: [
{
serviceprovider: {
type: 'web',
name: 'mastodon (demo)'
},
match: {
regularExpression: {},
isAmbiguous: true
},
profile: {
display: '@keyoxide@fosstodon.org',
uri: 'https://fosstodon.org/@keyoxide',
qr: null
},
proof: {
uri: 'https://fosstodon.org/@keyoxide',
request: {
fetcher: 'http',
access: 0,
format: 'json',
data: {
url: 'https://fosstodon.org/@keyoxide',
format: 'json'
}
}
},
claim: {
format: 1,
relation: 0,
path: [
'attachment',
'value'
]
}
}
],
verification: {
result: true,
completed: true,
errors: [],
proof: {
fetcher: 'http',
viaProxy: false
}
}
@use './styles/vars.scss';
@use './styles/layout.scss';
@use './styles/typography.scss';
@use './styles/forms.scss';
* {
box-sizing: border-box;
}
:focus {
z-index: 100;
}
/* HELPERS */
.spacer {
flex: 1;
}
.no-margin {
margin: 0 !important;
}
.full-width {
display: block;
width: 100% !important;
}
.half-width {
display: block;
width: 50% !important;
}
.select-all {
user-select: all;
}
#qr {
display: block;
width: 100% !important;
max-width: 256px !important;
height: auto !important;
margin: 0 auto 16px;
}
/* DIALOGS */
dialog {
max-width: 480px;
word-wrap: anywhere;
background-color: var(--section-background-color);
border: 0;
border-radius: 16px;
&::backdrop {
background-color: #000;
opacity: 0.8;
}
}
dialog form[method="Dialog"] {
margin: 1em 0 0 !important;
}
dialog form[method="Dialog"] input {
width: auto;
}
dialog p {
font-size: 1rem !important;
margin: 1rem 0;
}
dialog p:first-of-type {
margin-top: 0;
}

View file

@ -0,0 +1,175 @@
/*
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
.form-wrapper {
align-items: center;
padding-top: 1.4rem;
padding-bottom: 1.6rem;
margin-bottom: 48px;
}
.form-wrapper *:last-child {
margin-bottom: 0;
}
.form-wrapper form {
display: flex;
flex-direction: column;
margin: 0;
}
.form-wrapper h2 {
margin-top: 0;
}
form input[type="text"],
form input[type="search"] {
margin: 8px 0;
padding: 4px;
border: 1px solid var(--input-border-color);
border-radius: 3px;
font-size: 0.9rem;
}
form textarea {
width: 100%;
height: 128px;
margin: 8px 0;
resize: vertical;
font-size: 0.9rem;
border: 1px solid var(--input-border-color);
}
.button-wrapper {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0;
}
input,
textarea {
color: var(--input-text-color);
background-color: var(--input-background-color);
border: solid 1px var(--input-border-color);
border-radius: 4px;
&:hover {
background-color: var(--input-background-color-hover);
border-color: var(--input-border-color-hover);
color: var(--input-text-color-hover);
}
}
input[type="button"],
input[type="submit"],
button,
a.button {
display: inline-block;
margin: 8px 0;
padding: 4px 8px;
font-family: sans-serif;
font-size: 0.9rem;
text-decoration: none;
color: var(--button-text-color);
background-color: var(--button-background-color);
border: solid 1px var(--button-border-color);
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: var(--button-background-color-hover);
border-color: var(--button-border-color-hover);
color: var(--button-text-color-hover);
}
&.themed {
padding: 8px 12px;
color: var(--text-color-inverse);
background-color: var(--primary-color);
border: 0;
&:hover {
color: var(--text-color-inverse);
background-color: var(--primary-color-subtle);
}
}
}
button {
margin-right: 8px;
}
button.inline {
min-height: auto;
margin: 0;
padding: 2px 8px;
}
#search {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50vh;
gap: 48px;
background: transparent;
border: 0;
& > svg {
width: 96px;
fill: var(--primary-color);
}
form {
display: flex;
flex-direction: row;
width: 100%;
max-width: 30em;
input {
font-size: 1rem;
}
input[type="search"] {
flex: 1;
min-width: 0;
margin: 0;
padding: 8px;
border-radius: 8px 0 0 8px;
}
input[type="submit"] {
margin: 0;
padding: 8px 16px;
border-left: 0;
border-radius: 0 8px 8px 0;
}
}
}

View file

@ -0,0 +1,220 @@
/*
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
hr {
margin: 3em 0;
color: var(--line-color-subtle);
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
margin: 0 8px;
padding: 1.6rem 0 0;
line-height: 1.4rem;
font-family: sans-serif;
color: var(--text-color);
background-color: var(--body-background-color);
}
header {
margin: 0 0 3rem;
nav {
flex: 1;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 16px;
@media screen and (max-width: 400px) {
flex-direction: column;
align-items: start;
}
a.logo {
display: flex;
align-items: center;
gap: 16px;
font-size: 1.8rem;
text-decoration: none;
font-weight: bold;
@media screen and (max-width: 480px) {
gap: 8px;
font-size: 1.4rem;
}
img {
width: 40px;
height: 40px;
@media screen and (max-width: 480px) {
width: 32px;
height: 32px;
}
}
}
a {
margin: 0;
color: var(--text-color);
&:hover, &:active {
color: var(--link-color-hover);
}
}
.links {
display: flex;
gap: 12px;
}
}
}
main {
flex: 1;
}
footer {
margin: 4.8rem 0 0;
background-color: var(--footer-background-color);
color: var(--footer-text-color);
a {
display: inline-block;
color: var(--footer-text-color);
}
}
.container {
width: 100%;
max-width: 920px;
margin: 0 auto;
}
section {
margin: 0 0 32px;
padding: 16px;
background-color: var(--section-background-color);
border-radius: 8px;
h1:first-child, h2:first-child {
margin-top: 0;
}
p:last-child {
margin-bottom: 0;
}
&.transparent {
padding-left: 0;
padding-right: 0;
background-color: transparent;
border: 0;
}
}
.profile {
display: flex;
gap: 16px;
padding-top: 24px;
@media screen and (max-width: 600px) {
flex-direction: column;
padding-top: 32px;
}
.profile__name {
font-size: 1.6rem;
color: var(--text-color);
}
.profile__header {
flex: 3;
display: flex;
flex-direction: column;
flex-wrap: wrap;
align-items: center;
gap: 16px;
& p, & small {
margin: 0;
}
}
.profile__claims {
flex: 4;
}
.profile__avatar {
display: inline-block;
min-width: 96px;
max-width: 128px;
line-height: 0;
text-align: center;
border-radius: 16px;
}
.persona__description {
display: block;
margin: 8px 0 8px;
font-size: 0.9em;
svg {
width: 16px;
height: 16px;
vertical-align: sub;
}
}
}
.screenshots {
display: flex;
gap: 16px;
width: 100%;
padding: 8px;
background-color: #fafafa;
overflow-y: scroll;
img {
height: 400px;
}
}
.banners {
display: flex;
flex-wrap: wrap;
gap: 8px;
a {
img {
height: 32px;
}
}
}

View file

@ -0,0 +1,146 @@
/*
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
h1 {
font-size: 1.6em;
margin: 3.2rem 0 1.6rem;
font-weight: normal;
color: var(--h1-color);
cursor: default;
}
h2 {
margin: 1em 0 0.5em;
font-size: 1.2em;
font-weight: bold;
color: var(--h2-color);
cursor: default;
small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--h2-small-color);
color: #fff;
border-radius: 4px;
font-size: 0.8em;
}
}
h3 {
margin: 1em 0 0.5em;
font-size: 1.1em;
line-height: 1.6rem;
color: var(--h3-color);
font-weight: normal;
cursor: default;
small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--h3-small-color);
color: #fff;
border-radius: 4px;
}
}
h4 {
margin: 1.6rem 0;
font-size: 1em;
line-height: 1.6rem;
color: var(--h4-color);
font-weight: bold;
cursor: default;
small {
margin-left: 0.8rem;
padding: 3px 6px;
background-color: var(--h4-small-color);
color: #fff;
border-radius: 4px;
}
}
a {
color: var(--link-color);
&:hover {
color: var(--link-color-hover);
}
}
a.button i {
font-size: 1.4em;
}
a.button.button--donate {
display: inline-flex;
align-items: center;
gap: 8px;
margin-right: 12px;
padding: 8px 16px;
font-size: 0.95rem;
border: 0;
}
a.button.button--donate svg {
width: 24px;
height: 24px;
fill: var(--text-color);
}
a.button.button--donate.button--opencollective {
color: #fff;
background-color: #0c2d66;
}
a.button.button--donate.button--opencollective svg {
fill: #fff;
}
a.button.button--donate.button--opencollective:hover {
color: #fff;
background-color: #144aa9;
}
a.button.button--donate.button--liberapay {
color: #333;
background-color: #ffee16;
}
a.button.button--donate.button--liberapay svg {
fill: #333;
}
a.button.button--donate.button--liberapay:hover {
color: #333;
background-color: #fff463;
}
a.button.button--donate.button--kofi {
color: #333;
background-color: #1ac0ff;
}
a.button.button--donate.button--kofi svg {
fill: #333;
}
a.button.button--donate.button--kofi:hover {
color: #333;
background-color: #66d4ff;
}
ul {
padding-left: 1em;
list-style: '- ';
}

176
static-src/styles/vars.scss Normal file
View file

@ -0,0 +1,176 @@
/*
Copyright (C) 2023 Yarmo Mackenbach
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
details.
You should have received a copy of the GNU Affero General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer network,
you should also make sure that it provides a way for users to get its source.
For example, if your program is a web application, its interface could display
a "Source" link that leads users to an archive of the code. There are many
ways you could offer source, and different solutions will be better for different
programs; see section 13 for the specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary. For
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
:root {
--white: hsl(0, 0%, 100%);
--black: hsl(0, 0%, 0%);
--grey-10: hsl(0, 0%, 99%);
--grey-50: hsl(0, 0%, 95%);
--grey-100: hsl(0, 0%, 90%);
--grey-200: hsl(0, 0%, 80%);
--grey-300: hsl(0, 0%, 70%);
--grey-400: hsl(0, 0%, 60%);
--grey-500: hsl(0, 0%, 50%);
--grey-600: hsl(0, 0%, 40%);
--grey-700: hsl(0, 0%, 30%);
--grey-800: hsl(0, 0%, 20%);
--grey-900: hsl(0, 0%, 10%);
--grey-950: hsl(0, 0%, 7%);
--green-300: hsl(110, 45%, 70%);
--green-400: hsl(110, 45%, 60%);
--green-600: hsl(110, 45%, 40%);
--red-300: hsl(10, 60%, 70%);
--red-400: hsl(10, 60%, 60%);
--blue-500: rgb(67, 176, 234);
--blue-700: hsl(201, 90%, 30%);
--purple-50: rgb(249, 248, 251);
--purple-100: rgb(238, 236, 248);
--purple-200: hsl(250, 48%, 90%);
--purple-300: hsl(250, 48%, 80%);
--purple-400: hsl(250, 48%, 70%);
--purple-500: hsl(250, 48%, 65%);
--purple-600: hsl(250, 48%, 60%);
--purple-700: hsl(250, 48%, 55%);
--purple-900: hsl(250, 38%, 45%);
--yellow-100: hsl(56, 100%, 95%);
--yellow-200: hsl(56, 100%, 90%);
--yellow-500: hsl(56, 100%, 65%);
}
:root {
--primary-color-light: var(--purple-700);
--primary-color-dark: var(--purple-300);
--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);
--text-color: var(--grey-800);
--text-color-subtle: var(--grey-500);
--text-color-inverse: var(--white);
--h1-color: var(--text-color);
--h2-color: var(--text-color);
--h2-small-color: var(--primary-color-subtle);
--h3-color: var(--text-color-subtle);
--h3-small-color: var(--primary-color-subtle);
--h4-color: var(--text-color-subtle);
--h4-small-color: var(--primary-color-subtle);
--link-color: var(--blue-700);
--link-color-subtle: var(--text-color);
--link-color-hover: var(--primary-color);
--line-color-subtle: var(--grey-200);
--button-text-color: var(--text-color);
--button-text-color-hover: var(--text-color);
--button-border-color: var(--grey-300);
--button-border-color-hover: var(--grey-300);
--button-background-color: var(--grey-100);
--button-background-color-hover: var(--grey-200);
--input-text-color: var(--text-color);
--input-text-color-hover: var(--text-color);
--input-border-color: var(--grey-300);
--input-border-color-hover: var(--grey-300);
--input-background-color: var(--white);
--input-background-color-hover: var(--white);
--footer-text-color: var(--text-color-subtle);
@media (prefers-color-scheme: dark) {
color-scheme: dark;
--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);
--text-color: var(--grey-50);
--text-color-subtle: var(--grey-300);
--text-color-inverse: var(--grey-800);
--h1-color: var(--text-color);
--h2-color: var(--text-color);
--h2-small-color: var(--primary-color-subtle);
--h3-color: var(--text-color-subtle);
--h3-small-color: var(--primary-color-subtle);
--h4-color: var(--text-color-subtle);
--h4-small-color: var(--primary-color-subtle);
--link-color: var(--blue-500);
--link-color-subtle: var(--text-color);
--link-color-hover: var(--primary-color);
--line-color-subtle: var(--grey-700);
--button-text-color: var(--text-color);
--button-text-color-hover: var(--text-color);
--button-border-color: var(--grey-700);
--button-border-color-hover: var(--grey-700);
--button-background-color: var(--grey-700);
--button-background-color-hover: var(--grey-600);
--input-text-color: var(--text-color);
--input-text-color-hover: var(--text-color);
--input-border-color: var(--grey-700);
--input-border-color-hover: var(--grey-700);
--input-background-color: var(--grey-800);
--input-background-color-hover: var(--grey-800);
--footer-text-color: var(--text-color-subtle);
}
}
kx-claim, kx-key {
--loader-color: var(--grey-400);
--success-color: var(--green-600);
--failure-color: var(--red-400);
--background-color: var(--grey-10);
--header-background-color: var(--grey-50);
@media (prefers-color-scheme: dark) {
--loader-color: var(--grey-400);
--success-color: var(--green-400);
--failure-color: var(--red-300);
--background-color: var(--grey-800);
--header-background-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
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/
import dialogPolyfill from 'dialog-polyfill'
import QRCode from 'qrcode'
import * as openpgp from 'openpgp'
import * as utils from './utils.js'
@ -51,17 +50,6 @@ const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-
// Initialize UI elements and event listeners
export function init() {
// Register modals
document.querySelectorAll('dialog').forEach(function(d) {
dialogPolyfill.registerDialog(d);
d.addEventListener('click', function(ev) {
if (ev && ev.target != d) {
return;
}
d.close();
});
});
// Run context-dependent scripts
if (elFormEncrypt) {
runEncryptionForm()
@ -349,8 +337,8 @@ const runArgon2GenerationUtility = () => {
elFeedback.innerHTML = "";
} else {
let feedbackContent = "";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixArgon2Input();'>Fix now</button><br>";
}
if (!(elInput.value === elInput.value.toLowerCase())) {
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');
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}`;
}
@ -414,8 +402,8 @@ const runBcryptGenerationUtility = () => {
elFeedback.innerHTML = "";
} else {
let feedbackContent = "";
if (!(/openpgp4fpr:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
if (!(/[openpgp4fpr|aspe]:[0-9a-zA-Z]+/.test(elInput.value))) {
feedbackContent += "❗ Valid OpenPGP proofs must begin with <strong>openpgp4fpr:</strong>. <button class='inline' onclick='window.kx__fixBcryptInput();'>Fix now</button><br>";
}
if (!(elInput.value === elInput.value.toLowerCase())) {
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');
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}`;
}

View file

@ -46,19 +46,20 @@ export async function computeWKDLocalPart(localPart) {
// Generate Keyoxide profile URL
export async function generateProfileURL(data) {
let hostname = data.hostname || window.location.hostname;
let scheme = data.scheme || window.location.protocol.slice(0,-1);
if (data.input == "") {
return "Waiting for input…";
}
switch (data.source) {
case "wkd":
return `https://${hostname}/${data.input}`;
return `${scheme}://${hostname}/${data.input}`;
break;
case "hkp":
if (/.*@.*\..*/.test(data.input)) {
return `https://${hostname}/hkp/${data.input}`;
return `${scheme}://${hostname}/hkp/${data.input}`;
} else {
return `https://${hostname}/${data.input}`;
return `${scheme}://${hostname}/${data.input}`;
}
break;
case "keybase":
@ -67,40 +68,29 @@ export async function generateProfileURL(data) {
return "Incorrect Keybase public key URL.";
}
const match = data.input.match(re);
return `https://${hostname}/keybase/${match[1]}/${match[2]}`;
return `${scheme}://${hostname}/keybase/${match[1]}/${match[2]}`;
break;
}
}
// Fetch OpenPGP key based on information stored in window
export async function fetchProfileKey() {
if (window.kx.key.object && window.kx.key.object instanceof openpgp.PublicKey) {
if (window.kx.publicKey.key && window.kx.publicKey.key instanceof openpgp.PublicKey) {
return;
}
const rawKeyData = await fetch(window.kx.key.url)
let key, errorMsg
try {
key = (await openpgp.readKey({
binaryKey: new Uint8Array(await rawKeyData.clone().arrayBuffer())
armoredKey: window.kx.publicKey.encodedKey
}))
} catch(error) {
} catch (error) {
errorMsg = error.message
}
if (!key) {
try {
key = (await openpgp.readKey({
armoredKey: await rawKeyData.clone().text()
}))
} catch (error) {
errorMsg = error.message
}
}
if (key) {
window.kx.key.object = key
window.kx.publicKey.key = key
return
} else {
throw new Error(`Public key could not be fetched (${errorMsg})`)
@ -240,4 +230,4 @@ export async function verifyBcryptHash(input, hash) {
} catch (_) {
return false;
}
}
}

View file

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

View file

@ -66,38 +66,78 @@ describe('browser', function () {
})
})
describe('generateProfileURL()', function () {
it('should handle a WKD URL', async function () {
it('should handle a https WKD URL', async function () {
const local = await utils.generateProfileURL({
source: 'wkd',
input: 'test@doip.rocks',
hostname: 'keyoxide.instance'
hostname: 'keyoxide.instance',
scheme: 'https'
})
local.should.equal('https://keyoxide.instance/test@doip.rocks')
})
it('should handle a HKP+email URL', async function () {
it('should handle a http WKD URL', async function () {
const local = await utils.generateProfileURL({
source: 'wkd',
input: 'test@doip.rocks',
hostname: 'keyoxide.instance',
scheme: 'http'
})
local.should.equal('http://keyoxide.instance/test@doip.rocks')
})
it('should handle a https HKP+email URL', async function () {
const local = await utils.generateProfileURL({
source: 'hkp',
input: 'test@doip.rocks',
hostname: 'keyoxide.instance'
hostname: 'keyoxide.instance',
scheme: 'https'
})
local.should.equal('https://keyoxide.instance/hkp/test@doip.rocks')
})
it('should handle a HKP+fingerprint URL', async function () {
it('should handle a http HKP+email URL', async function () {
const local = await utils.generateProfileURL({
source: 'hkp',
input: 'test@doip.rocks',
hostname: 'keyoxide.instance',
scheme: 'http'
})
local.should.equal('http://keyoxide.instance/hkp/test@doip.rocks')
})
it('should handle a https HKP+fingerprint URL', async function () {
const local = await utils.generateProfileURL({
source: 'hkp',
input: '3637202523E7C1309AB79E99EF2DC5827B445F4B',
hostname: 'keyoxide.instance'
hostname: 'keyoxide.instance',
scheme: 'https'
})
local.should.equal('https://keyoxide.instance/3637202523E7C1309AB79E99EF2DC5827B445F4B')
})
it('should handle a keybase URL', async function () {
it('should handle a http HKP+fingerprint URL', async function () {
const local = await utils.generateProfileURL({
source: 'hkp',
input: '3637202523E7C1309AB79E99EF2DC5827B445F4B',
hostname: 'keyoxide.instance',
scheme: 'http'
})
local.should.equal('http://keyoxide.instance/3637202523E7C1309AB79E99EF2DC5827B445F4B')
})
it('should handle a https keybase URL', async function () {
const local = await utils.generateProfileURL({
source: 'keybase',
input: 'https://keybase.io/doip/pgp_keys.asc?fingerprint=3637202523E7C1309AB79E99EF2DC5827B445F4B',
hostname: 'keyoxide.instance'
hostname: 'keyoxide.instance',
scheme: 'https'
})
local.should.equal('https://keyoxide.instance/keybase/doip/3637202523E7C1309AB79E99EF2DC5827B445F4B')
})
it('should handle a http keybase URL', async function () {
const local = await utils.generateProfileURL({
source: 'keybase',
input: 'https://keybase.io/doip/pgp_keys.asc?fingerprint=3637202523E7C1309AB79E99EF2DC5827B445F4B',
hostname: 'keyoxide.instance',
scheme: 'http'
})
local.should.equal('http://keyoxide.instance/keybase/doip/3637202523E7C1309AB79E99EF2DC5827B445F4B')
})
})
})
})
})

View file

@ -1,6 +1,11 @@
import 'chai/register-should.js'
import esmock from 'esmock'
import * as doipjs from 'doipjs'
import * as utils from '../src/server/utils.js'
const _env = Object.assign({},process.env)
describe('server', function () {
describe('utils', function () {
describe('computeWKDLocalPart()', function () {
@ -26,4 +31,89 @@ describe('server', function () {
})
})
})
})
// NOTE: This is necessarily brittle. If these tests fail
// in the future, start looking here for what new behaviour
// in the implementation is or isn't getting mocked
// appropriately.
describe('index', function () {
describe('generateHKPProfile()', function() {
let index;
let fingerprint;
/** @type {import('doipjs').Profile */
let profile;
this.beforeEach(async () => {
// Common arrangement pieces that don't change per test
fingerprint = '79895B2E0F87503F1DDE80B649765D7F0DDD9BD5'
process.env.DOMAIN = "keyoxide.org"
const persona = new doipjs.Persona("test", [new doipjs.Claim('dns:domain.tld?type=TXT')])
profile = new doipjs.Profile(doipjs.enums.ProfileType.OPENPGP, fingerprint, [persona])
// mock the appropriate pieces of our dependencies so we
// can test just the `keyoxide.url` return value.
index = await esmock('../src/server/index.js', {
'../src/server/openpgpProfiles.js': {
fetchHKP: () => {
return Promise.resolve(profile)
}
},
'libravatar': {
get_avatar_url: () => {
return "example.org/avatar.png"
}
}
})
})
this.afterEach(() => {
process.env = _env
})
it('should handle implicit scheme for keyoxide URL', async function () {
// Arrange
// no setting process.env.SCHEME
// Act
const local = await index.generateHKPProfile(fingerprint)
// Assert
local.verifiers[0].url.should.equal(`https://keyoxide.org/hkp/${fingerprint}`)
})
it('should handle explicit http scheme for keyoxide URL', async function () {
// Arrange
process.env.SCHEME = "http"
// Act
const local = await index.generateHKPProfile(fingerprint)
// Assert
local.verifiers[0].url.should.equal(`http://keyoxide.org/hkp/${fingerprint}`)
})
it('should handle explicit https scheme for keyoxide URL', async function () {
// Arrange
process.env.SCHEME = "https"
// Act
const local = await index.generateHKPProfile(fingerprint)
// Assert
local.verifiers[0].url.should.equal(`https://keyoxide.org/hkp/${fingerprint}`)
})
})
})
})

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

@ -1,6 +1,6 @@
extends templates/base.pug
block content
section.long_form.narrow
section
h1= title
.card !{ content }
| !{ content }

View file

@ -1,76 +1,44 @@
extends templates/base.pug
block content
#search.form-wrapper.card
h2#searchTitle View a profile
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")
label#searchQuery(for="query") Query for fingerprint or email identifier
input#query(type="search" name="query" required placeholder="3637202523e7c1309ab79e99ef2dc5827b445f4b, test@doip.rocks" aria-labelledby="searchTitle searchQuery")
input#query(type="search" name="query" required placeholder="Search for a profile")
input(type="submit" value="Search")
input(type="submit" value="View profile")
p Or view a
a(href="/sig") plaintext signature
| profile.
if highlights.length > 0
h2 Highlights
.hcards.hcards--highlights
each hl in highlights
.card.card--small-profile
h3.name= hl.name
p
span.fingerprint= hl.fingerprint
br
span.details= hl.description
.spacer
p
a(href=`/${hl.fingerprint}`).button.full-width View profile
- var n = 0
while n < 3-highlights.length
.card.card--small-profile-dummy
- n++
h2 About Keyoxide
.hcards.hcards--features.hcards--max-2
.card
h3 Online identity
p Verifying online identity with cryptography. View
a(href="/project@keyoxide.org") Keyoxide's profile
| .
.card
h3 Mobile app
p Available on Android and iOS. More information on
a(href="https://mobile.keyoxide.org") mobile.keyoxide.org
| .
.card
h3 Decentralization &amp; privacy
p No central server or database. No collected data. Control how your data is stored and accessed.
.card
h3 Cryptography
p Your online identity verifiably signed with widely-used cryptographic standards (OpenPGP, others coming).
.card
h3 Open Source
p All Keyoxide projects are licensed under AGPL-3.0-or-later.
.card
h3 Transparent funding
p Funded by donations. Keyoxide stands against VC and surveillance capitalism.
h2 Community
.card
section
h2 About Keyoxide
p Keyoxide is a decentralized tool to create and verify decentralized online identities.
p Just like passports for real life identities, Keyoxide can be used to verify the online identity of people to make sure one is interacting with whom they intended to be and not an imposter.
p Unlike real life passports, Keyoxide works with online identities or "personas", meaning these identities can be anonymous and one can have multiple separate personas to protect their privacy, both online and in real life.
p
| Discussion of the Keyoxide project happens primarily on the
| Here is what a
a(href="/project@keyoxide.org") Keyoxide profile
| looks like.
p
a(href="https://docs.keyoxide.org/getting-started") Get started
| and create your own!
section
h2 Community
p
| Discussion of the Keyoxide project primarily happens on the
a(href="https://matrix.to/#/#keyoxide:matrix.org") #keyoxide Matrix channel
| and the
a(href="https://community.keyoxide.org") Keyoxide Community Forum
| . This is the place to propose new service providers for identity verification, make feature suggestions or report bugs.
| .
| The Matrix channel is great for troubleshooting.
| The forum is the place to propose new service providers for identity verification, make feature suggestions or report bugs.
p
| There is also the
a(href="irc://irc.libera.chat/#keyoxide") #keyoxide:libera.chat IRC room
| , the
a(href="https://matrix.to/#/#keyoxide:matrix.org") #keyoxide Matrix channel
| and the
a(href="https://lists.sr.ht/~yarmo/keyoxide-devel") keyoxide-devel mailing list
| . The IRC room and Matrix channel are bridged together.
p
| The project is also present on the fediverse as
| The project is also present on the fediverse:
a(href="https://fosstodon.org/@keyoxide") @keyoxide@fosstodon.org
| .
p
@ -79,10 +47,10 @@ block content
| .
h2 Fund the project
.card
section
h2 Fund the project
p
| The development of Keyoxide and the Decentralized OpenPGP Identity Proofs ecosystem is entirely funded by donations.
| The development of Keyoxide and the Decentralized Online Identity Proofs ecosystem is entirely funded by donations.
p
| The Keyoxide project was awarded a NGI Zero grant from the
a(href='https://nlnet.nl/') NLnet Foundation
@ -90,9 +58,6 @@ block content
p
| We rely on your support to keep working on Big Tech-independent secure online identity.
p
a.button.button--donate.button--liberapay(href='https://liberapay.com/Keyoxide/')
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 32 32"><path d="M3.093 0A3.093 3.093 0 0 0 0 3.093v25.813a3.093 3.093 0 0 0 3.093 3.093h25.813a3.093 3.093 0 0 0 3.093-3.093V3.093A3.093 3.093 0 0 0 28.906 0zm12.276 5.307L12.344 17.85a4.056 4.056 0 0 0-.099.719c-.011.197.031.396.119.572a.926.926 0 0 0 .448.401c.209.104.505.172.881.199l-.652 2.676c-1.031 0-1.844-.129-2.427-.4c-.588-.271-1.011-.636-1.265-1.099a3.245 3.245 0 0 1-.371-1.6a9.035 9.035 0 0 1 .251-1.927l2.765-11.563zm5.204 5.182c.812 0 1.509.125 2.099.371a3.862 3.862 0 0 1 1.448 1.015c.375.428.656.928.839 1.5c.181.573.271 1.188.271 1.839c0 1.057-.172 2.027-.521 2.907a6.915 6.915 0 0 1-1.448 2.276a6.49 6.49 0 0 1-2.224 1.489c-.859.355-1.801.531-2.817.531c-.489 0-.984-.041-1.479-.129l-.98 3.943h-3.224l3.615-15.063a17.858 17.858 0 0 1 1.989-.469c.803-.14 1.615-.213 2.433-.208zm-.417 2.724a6.07 6.07 0 0 0-1.307.131l-1.521 6.333c.245.063.547.088.912.088c.567 0 1.083-.104 1.547-.317a3.463 3.463 0 0 0 1.187-.88a4.07 4.07 0 0 0 .761-1.36a5.308 5.308 0 0 0 .271-1.755c0-.625-.136-1.151-.412-1.589c-.276-.432-.755-.651-1.437-.651z"></path></svg>
| Donate via Liberapay
a.button.button--donate.button--kofi(href='https://ko-fi.com/keyoxide/')
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 32 32"><path d="M31.844 11.932c-1.032-5.448-6.48-6.125-6.48-6.125H.964C.156 5.807.057 6.87.057 6.87S-.052 16.637.03 22.637c.22 3.228 3.448 3.561 3.448 3.561s11.021-.031 15.953-.067c3.251-.568 3.579-3.423 3.541-4.98c5.808.323 9.896-3.776 8.871-9.219zm-14.751 4.683c-1.661 1.932-5.348 5.297-5.348 5.297s-.161.161-.417.031c-.099-.073-.14-.12-.14-.12c-.595-.588-4.491-4.063-5.381-5.271c-.943-1.287-1.385-3.599-.119-4.948c1.265-1.344 4.005-1.448 5.817.541c0 0 2.083-2.375 4.625-1.281c2.536 1.095 2.443 4.016.963 5.751zm8.23.636c-1.24.156-2.244.036-2.244.036V9.714h2.359s2.631.735 2.631 3.516c0 2.552-1.313 3.557-2.745 4.021z"></path></svg>
| Donate via Ko-fi
a.button.button--donate.button--opencollective(href='https://opencollective.com/keyoxide')
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12c2.54 0 4.894-.79 6.834-2.135l-3.107-3.109a7.715 7.715 0 1 1 0-13.512l3.107-3.109A11.943 11.943 0 0 0 12 0zm9.865 5.166l-3.109 3.107A7.67 7.67 0 0 1 19.715 12a7.682 7.682 0 0 1-.959 3.727l3.109 3.107A11.943 11.943 0 0 0 24 12c0-2.54-.79-4.894-2.135-6.834z"></path></svg>
| Donate via OpenCollective

View file

@ -1,30 +1,33 @@
footer
.container
.hcards
div
h1 Keyoxide
a(href="/") Homepage
br
a(href="/util") Utilities
br
a(href="/privacy") Privacy policy
p
| This site:
a(href="/") Homepage
| •
a(href="/util") Utilities
| •
a(href="/privacy") Privacy policy
br
div
h1 Keyoxide project
a(href="https://keyoxide.org") Keyoxide.org
br
a(href="https://community.keyoxide.org") Community forum
br
a(href="https://docs.keyoxide.org") Documentation
br
a(href="https://blog.keyoxide.org") Blog
| Keyoxide project:
a(href="https://community.keyoxide.org") Forum
| •
a(href="https://docs.keyoxide.org") Documentation
| •
a(href="https://blog.keyoxide.org") Blog
| •
a(href="https://codeberg.org/keyoxide") Source code
br
div
h1 Development
a(href="https://codeberg.org/keyoxide/") Source code
br
a(href="https://doip.rocks") doip.rocks
br
a(href="https://ariadne.id") ariadne.id
| Related:
a(href="https://doip.rocks") doip.rocks
| •
a(href="https://ariadne.id") ariadne.id
p.copyright &copy; 2022 Keyoxide project contributors
p
| Version
a(href=meta.keyoxide.sourceUrl)= meta.keyoxide.semver
br
| &copy; 2020-2024 Keyoxide project contributors

View file

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

View file

@ -1,46 +1,55 @@
extends templates/base.pug
mixin generateUser(user, isPrimary)
h2
span.p-email #{user.userData.email}
if isPrimary
small.primary primary
if user.userData.comment
span.p-comment &#9432; #{user.userData.comment}
each claim in user.claims
if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim)
details(aria-label="Claim")
summary
.info
p.subtitle= claim.matches[0].serviceprovider.name
p.title= claim.matches[0].profile.display
.icons
.verificationStatus(data-value='running')
.inProgress
.content
.subsection
img(src='/static/img/link.png')
div
if (claim.matches[0].profile.uri)
p Profile link:
a(rel='me' href=claim.matches[0].profile.uri aria-label="Link to profile")= claim.matches[0].profile.uri
else
p Profile link: not accessible from browser
if (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
mixin generatePersona(persona, isPrimary)
if persona.claims.length > 0
h2
if persona.email
| Identity claims (
span.p-email #{persona.email}
| )
else
| Identity claims
if isPrimary
small.primary primary
if persona.description
span.persona__description.p-comment
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-info"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
| #{persona.description}
each claim in persona.claims
if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim,data-status='running')
details(aria-label="Claim")
summary
.info
img(src=`https://design.keyoxide.org/brands/service-providers/_/icon.svg` onerror="this.src='https://design.keyoxide.org/brands/service-providers/_/icon.svg'")
p
span.title= claim.display.profileName
span.subtitle-wrapper
| [
span.subtitle= claim.display.serviceproviderName
| ]
.icons
.verificationStatus
.inProgress
<svg style="width:24px;height:24px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z"></path></svg>
.success
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
.failure
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
.content
.subsection
img(src='/static/img/link.png')
div
p Claim link:
a(rel="me" href=claim.uri aria-label="Link to claim")= claim.uri
block content
script.
kx = {
key: {
url: "!{data && data.key && data.key.fetchURL ? data.key.fetchURL : null}",
object: null
if (data && 'publicKey' in data)
script.
kx = {
publicKey: !{JSON.stringify(data.publicKey)}
}
}
if (data && 'errors' in data && data.errors.length > 0)
section
@ -58,7 +67,7 @@ block content
a(href="https://docs.keyoxide.org/getting-started/something-went-wrong/") documentation
| for help.
else
section.profile.narrow.h-card
section.profile
noscript
p Keyoxide requires JavaScript to function.
@ -80,12 +89,7 @@ block content
dialog#dialog--verifySignature
div
form(method='post')
label(for="sigVerInput") Signature
textarea#sigVerInput.input(name='signature')
input.no-margin(type='submit' name='submit' value='VERIFY SIGNATURE')
br
br
label(for="sigVerOutput") Verification result
label(for="sigVerInput") Signature name
textarea#sigVerOutput.output(name='message' placeholder='Waiting for input' readonly)
form(method="dialog")
input(type="submit" value="Close")
@ -107,10 +111,22 @@ block content
input(type='submit', name='submit', value='Generate profile')
unless (isSignature && !signature)
#profileHeader.card.card--transparent.card--profileHeader
img#profileAvatar.u-logo(src=data.extra.avatarURL alt="avatar")
if (theme)
style
| :root {
| --primary-color-light: #{theme.primary.light};
| --primary-color-dark: #{theme.primary.dark};
| --primary-color-subtle-light: #{theme.primarySubtle.light};
| --primary-color-subtle-dark: #{theme.primarySubtle.dark};
| --background-color-light: #{theme.background.light};
| --background-color-dark: #{theme.background.dark};
| }
.profile__header
img.profile__avatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
p#profileName.p-name= data.keyData.users[data.keyData.primaryUserIndex].userData.name
p.profile__name.p-name= data.personas[data.primaryPersonaIndex].name
if (data.personas[data.primaryPersonaIndex].description)
p= data.personas[data.primaryPersonaIndex].description
if (enable_message_encryption || enable_signature_verification)
.button-wrapper
@ -119,27 +135,33 @@ block content
if (enable_signature_verification)
button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature
+generateUser(data.keyData.users[data.keyData.primaryUserIndex], true)
each user, index in data.keyData.users
unless index == data.keyData.primaryUserIndex
+generateUser(user, false)
.profile__claims
+generatePersona(data.personas[data.primaryPersonaIndex], true && data.personas.length > 1)
each persona, index in data.personas
unless index == data.primaryPersonaIndex
+generatePersona(persona, false)
h2 Profile
if data.verifiers.length > 0
button.themed(onClick=`showQR('${data.verifiers[0].url}', 'profile_verifier_url')` aria-label='Show profile ID QR') Keyoxide profile QR
button.themed(onClick=`showQR('${data.identifier}', 'profile_identifier')` aria-label='Show profile ID QR') Profile ID QR
#profileProofs.card.card--transparent
h2 Key
kx-key.kx-item(data-keydata=data.keyData)
details(aria-label="Key")
summary
.info
p.subtitle= data.keyData.key.fetchMethod
p.title= data.keyData.fingerprint
.content
.subsection
img(src='/static/img/link.png')
div
p Key link:
a.u-key(href=data.keyData.key.uri rel="pgpkey" aria-label="Link to cryptographic key")= data.keyData.key.uri
hr
.subsection
img(src='/static/img/qrcode.png')
div
button(onClick=`showQR('${data.keyData.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR
section
h2 Profile information
if (data && data.publicKey)
h3 Public key
kx-key.kx-item(data-keydata=data.publicKey)
details(aria-label="Key")
summary
.info
p
span.title= data.identifier
span.subtitle-wrapper
| [
span.subtitle= data.publicKey.fetch.method
| ]
.content
.subsection
img(src='/static/img/link.png')
div
p Key link:
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
extends ../templates/base.pug
block content
section.narrow
h1 Keyoxide utilities
section
h2 Keyoxide utilities
p
a(href="/util/profile-url") Get the URL for a Keyoxide profile
p

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug
block content
section.narrow
section
h1 Profile URL
form#form-util-profile-url(method='post')
p This tool generates an URL for your Keyoxide profile page.

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug
block content
section.narrow
section
h1 QR Code
form#form-util-qr(method='post')
pre

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug
block content
section.narrow
section
h1 QR Code
form#form-util-qrfp(method='post')
p

View file

@ -1,7 +1,7 @@
extends ../templates/base.pug
block content
section.narrow
section
h1 Web Key Directory URL generator
form#form-util-wkd(method='post')
p

View file

@ -13,10 +13,8 @@ export default (env) => {
mode: env.mode,
entry: {
main: {
import: './static-src/index.js',
dependOn: 'openpgp',
},
openpgp: './node_modules/openpgp/dist/openpgp.js',
import: './static-src/index.js'
}
},
output: {
filename: '[name].js',
@ -27,10 +25,11 @@ export default (env) => {
module: {
rules: [
{
test: /\.css$/,
test: /\.s[ca]ss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
'css-loader',
'sass-loader'
]
}
]
@ -62,12 +61,19 @@ export default (env) => {
new CopyPlugin({
patterns: [
{ from: './static-src/files/', to: '../static/' },
{ from: './node_modules/openpgp/dist/openpgp.js', to: '../static/openpgp.js' },
{ from: './node_modules/doipjs/dist/doip.core.js', to: '../static/doip.js' },
{ from: './node_modules/doipjs/dist/doip.fetchers.minimal.js', to: '../static/doipFetchers.js' },
],
options: {
concurrency: 10,
},
}),
],
externals: {
doipjs: 'doip',
openpgp: 'openpgp'
}
}
} else {
return {}

13358
yarn.lock

File diff suppressed because it is too large Load diff