feat: update server code

This commit is contained in:
Yarmo Mackenbach 2023-07-13 11:10:58 +02:00
parent 6676c78961
commit 59fc51c407
No known key found for this signature in database
GPG key ID: 3C57D093219103A3
8 changed files with 559 additions and 268 deletions

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/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
import express from 'express' import express from 'express'
import apiRouter0 from '../api/v0/index.js' import apiRouter3 from '../api/v3/index.js'
import apiRouter1 from '../api/v1/index.js'
import apiRouter2 from '../api/v2/index.js'
const router = express.Router() const router = express.Router()
if ((process.env.ENABLE_MAIN_MODULE ?? 'true') === 'true') { router.get('/0', (req, res) => {
router.use('/0', apiRouter0) return res.status(501).send('Proxy v0 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
} })
router.use('/1', apiRouter1) router.get('/1', (req, res) => {
router.use('/2', apiRouter2) 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 export default router

View file

@ -30,7 +30,6 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import express from 'express' import express from 'express'
import markdownImport from 'markdown-it' import markdownImport from 'markdown-it'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import demoData from '../server/demo.js'
const router = express.Router() const router = express.Router()
const md = markdownImport({ typographer: true }) 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) => { router.get('/privacy', (req, res) => {

View file

@ -30,6 +30,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import express from 'express' import express from 'express'
import bodyParserImport from 'body-parser' import bodyParserImport from 'body-parser'
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js' import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
import { Profile } from 'doipjs'
const router = express.Router() const router = express.Router()
const bodyParser = bodyParserImport.urlencoded({ extended: false }) const bodyParser = bodyParserImport.urlencoded({ extended: false })
@ -41,10 +42,10 @@ router.get('/sig', (req, res) => {
router.post('/sig', bodyParser, async (req, res) => { router.post('/sig', bodyParser, async (req, res) => {
const data = await generateSignatureProfile(req.body.signature) const data = await generateSignatureProfile(req.body.signature)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
isSignature: true, isSignature: true,
signature: req.body.signature, signature: req.body.signature,
enable_message_encryption: false, enable_message_encryption: false,
@ -55,10 +56,10 @@ router.post('/sig', bodyParser, async (req, res) => {
router.get('/wkd/:id', async (req, res) => { router.get('/wkd/:id', async (req, res) => {
const data = await generateWKDProfile(req.params.id) const data = await generateWKDProfile(req.params.id)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })
@ -67,10 +68,10 @@ router.get('/wkd/:id', async (req, res) => {
router.get('/hkp/:id', async (req, res) => { router.get('/hkp/:id', async (req, res) => {
const data = await generateHKPProfile(req.params.id) const data = await generateHKPProfile(req.params.id)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })
@ -79,10 +80,10 @@ router.get('/hkp/:id', async (req, res) => {
router.get('/hkp/:server/:id', async (req, res) => { router.get('/hkp/:server/:id', async (req, res) => {
const data = await generateHKPProfile(req.params.id, req.params.server) const data = await generateHKPProfile(req.params.id, req.params.server)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: 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) => { router.get('/keybase/:username/:fingerprint', async (req, res) => {
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint) const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })
@ -103,10 +104,10 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => {
router.get('/:id', async (req, res) => { router.get('/:id', async (req, res) => {
const data = await generateAutoProfile(req.params.id) const data = await generateAutoProfile(req.params.id)
const title = utils.generatePageTitle('profile', data) const title = utils.generatePageTitle('profile', data)
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr) res.set('ariadne-identity-proof', data.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })

376
src/schemas.js Normal file
View 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
}

View file

@ -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
}
}
}

View file

@ -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 logger from '../log.js'
import * as doipjs from 'doipjs' 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' 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) => { const generateWKDProfile = async (id) => {
logger.debug('Generating a WKD profile', logger.debug('Generating a WKD profile',
{ component: 'wkd_profile_generator', action: 'start', profile_id: id }) { component: 'wkd_profile_generator', action: 'start', profile_id: id })
return fetchWKD(id) return fetchWKD(id)
.then(async key => { .then(async profile => {
let keyData = await doipjs.keys.process(key.publicKey) profile.addVerifier('keyoxide', `https://${process.env.DOMAIN}/wkd/${id}`)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}` profile = processOpenPgpProfile(profile)
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}`
logger.debug('Generating a WKD profile', logger.debug('Generating a WKD profile',
{ component: 'wkd_profile_generator', action: 'done', profile_id: id }) { component: 'wkd_profile_generator', action: 'done', profile_id: id })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate WKD profile', logger.warn('Failed to generate WKD profile',
{ component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id }) { component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
@ -78,41 +81,27 @@ const generateHKPProfile = async (id, keyserverDomain) => {
{ component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' }) { component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
return fetchHKP(id, keyserverDomain) return fetchHKP(id, keyserverDomain)
.then(async key => { .then(async profile => {
let keyData = await doipjs.keys.process(key.publicKey) let keyoxideUrl
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
keyData.key.fetchMethod = 'hkp'
keyData.key.uri = key.fetchURL
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') { if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${id}` keyoxideUrl = `https://${process.env.DOMAIN}/hkp/${id}`
} else { } 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', logger.debug('Generating a HKP profile',
{ component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' }) { component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate HKP profile', logger.warn('Failed to generate HKP profile',
{ component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' }) { component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
@ -121,25 +110,31 @@ const generateHKPProfile = async (id, keyserverDomain) => {
const generateAutoProfile = async (id) => { const generateAutoProfile = async (id) => {
let result let result
const aspeRe = /aspe:(.*):(.*)/
if (aspeRe.test(id)) {
result = await generateAspeProfile(id)
if (result && !('errors' in result)) {
return result
}
}
if (id.includes('@')) { if (id.includes('@')) {
result = await generateWKDProfile(id) result = await generateWKDProfile(id)
if (result && result.errors.length === 0) { if (result && !('errors' in result)) {
return result return result
} }
} }
result = await generateHKPProfile(id) result = await generateHKPProfile(id)
if (result && result.errors.length === 0) { if (result && !('errors' in result)) {
return result return result
} }
return { return {
key: {}, errors: ['No public profile/keys could be found']
keyData: {},
keyoxide: {},
extra: {},
errors: ['No public keys could be found']
} }
} }
@ -149,34 +144,20 @@ const generateSignatureProfile = async (signature) => {
return fetchSignature(signature) return fetchSignature(signature)
.then(async key => { .then(async key => {
let keyData = key.keyData let profile = await doipjs.signatures.parse(key.publicKey)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}` profile.addVerifier('keyoxide', keyoxideUrl)
key.keyData = undefined profile = processOpenPgpProfile(profile)
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
logger.debug('Generating a signature profile', logger.debug('Generating a signature profile',
{ component: 'signature_profile_generator', action: 'done' }) { component: 'signature_profile_generator', action: 'done' })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate a signature profile', logger.warn('Failed to generate a signature profile',
{ component: 'signature_profile_generator', action: 'failure', error: err.message }) { component: 'signature_profile_generator', action: 'failure', error: err.message })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
@ -187,72 +168,91 @@ const generateKeybaseProfile = async (username, fingerprint) => {
{ component: 'keybase_profile_generator', action: 'start', username, fingerprint }) { component: 'keybase_profile_generator', action: 'start', username, fingerprint })
return fetchKeybase(username, fingerprint) return fetchKeybase(username, fingerprint)
.then(async key => { .then(async profile => {
let keyData = await doipjs.keys.process(key.publicKey) profile.addVerifier('keyoxide', `https://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}` profile = processOpenPgpProfile(profile)
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}`
logger.debug('Generating a Keybase profile', logger.debug('Generating a Keybase profile',
{ component: 'keybase_profile_generator', action: 'done', username, fingerprint }) { component: 'keybase_profile_generator', action: 'done', username, fingerprint })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate a Keybase profile', logger.warn('Failed to generate a Keybase profile',
{ component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint }) { component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
} }
const processKeyData = (keyData) => { const processAspProfile = async (/** @type {import('doipjs').Profile */ profile) => {
keyData.users.forEach(user => { profile.personas.forEach(persona => {
// Remove faulty claims // Remove faulty claims
user.claims = user.claims.filter(claim => { persona.claims = persona.claims.filter(claim => {
return claim instanceof doipjs.Claim return claim instanceof doipjs.Claim
}) })
// Match claims // Match claims
user.claims.forEach(claim => { persona.claims.forEach(claim => {
claim.match() claim.match()
}) })
// Sort claims // Sort claims
user.claims.sort((a, b) => { persona.claims.sort((a, b) => {
if (a.matches.length === 0) return 1 if (a.matches.length === 0) return 1
if (b.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 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 1
} }
return 0 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) => { const computeExtraData = async (key, keyData) => {
@ -265,6 +265,7 @@ const computeExtraData = async (key, keyData) => {
} }
} }
export { generateAspeProfile }
export { generateWKDProfile } export { generateWKDProfile }
export { generateHKPProfile } export { generateHKPProfile }
export { generateAutoProfile } export { generateAutoProfile }

View file

@ -39,10 +39,9 @@ const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
const fetchWKD = (id) => { const fetchWKD = (id) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let publicKey = null
publicKey: null, let profile = null
fetchURL: null let fetchURL = null
}
if (!id.includes('@')) { if (!id.includes('@')) {
reject(new Error(`The WKD identifier "${id}" is invalid`)) reject(new Error(`The WKD identifier "${id}" is invalid`))
@ -59,14 +58,14 @@ const fetchWKD = (id) => {
const hash = createHash('md5').update(id).digest('hex') const hash = createHash('md5').update(id).digest('hex')
if (c && await c.get(hash)) { if (c && await c.get(hash)) {
plaintext = Uint8Array.from((await c.get(hash)).split(',')) profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
} }
if (!plaintext) { if (!profile) {
try { try {
plaintext = await got(urlAdvanced).then((response) => { plaintext = await got(urlAdvanced).then((response) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
output.fetchURL = urlAdvanced fetchURL = urlAdvanced
return new Uint8Array(response.rawBody) return new Uint8Array(response.rawBody)
} else { } else {
return null return null
@ -76,7 +75,7 @@ const fetchWKD = (id) => {
try { try {
plaintext = await got(urlDirect).then((response) => { plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
output.fetchURL = urlDirect fetchURL = urlDirect
return new Uint8Array(response.rawBody) return new Uint8Array(response.rawBody)
} else { } else {
return null return null
@ -91,24 +90,29 @@ const fetchWKD = (id) => {
reject(new Error('No public keys could be fetched using WKD')) 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 { try {
output.publicKey = await readKey({ publicKey = await readKey({
binaryKey: plaintext binaryKey: plaintext
}) })
} catch (error) { } catch (error) {
reject(new Error('No public keys could be read from the data fetched using WKD')) reject(new Error('No public keys could be read from the data fetched using WKD'))
} }
if (!output.publicKey) { if (!publicKey) {
reject(new Error('No public keys could be read from the data fetched using WKD')) reject(new Error('No public keys could be read from the data fetched using WKD'))
} }
resolve(output) profile = await doipjs.openpgp.parsePublicKey(publicKey)
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)
}
resolve(profile)
})() })()
}) })
} }
@ -116,10 +120,8 @@ const fetchWKD = (id) => {
const fetchHKP = (id, keyserverDomain) => { const fetchHKP = (id, keyserverDomain) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let profile = null
publicKey: null, let fetchURL = null
fetchURL: null
}
const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org' const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org'
@ -135,31 +137,36 @@ const fetchHKP = (id, keyserverDomain) => {
query = `0x${sanitizedId}` 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') const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
if (c && await c.get(hash)) { if (c && await c.get(hash)) {
output.publicKey = await readKey({ profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
armoredKey: await c.get(hash) }
})
} else { if (!profile) {
try { try {
output.publicKey = await doipjs.keys.fetchHKP(query, keyserverDomainNormalized) profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
} catch (error) { } catch (error) {
profile = null
}
}
if (!profile) {
reject(new Error('No public keys could be fetched using HKP')) reject(new Error('No public keys could be fetched using HKP'))
} return
} }
if (!output.publicKey) { profile.publicKey.fetch.method = 'hkp'
reject(new Error('No public keys could be fetched using 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)
} }
if (c && output.publicKey instanceof PublicKey) { resolve(profile)
await c.set(hash, output.publicKey.armor(), 60 * 1000)
}
resolve(output)
})() })()
}) })
} }
@ -167,11 +174,7 @@ const fetchHKP = (id, keyserverDomain) => {
const fetchSignature = (signature) => { const fetchSignature = (signature) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let profile = null
publicKey: null,
fetchURL: null,
keyData: null
}
// Check validity of signature // Check validity of signature
let signatureData let signatureData
@ -185,30 +188,18 @@ const fetchSignature = (signature) => {
// Process the signature // Process the signature
try { try {
output.keyData = await doipjs.signatures.process(signature) profile = await doipjs.signatures.parse(signature)
output.publicKey = output.keyData.key.data
// TODO Find the URL to the key // TODO Find the URL to the key
output.fetchURL = null
} catch (error) { } catch (error) {
reject(new Error(`Signature could not be properly read (${error.message})`)) reject(new Error(`Signature could not be properly read (${error.message})`))
} }
// Check if a key was fetched // Check if a key was fetched
if (!output.publicKey) { if (!profile) {
reject(new Error('No public keys could be fetched')) reject(new Error('No profile could be fetched'))
} }
// Check validity of signature resolve(profile)
const verified = await verify({
message: signatureData,
verificationKeys: output.publicKey
})
if (!await verified.signatures[0].verified) {
reject(new Error('Signature was invalid'))
}
resolve(output)
})() })()
}) })
} }
@ -216,23 +207,24 @@ const fetchSignature = (signature) => {
const fetchKeybase = (username, fingerprint) => { const fetchKeybase = (username, fingerprint) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let profile = null
publicKey: null, let fetchURL = null
fetchURL: null
}
try { try {
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint) profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}` fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
} catch (error) { } catch (error) {
reject(new Error('No public keys could be fetched from Keybase')) 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')) 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)
})() })()
}) })
} }

View file

@ -39,7 +39,7 @@ export function generatePageTitle (type, data) {
switch (type) { switch (type) {
case 'profile': case 'profile':
try { try {
return `${data.keyData.users[data.keyData.primaryUserIndex].userData.name} - Keyoxide` return `${data.personas[data.primaryPersonaIndex].name} - Keyoxide`
} catch (error) { } catch (error) {
return 'Profile - Keyoxide' return 'Profile - Keyoxide'
} }