diff --git a/src/routes/api.js b/src/routes/api.js
index 969a1aa..8e15298 100644
--- a/src/routes/api.js
+++ b/src/routes/api.js
@@ -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 .
*/
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
diff --git a/src/routes/main.js b/src/routes/main.js
index ed1aa49..34fb518 100644
--- a/src/routes/main.js
+++ b/src/routes/main.js
@@ -30,7 +30,6 @@ more information on this, and how to apply and follow the GNU AGPL, see {
}
}
- res.render('index', { highlights, demoData })
+ res.render('index', { highlights })
})
router.get('/privacy', (req, res) => {
diff --git a/src/routes/profile.js b/src/routes/profile.js
index c7040d4..78149f5 100644
--- a/src/routes/profile.js
+++ b/src/routes/profile.js
@@ -30,6 +30,7 @@ more information on this, and how to apply and follow the GNU AGPL, see {
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
})
diff --git a/src/schemas.js b/src/schemas.js
new file mode 100644
index 0000000..9ac16ae
--- /dev/null
+++ b/src/schemas.js
@@ -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 .
+
+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 .
+*/
+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
+}
diff --git a/src/server/demo.js b/src/server/demo.js
deleted file mode 100644
index e03d05d..0000000
--- a/src/server/demo.js
+++ /dev/null
@@ -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 .
-
-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 .
-*/
-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
- }
- }
-}
diff --git a/src/server/index.js b/src/server/index.js
index 06ad467..176425d 100644
--- a/src/server/index.js
+++ b/src/server/index.js
@@ -29,45 +29,48 @@ more information on this, and how to apply and follow the GNU AGPL, see {
+ 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 }
diff --git a/src/server/keys.js b/src/server/openpgpProfiles.js
similarity index 69%
rename from src/server/keys.js
rename to src/server/openpgpProfiles.js
index 5b61aff..8bec8fd 100644
--- a/src/server/keys.js
+++ b/src/server/openpgpProfiles.js
@@ -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)
})()
})
}
diff --git a/src/server/utils.js b/src/server/utils.js
index cd83172..30884e2 100644
--- a/src/server/utils.js
+++ b/src/server/utils.js
@@ -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'
}