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