keyoxide-web/api/v2/keyoxide_profile.js

391 lines
12 KiB
JavaScript
Raw Normal View History

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