2021-10-14 03:18:15 -06:00
|
|
|
/*
|
|
|
|
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/>.
|
|
|
|
*/
|
|
|
|
const router = require('express').Router()
|
|
|
|
const { check, validationResult } = require('express-validator')
|
2021-10-15 14:23:29 -06:00
|
|
|
const Ajv = require("ajv")
|
|
|
|
const ajv = new Ajv({coerceTypes: true})
|
2021-10-14 03:18:15 -06:00
|
|
|
const kx = require('../../server')
|
|
|
|
|
2021-10-15 14:23:29 -06:00
|
|
|
const apiProfileSchema = {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
keyData: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
fingerprint: {
|
|
|
|
type: "string"
|
|
|
|
},
|
2021-10-17 01:08:56 -06:00
|
|
|
openpgp4fpr: {
|
|
|
|
type: "string"
|
|
|
|
},
|
2021-10-15 14:23:29 -06:00
|
|
|
users: {
|
|
|
|
type: "array",
|
|
|
|
items: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
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",
|
|
|
|
properties: {
|
|
|
|
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",
|
|
|
|
properties: {
|
|
|
|
fetcher: { type: "string" },
|
|
|
|
access: { type: "string" },
|
|
|
|
format: { type: "string" },
|
|
|
|
data: { type: "object" },
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
claim: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
format: { type: "string" },
|
|
|
|
relation: { type: "string" },
|
|
|
|
path: {
|
|
|
|
type: "array",
|
|
|
|
items: {
|
|
|
|
type: "string"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
verification: {
|
|
|
|
type: "object"
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
primaryUserIndex: {
|
|
|
|
type: "integer"
|
|
|
|
},
|
|
|
|
key: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
data: { type: "object" },
|
|
|
|
fetchMethod: { type: "string" },
|
|
|
|
uri: { type: "string" },
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
extra: {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
avatarURL: { type: "string" },
|
|
|
|
}
|
|
|
|
},
|
|
|
|
errors: {
|
|
|
|
type: "array"
|
|
|
|
},
|
|
|
|
},
|
|
|
|
required: ["keyData", "extra", "errors"],
|
|
|
|
additionalProperties: false
|
|
|
|
}
|
|
|
|
|
|
|
|
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
|
|
|
|
2021-10-14 03:18:15 -06:00
|
|
|
const doVerification = async (data) => {
|
|
|
|
let promises = []
|
|
|
|
let results = []
|
|
|
|
|
|
|
|
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
2021-10-15 14:23:29 -06:00
|
|
|
const user = data.keyData.users[iUser]
|
2021-10-14 03:18:15 -06:00
|
|
|
|
|
|
|
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
2021-10-15 14:23:29 -06:00
|
|
|
const claim = user.claims[iClaim]
|
2021-10-14 03:18:15 -06:00
|
|
|
|
|
|
|
promises.push(
|
|
|
|
new Promise(async (resolve, reject) => {
|
|
|
|
await claim.verify()
|
|
|
|
results.push([iUser, iClaim, claim])
|
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
await Promise.all(promises)
|
|
|
|
|
|
|
|
results.forEach(result => {
|
|
|
|
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
|
|
|
})
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2021-10-15 14:23:29 -06:00
|
|
|
const sanitize = (data) => {
|
|
|
|
let results = []
|
|
|
|
|
|
|
|
const dataClone = JSON.parse(JSON.stringify(data))
|
2021-10-19 07:45:22 -06:00
|
|
|
|
2021-10-15 14:23:29 -06:00
|
|
|
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
|
|
|
|
if (!claim.verification) {
|
|
|
|
claim.verification = {}
|
|
|
|
}
|
2021-10-16 00:00:51 -06:00
|
|
|
// 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]
|
|
|
|
})
|
2021-10-15 14:23:29 -06:00
|
|
|
|
|
|
|
data.keyData.users[iUser].claims[iClaim] = claim
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const valid = apiProfileValidate(data)
|
|
|
|
if (!valid) {
|
|
|
|
throw new Error(`Profile data sanitization error`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2021-10-14 03:18:15 -06:00
|
|
|
router.get('/profile/fetch',
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate profile
|
|
|
|
let data
|
|
|
|
switch (req.query.protocol) {
|
|
|
|
case 'wkd':
|
|
|
|
data = await kx.generateWKDProfile(req.query.query)
|
|
|
|
break;
|
|
|
|
case 'hkp':
|
|
|
|
data = await kx.generateHKPProfile(req.query.query)
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (req.query.query.includes('@')) {
|
|
|
|
data = await kx.generateWKDProfile(req.query.query)
|
|
|
|
} else {
|
|
|
|
data = await kx.generateHKPProfile(req.query.query)
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2021-10-19 07:45:22 -06:00
|
|
|
if (data.errors.length > 0) {
|
|
|
|
delete data.key
|
|
|
|
res.status(500).send(data)
|
|
|
|
}
|
|
|
|
|
2021-10-14 03:18:15 -06:00
|
|
|
// Return public key
|
|
|
|
if (req.query.returnPublicKey) {
|
|
|
|
data.keyData.key.data = data.key.publicKey
|
|
|
|
}
|
|
|
|
delete data.key
|
|
|
|
|
|
|
|
// Do verification
|
|
|
|
if (req.query.doVerification) {
|
|
|
|
data = await doVerification(data)
|
|
|
|
}
|
|
|
|
|
2021-10-15 14:23:29 -06:00
|
|
|
try {
|
|
|
|
// Sanitize JSON
|
|
|
|
data = sanitize(data);
|
|
|
|
} catch (error) {
|
|
|
|
data.keyData = {}
|
|
|
|
data.extra = {}
|
|
|
|
data.errors = [error.message]
|
|
|
|
}
|
|
|
|
|
|
|
|
let statusCode = 200
|
|
|
|
if (data.errors.length > 0) {
|
|
|
|
statusCode = 500
|
|
|
|
}
|
|
|
|
|
2021-10-15 23:52:22 -06:00
|
|
|
res.status(statusCode).send(data)
|
2021-10-14 03:18:15 -06:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
router.get('/profile/verify',
|
|
|
|
check('data').exists().isJSON(),
|
|
|
|
async (req, res) => {
|
|
|
|
const valRes = validationResult(req);
|
|
|
|
if (!valRes.isEmpty()) {
|
|
|
|
res.status(400).send(valRes)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Do verification
|
|
|
|
data = await doVerification(req.query.data)
|
|
|
|
|
2021-10-15 14:23:29 -06:00
|
|
|
try {
|
|
|
|
// Sanitize JSON
|
|
|
|
data = sanitize(data);
|
|
|
|
} catch (error) {
|
|
|
|
data.keyData = {}
|
|
|
|
data.extra = {}
|
|
|
|
data.errors = [error.message]
|
|
|
|
}
|
|
|
|
|
|
|
|
let statusCode = 200
|
|
|
|
if (data.errors.length > 0) {
|
|
|
|
statusCode = 500
|
|
|
|
}
|
|
|
|
|
2021-10-15 23:52:22 -06:00
|
|
|
res.status(statusCode).send(data)
|
2021-10-14 03:18:15 -06:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
module.exports = router
|