mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 14:59:29 -07:00
feat: update server code
This commit is contained in:
parent
6676c78961
commit
59fc51c407
8 changed files with 559 additions and 268 deletions
|
@ -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
|
||||
|
|
|
@ -30,7 +30,6 @@ 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'
|
||||
|
||||
const router = express.Router()
|
||||
const md = markdownImport({ typographer: true })
|
||||
|
@ -48,7 +47,7 @@ router.get('/', (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
res.render('index', { highlights, demoData })
|
||||
res.render('index', { highlights })
|
||||
})
|
||||
|
||||
router.get('/privacy', (req, res) => {
|
||||
|
|
|
@ -30,6 +30,7 @@ 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 { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
|
||||
import { Profile } from 'doipjs'
|
||||
|
||||
const router = express.Router()
|
||||
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
||||
|
@ -41,10 +42,10 @@ router.get('/sig', (req, res) => {
|
|||
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.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
isSignature: true,
|
||||
signature: req.body.signature,
|
||||
enable_message_encryption: false,
|
||||
|
@ -55,10 +56,10 @@ router.post('/sig', bodyParser, async (req, res) => {
|
|||
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.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -67,10 +68,10 @@ router.get('/wkd/:id', async (req, res) => {
|
|||
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.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -79,10 +80,10 @@ router.get('/hkp/:id', async (req, res) => {
|
|||
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.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -91,10 +92,10 @@ router.get('/hkp/:server/:id', async (req, res) => {
|
|||
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.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -103,10 +104,10 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => {
|
|||
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.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
|
376
src/schemas.js
Normal file
376
src/schemas.js
Normal file
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
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"]
|
||||
},
|
||||
"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": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Account name to display in the user interface"
|
||||
},
|
||||
"url": {
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to link to in the user interface"
|
||||
},
|
||||
"serviceProviderName": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Name of the service provider to display in the user interface"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
|
@ -1,81 +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/>.
|
||||
*/
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,45 +29,48 @@ 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 => {
|
||||
profile.addVerifier('keyoxide', `https://${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 => {
|
||||
profile.addVerifier('keyoxide', `https://${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 +81,27 @@ 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 = `https://${process.env.DOMAIN}/hkp/${id}`
|
||||
} else {
|
||||
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||
keyoxideUrl = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||
}
|
||||
|
||||
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 +110,31 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
|||
const generateAutoProfile = async (id) => {
|
||||
let result
|
||||
|
||||
const aspeRe = /aspe:(.*):(.*)/
|
||||
|
||||
if (aspeRe.test(id)) {
|
||||
result = await generateAspeProfile(id)
|
||||
|
||||
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 +144,20 @@ const generateSignatureProfile = async (signature) => {
|
|||
|
||||
return fetchSignature(signature)
|
||||
.then(async key => {
|
||||
let keyData = key.keyData
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
key.keyData = undefined
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
let profile = await doipjs.signatures.parse(key.publicKey)
|
||||
profile.addVerifier('keyoxide', keyoxideUrl)
|
||||
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,72 +168,91 @@ 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 => {
|
||||
profile.addVerifier('keyoxide', `https://${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
|
||||
})
|
||||
})
|
||||
|
||||
keyData.primaryUserIndex ||= 0
|
||||
// Overwrite avatarUrl
|
||||
// TODO: don't overwrite avatarUrl once it's fully supported
|
||||
profile.personas[profile.primaryPersonaIndex].avatarUrl = `https://api.dicebear.com/6.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
|
||||
|
||||
return keyData
|
||||
return profile
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
// 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 computeExtraData = async (key, keyData) => {
|
||||
|
@ -265,6 +265,7 @@ const computeExtraData = async (key, keyData) => {
|
|||
}
|
||||
}
|
||||
|
||||
export { generateAspeProfile }
|
||||
export { generateWKDProfile }
|
||||
export { generateHKPProfile }
|
||||
export { generateAutoProfile }
|
||||
|
|
|
@ -39,10 +39,9 @@ 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
|
||||
}
|
||||
let publicKey = null
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
if (!id.includes('@')) {
|
||||
reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||
|
@ -59,14 +58,14 @@ const fetchWKD = (id) => {
|
|||
|
||||
const hash = createHash('md5').update(id).digest('hex')
|
||||
if (c && await c.get(hash)) {
|
||||
plaintext = Uint8Array.from((await c.get(hash)).split(','))
|
||||
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
|
||||
}
|
||||
|
||||
if (!plaintext) {
|
||||
if (!profile) {
|
||||
try {
|
||||
plaintext = await got(urlAdvanced).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
output.fetchURL = urlAdvanced
|
||||
fetchURL = urlAdvanced
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
|
@ -76,7 +75,7 @@ const fetchWKD = (id) => {
|
|||
try {
|
||||
plaintext = await got(urlDirect).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
output.fetchURL = urlDirect
|
||||
fetchURL = urlDirect
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
|
@ -91,24 +90,29 @@ const fetchWKD = (id) => {
|
|||
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 {
|
||||
publicKey = await readKey({
|
||||
binaryKey: plaintext
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
if (!publicKey) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
profile = await doipjs.openpgp.parsePublicKey(publicKey)
|
||||
profile.publicKey.fetch.method = 'wkd'
|
||||
profile.publicKey.fetch.query = id
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
}
|
||||
|
||||
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 (c && plaintext instanceof Uint8Array) {
|
||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
@ -116,10 +120,8 @@ const fetchWKD = (id) => {
|
|||
const fetchHKP = (id, keyserverDomain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org'
|
||||
|
||||
|
@ -135,31 +137,36 @@ const fetchHKP = (id, keyserverDomain) => {
|
|||
query = `0x${sanitizedId}`
|
||||
}
|
||||
|
||||
output.fetchURL = `https://${keyserverDomainNormalized}/pks/lookup?op=get&options=mr&search=${query}`
|
||||
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)) {
|
||||
output.publicKey = await readKey({
|
||||
armoredKey: await c.get(hash)
|
||||
})
|
||||
} else {
|
||||
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
try {
|
||||
output.publicKey = await doipjs.keys.fetchHKP(query, keyserverDomainNormalized)
|
||||
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be fetched using HKP'))
|
||||
profile = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
if (!profile) {
|
||||
reject(new Error('No public keys could be fetched using HKP'))
|
||||
return
|
||||
}
|
||||
|
||||
if (c && output.publicKey instanceof PublicKey) {
|
||||
await c.set(hash, output.publicKey.armor(), 60 * 1000)
|
||||
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)
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
@ -167,11 +174,7 @@ const fetchHKP = (id, keyserverDomain) => {
|
|||
const fetchSignature = (signature) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null,
|
||||
keyData: null
|
||||
}
|
||||
let profile = null
|
||||
|
||||
// Check validity of signature
|
||||
let signatureData
|
||||
|
@ -185,30 +188,18 @@ const fetchSignature = (signature) => {
|
|||
|
||||
// Process the signature
|
||||
try {
|
||||
output.keyData = await doipjs.signatures.process(signature)
|
||||
output.publicKey = output.keyData.key.data
|
||||
profile = await doipjs.signatures.parse(signature)
|
||||
// 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'))
|
||||
if (!profile) {
|
||||
reject(new Error('No profile 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)
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
@ -216,23 +207,24 @@ const fetchSignature = (signature) => {
|
|||
const fetchKeybase = (username, fingerprint) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
try {
|
||||
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint)
|
||||
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
||||
profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
|
||||
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) {
|
||||
if (!profile) {
|
||||
reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
profile.publicKey.fetch.method = 'http'
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
|
@ -39,7 +39,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'
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue