forked from Mirrors/keyoxide-web
Enforce JS code style
This commit is contained in:
parent
da57f4c57d
commit
f6df547951
12 changed files with 2062 additions and 1152 deletions
612
api/v0/index.js
612
api/v0/index.js
|
@ -34,357 +34,357 @@ import { generateWKDProfile, generateHKPProfile } from '../../server/index.js'
|
||||||
import 'dotenv/config.js'
|
import 'dotenv/config.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const ajv = new Ajv({coerceTypes: true})
|
const ajv = new Ajv({ coerceTypes: true })
|
||||||
|
|
||||||
const apiProfileSchema = {
|
const apiProfileSchema = {
|
||||||
type: "object",
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
keyData: {
|
keyData: {
|
||||||
type: "object",
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fingerprint: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
openpgp4fpr: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
fingerprint: {
|
userData: {
|
||||||
type: "string"
|
type: 'object',
|
||||||
},
|
properties: {
|
||||||
openpgp4fpr: {
|
id: { type: 'string' },
|
||||||
type: "string"
|
name: { type: 'string' },
|
||||||
},
|
email: { type: 'string' },
|
||||||
users: {
|
comment: { type: 'string' },
|
||||||
type: "array",
|
isPrimary: { type: 'boolean' },
|
||||||
items: {
|
isRevoked: { type: 'boolean' }
|
||||||
type: "object",
|
}
|
||||||
|
},
|
||||||
|
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: {
|
properties: {
|
||||||
userData: {
|
serviceProvider: {
|
||||||
type: "object",
|
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: {
|
properties: {
|
||||||
id: { type: "string" },
|
fetcher: { type: 'string' },
|
||||||
name: { type: "string" },
|
access: { type: 'string' },
|
||||||
email: { type: "string" },
|
format: { type: 'string' },
|
||||||
comment: { type: "string" },
|
data: { type: 'object' }
|
||||||
isPrimary: { type: "boolean" },
|
|
||||||
isRevoked: { type: "boolean" },
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
claims: {
|
}
|
||||||
type: "array",
|
},
|
||||||
|
claim: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
format: { type: 'string' },
|
||||||
|
relation: { type: 'string' },
|
||||||
|
path: {
|
||||||
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: "object",
|
type: 'string'
|
||||||
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"
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
profileName: { type: "string" },
|
|
||||||
profileURL: { type: "string" },
|
|
||||||
serviceProviderName: { type: "string" },
|
|
||||||
isVerificationDone: { type: "boolean" },
|
|
||||||
isVerified: { type: "boolean" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
type: 'object'
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
profileName: { type: 'string' },
|
||||||
|
profileURL: { type: 'string' },
|
||||||
|
serviceProviderName: { type: 'string' },
|
||||||
|
isVerificationDone: { type: 'boolean' },
|
||||||
|
isVerified: { type: 'boolean' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
primaryUserIndex: {
|
}
|
||||||
type: "integer"
|
}
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
data: { type: "object" },
|
|
||||||
fetchMethod: { type: "string" },
|
|
||||||
uri: { type: "string" },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
keyoxide: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
url: { type: "string" },
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
extra: {
|
primaryUserIndex: {
|
||||||
type: "object",
|
type: 'integer'
|
||||||
properties: {
|
|
||||||
avatarURL: { type: "string" },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
type: "array"
|
|
||||||
},
|
},
|
||||||
|
key: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
data: { type: 'object' },
|
||||||
|
fetchMethod: { type: 'string' },
|
||||||
|
uri: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ["keyData", "keyoxide", "extra", "errors"],
|
keyoxide: {
|
||||||
additionalProperties: false
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
avatarURL: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
type: 'array'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
||||||
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
||||||
|
|
||||||
const doVerification = async (data) => {
|
const doVerification = async (data) => {
|
||||||
let promises = []
|
const promises = []
|
||||||
let results = []
|
const results = []
|
||||||
let verificationOptions = {
|
const verificationOptions = {
|
||||||
proxy: {
|
proxy: {
|
||||||
hostname: process.env.PROXY_HOSTNAME,
|
hostname: process.env.PROXY_HOSTNAME,
|
||||||
policy: (process.env.PROXY_HOSTNAME != "") ? 'adaptive' : 'never'
|
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||||
const user = data.keyData.users[iUser]
|
const user = data.keyData.users[iUser]
|
||||||
|
|
||||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||||
const claim = user.claims[iClaim]
|
const claim = user.claims[iClaim]
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
new Promise(async (resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
await claim.verify(verificationOptions)
|
(async () => {
|
||||||
results.push([iUser, iClaim, claim])
|
await claim.verify(verificationOptions)
|
||||||
resolve()
|
results.push([iUser, iClaim, claim])
|
||||||
})
|
resolve()
|
||||||
)
|
})()
|
||||||
}
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
results.forEach(result => {
|
|
||||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
results.forEach(result => {
|
||||||
|
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitize = (data) => {
|
const sanitize = (data) => {
|
||||||
let results = []
|
const dataClone = JSON.parse(JSON.stringify(data))
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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
|
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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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]
|
||||||
|
})
|
||||||
|
|
||||||
const valid = apiProfileValidate(data)
|
data.keyData.users[iUser].claims[iClaim] = claim
|
||||||
if (!valid) {
|
|
||||||
throw new Error(`Profile data sanitization error`)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
const valid = apiProfileValidate(data)
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error('Profile data sanitization error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSummaryToClaims = (data) => {
|
const addSummaryToClaims = (data) => {
|
||||||
// To be removed when data is added by DOIP library
|
// To be removed when data is added by DOIP library
|
||||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
||||||
const user = data.keyData.users[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"
|
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
||||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
const claim = user.claims[claimIndex]
|
||||||
const isAmbiguous = isVerified
|
|
||||||
? false
|
const isVerificationDone = claim.status === 'verified'
|
||||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
const isVerified = isVerificationDone ? claim.verification.result : false
|
||||||
|
const isAmbiguous = isVerified
|
||||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
? false
|
||||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
||||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : "",
|
|
||||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : "",
|
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
||||||
isVerificationDone: isVerificationDone,
|
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
||||||
isVerified: isVerified,
|
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
||||||
}
|
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
||||||
}
|
isVerificationDone,
|
||||||
|
isVerified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/profile/fetch',
|
router.get('/profile/fetch',
|
||||||
check('query').exists(),
|
check('query').exists(),
|
||||||
check('protocol').optional().toLowerCase().isIn(["hkp", "wkd"]),
|
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
|
||||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
check('doVerification').default(false).isBoolean().toBoolean(),
|
||||||
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const valRes = validationResult(req);
|
const valRes = validationResult(req)
|
||||||
if (!valRes.isEmpty()) {
|
if (!valRes.isEmpty()) {
|
||||||
res.status(400).send(valRes)
|
res.status(400).send(valRes)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
delete data.key
|
|
||||||
res.status(500).send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data)
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
delete data.key
|
||||||
|
res.status(500).send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sanitize JSON
|
||||||
|
data = sanitize(data)
|
||||||
|
} catch (error) {
|
||||||
|
data.keyData = {}
|
||||||
|
data.extra = {}
|
||||||
|
data.errors = [error.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add missing data
|
||||||
|
data = addSummaryToClaims(data)
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(data)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
router.get('/profile/verify',
|
router.get('/profile/verify',
|
||||||
check('data').exists().isJSON(),
|
check('data').exists().isJSON(),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const valRes = validationResult(req)
|
const valRes = validationResult(req)
|
||||||
if (!valRes.isEmpty()) {
|
if (!valRes.isEmpty()) {
|
||||||
res.status(400).send(valRes)
|
res.status(400).send(valRes)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// Do verification
|
|
||||||
data = await doVerification(req.query.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data);
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do verification
|
||||||
|
let data = await doVerification(req.query.data)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sanitize JSON
|
||||||
|
data = sanitize(data)
|
||||||
|
} catch (error) {
|
||||||
|
data.keyData = {}
|
||||||
|
data.extra = {}
|
||||||
|
data.errors = [error.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add missing data
|
||||||
|
data = addSummaryToClaims(data)
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(data)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -34,357 +34,357 @@ import { generateWKDProfile, generateHKPProfile } from '../../server/index.js'
|
||||||
import 'dotenv/config.js'
|
import 'dotenv/config.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const ajv = new Ajv({coerceTypes: true})
|
const ajv = new Ajv({ coerceTypes: true })
|
||||||
|
|
||||||
const apiProfileSchema = {
|
const apiProfileSchema = {
|
||||||
type: "object",
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
keyData: {
|
keyData: {
|
||||||
type: "object",
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
fingerprint: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
openpgp4fpr: {
|
||||||
|
type: 'string'
|
||||||
|
},
|
||||||
|
users: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
fingerprint: {
|
userData: {
|
||||||
type: "string"
|
type: 'object',
|
||||||
},
|
properties: {
|
||||||
openpgp4fpr: {
|
id: { type: 'string' },
|
||||||
type: "string"
|
name: { type: 'string' },
|
||||||
},
|
email: { type: 'string' },
|
||||||
users: {
|
comment: { type: 'string' },
|
||||||
type: "array",
|
isPrimary: { type: 'boolean' },
|
||||||
items: {
|
isRevoked: { type: 'boolean' }
|
||||||
type: "object",
|
}
|
||||||
|
},
|
||||||
|
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: {
|
properties: {
|
||||||
userData: {
|
serviceProvider: {
|
||||||
type: "object",
|
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: {
|
properties: {
|
||||||
id: { type: "string" },
|
fetcher: { type: 'string' },
|
||||||
name: { type: "string" },
|
access: { type: 'string' },
|
||||||
email: { type: "string" },
|
format: { type: 'string' },
|
||||||
comment: { type: "string" },
|
data: { type: 'object' }
|
||||||
isPrimary: { type: "boolean" },
|
|
||||||
isRevoked: { type: "boolean" },
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
claims: {
|
}
|
||||||
type: "array",
|
},
|
||||||
|
claim: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
format: { type: 'string' },
|
||||||
|
relation: { type: 'string' },
|
||||||
|
path: {
|
||||||
|
type: 'array',
|
||||||
items: {
|
items: {
|
||||||
type: "object",
|
type: 'string'
|
||||||
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"
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
profileName: { type: "string" },
|
|
||||||
profileURL: { type: "string" },
|
|
||||||
serviceProviderName: { type: "string" },
|
|
||||||
isVerificationDone: { type: "boolean" },
|
|
||||||
isVerified: { type: "boolean" },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
type: 'object'
|
||||||
|
},
|
||||||
|
summary: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
profileName: { type: 'string' },
|
||||||
|
profileURL: { type: 'string' },
|
||||||
|
serviceProviderName: { type: 'string' },
|
||||||
|
isVerificationDone: { type: 'boolean' },
|
||||||
|
isVerified: { type: 'boolean' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
primaryUserIndex: {
|
}
|
||||||
type: "integer"
|
}
|
||||||
},
|
|
||||||
key: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
data: { type: "object" },
|
|
||||||
fetchMethod: { type: "string" },
|
|
||||||
uri: { type: "string" },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
keyoxide: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
url: { type: "string" },
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
extra: {
|
primaryUserIndex: {
|
||||||
type: "object",
|
type: 'integer'
|
||||||
properties: {
|
|
||||||
avatarURL: { type: "string" },
|
|
||||||
}
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
type: "array"
|
|
||||||
},
|
},
|
||||||
|
key: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
data: { type: 'object' },
|
||||||
|
fetchMethod: { type: 'string' },
|
||||||
|
uri: { type: 'string' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
required: ["keyData", "keyoxide", "extra", "errors"],
|
keyoxide: {
|
||||||
additionalProperties: false
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
url: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extra: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
avatarURL: { type: 'string' }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errors: {
|
||||||
|
type: 'array'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
||||||
|
additionalProperties: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
||||||
|
|
||||||
const doVerification = async (data) => {
|
const doVerification = async (data) => {
|
||||||
let promises = []
|
const promises = []
|
||||||
let results = []
|
const results = []
|
||||||
let verificationOptions = {
|
const verificationOptions = {
|
||||||
proxy: {
|
proxy: {
|
||||||
hostname: process.env.PROXY_HOSTNAME,
|
hostname: process.env.PROXY_HOSTNAME,
|
||||||
policy: (process.env.PROXY_HOSTNAME != "") ? 'adaptive' : 'never'
|
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||||
const user = data.keyData.users[iUser]
|
const user = data.keyData.users[iUser]
|
||||||
|
|
||||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||||
const claim = user.claims[iClaim]
|
const claim = user.claims[iClaim]
|
||||||
|
|
||||||
promises.push(
|
promises.push(
|
||||||
new Promise(async (resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
await claim.verify(verificationOptions)
|
(async () => {
|
||||||
results.push([iUser, iClaim, claim])
|
await claim.verify(verificationOptions)
|
||||||
resolve()
|
results.push([iUser, iClaim, claim])
|
||||||
})
|
resolve()
|
||||||
)
|
})()
|
||||||
}
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
await Promise.all(promises)
|
}
|
||||||
|
await Promise.all(promises)
|
||||||
results.forEach(result => {
|
|
||||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
results.forEach(result => {
|
||||||
|
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitize = (data) => {
|
const sanitize = (data) => {
|
||||||
let results = []
|
const dataClone = JSON.parse(JSON.stringify(data))
|
||||||
|
|
||||||
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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 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
|
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]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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]
|
||||||
|
})
|
||||||
|
|
||||||
const valid = apiProfileValidate(data)
|
data.keyData.users[iUser].claims[iClaim] = claim
|
||||||
if (!valid) {
|
|
||||||
throw new Error(`Profile data sanitization error`)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
const valid = apiProfileValidate(data)
|
||||||
|
if (!valid) {
|
||||||
|
throw new Error('Profile data sanitization error')
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const addSummaryToClaims = (data) => {
|
const addSummaryToClaims = (data) => {
|
||||||
// To be removed when data is added by DOIP library
|
// To be removed when data is added by DOIP library
|
||||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
||||||
const user = data.keyData.users[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"
|
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
||||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
const claim = user.claims[claimIndex]
|
||||||
const isAmbiguous = isVerified
|
|
||||||
? false
|
const isVerificationDone = claim.status === 'verified'
|
||||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
const isVerified = isVerificationDone ? claim.verification.result : false
|
||||||
|
const isAmbiguous = isVerified
|
||||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
? false
|
||||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
||||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : "",
|
|
||||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : "",
|
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
||||||
isVerificationDone: isVerificationDone,
|
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
||||||
isVerified: isVerified,
|
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
||||||
}
|
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
||||||
}
|
isVerificationDone,
|
||||||
|
isVerified
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
router.get('/fetch',
|
router.get('/fetch',
|
||||||
check('query').exists(),
|
check('query').exists(),
|
||||||
check('protocol').optional().toLowerCase().isIn(["hkp", "wkd"]),
|
check('protocol').optional().toLowerCase().isIn(['hkp', 'wkd']),
|
||||||
check('doVerification').default(false).isBoolean().toBoolean(),
|
check('doVerification').default(false).isBoolean().toBoolean(),
|
||||||
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
check('returnPublicKey').default(false).isBoolean().toBoolean(),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const valRes = validationResult(req);
|
const valRes = validationResult(req)
|
||||||
if (!valRes.isEmpty()) {
|
if (!valRes.isEmpty()) {
|
||||||
res.status(400).send(valRes)
|
res.status(400).send(valRes)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
delete data.key
|
|
||||||
res.status(500).send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data)
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
delete data.key
|
||||||
|
res.status(500).send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sanitize JSON
|
||||||
|
data = sanitize(data)
|
||||||
|
} catch (error) {
|
||||||
|
data.keyData = {}
|
||||||
|
data.extra = {}
|
||||||
|
data.errors = [error.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add missing data
|
||||||
|
data = addSummaryToClaims(data)
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(data)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
router.get('/verify',
|
router.get('/verify',
|
||||||
check('data').exists().isJSON(),
|
check('data').exists().isJSON(),
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
const valRes = validationResult(req)
|
const valRes = validationResult(req)
|
||||||
if (!valRes.isEmpty()) {
|
if (!valRes.isEmpty()) {
|
||||||
res.status(400).send(valRes)
|
res.status(400).send(valRes)
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// Do verification
|
|
||||||
data = await doVerification(req.query.data)
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Sanitize JSON
|
|
||||||
data = sanitize(data);
|
|
||||||
} catch (error) {
|
|
||||||
data.keyData = {}
|
|
||||||
data.extra = {}
|
|
||||||
data.errors = [error.message]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add missing data
|
|
||||||
data = addSummaryToClaims(data)
|
|
||||||
|
|
||||||
let statusCode = 200
|
|
||||||
if (data.errors.length > 0) {
|
|
||||||
statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(statusCode).send(data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do verification
|
||||||
|
let data = await doVerification(req.query.data)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Sanitize JSON
|
||||||
|
data = sanitize(data)
|
||||||
|
} catch (error) {
|
||||||
|
data.keyData = {}
|
||||||
|
data.extra = {}
|
||||||
|
data.errors = [error.message]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add missing data
|
||||||
|
data = addSummaryToClaims(data)
|
||||||
|
|
||||||
|
let statusCode = 200
|
||||||
|
if (data.errors.length > 0) {
|
||||||
|
statusCode = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(statusCode).send(data)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
"mini-css-extract-plugin": "^2.5.3",
|
"mini-css-extract-plugin": "^2.5.3",
|
||||||
"mocha": "^9.2.1",
|
"mocha": "^9.2.1",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
|
"standard": "^17.0.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"webpack": "^5.69.1",
|
"webpack": "^5.69.1",
|
||||||
"webpack-bundle-analyzer": "^4.5.0",
|
"webpack-bundle-analyzer": "^4.5.0",
|
||||||
|
@ -42,12 +43,14 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node --experimental-fetch ./",
|
"start": "node --experimental-fetch ./",
|
||||||
"dev": "yarn run watch & yarn run build:static:dev",
|
"dev": "yarn run watch & yarn run build:static:dev",
|
||||||
"test": "mocha",
|
"test": "yarn run standard:check && mocha",
|
||||||
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
|
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
|
||||||
"build": "yarn run build:server & yarn run build:static",
|
"build": "yarn run build:server & yarn run build:static",
|
||||||
"build:server": "ncc build ./src/index.js -e jsdom -o dist",
|
"build:server": "ncc build ./src/index.js -e jsdom -o dist",
|
||||||
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production",
|
"build:static": "webpack --config webpack.config.js --env static=true --env mode=production",
|
||||||
"build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
|
"build:static:dev": "webpack --config webpack.config.js --env static=true --env mode=development",
|
||||||
|
"standard:check": "./node_modules/.bin/standard ./src ./api ./routes ./server",
|
||||||
|
"standard:fix": "./node_modules/.bin/standard --fix ./src ./api ./routes ./server",
|
||||||
"license:check": "./node_modules/.bin/license-check-and-add check",
|
"license:check": "./node_modules/.bin/license-check-and-add check",
|
||||||
"license:add": "./node_modules/.bin/license-check-and-add add",
|
"license:add": "./node_modules/.bin/license-check-and-add add",
|
||||||
"license:remove": "./node_modules/.bin/license-check-and-add remove"
|
"license:remove": "./node_modules/.bin/license-check-and-add remove"
|
||||||
|
|
100
routes/main.js
100
routes/main.js
|
@ -33,70 +33,70 @@ import { readFileSync } from 'fs'
|
||||||
import demoData from '../server/demo.js'
|
import demoData from '../server/demo.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
const md = markdownImport({typographer: true})
|
const md = markdownImport({ typographer: true })
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
let highlights = []
|
const highlights = []
|
||||||
for (let index = 1; index < 4; index++) {
|
for (let index = 1; index < 4; index++) {
|
||||||
if (process.env[`KX_HIGHLIGHTS_${index}_NAME`]
|
if (process.env[`KX_HIGHLIGHTS_${index}_NAME`] &&
|
||||||
&& process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) {
|
process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]) {
|
||||||
highlights.push({
|
highlights.push({
|
||||||
name: process.env[`KX_HIGHLIGHTS_${index}_NAME`],
|
name: process.env[`KX_HIGHLIGHTS_${index}_NAME`],
|
||||||
description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`],
|
description: process.env[`KX_HIGHLIGHTS_${index}_DESCRIPTION`],
|
||||||
fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`],
|
fingerprint: process.env[`KX_HIGHLIGHTS_${index}_FINGERPRINT`]
|
||||||
})
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.render('index', { highlights: highlights, demoData: demoData })
|
res.render('index', { highlights, demoData })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/privacy', (req, res) => {
|
router.get('/privacy', (req, res) => {
|
||||||
let rawContent = readFileSync(`./content/privacy-policy.md`, "utf8")
|
const rawContent = readFileSync('./content/privacy-policy.md', 'utf8')
|
||||||
const content = md.render(rawContent)
|
const content = md.render(rawContent)
|
||||||
res.render(`article`, { title: `Privacy policy`, content: content })
|
res.render('article', { title: 'Privacy policy', content })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/.well-known/webfinger', (req, res) => {
|
router.get('/.well-known/webfinger', (req, res) => {
|
||||||
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
||||||
res.status(404).send('<body><pre>Cannot GET /.well-known/webfinger</pre></body>')
|
res.status(404).send('<body><pre>Cannot GET /.well-known/webfinger</pre></body>')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = {
|
const body = {
|
||||||
'subject': `acct:keyoxide@${process.env.DOMAIN}`,
|
subject: `acct:keyoxide@${process.env.DOMAIN}`,
|
||||||
'aliases': [`https://${process.env.DOMAIN}/users/keyoxide`],
|
aliases: [`https://${process.env.DOMAIN}/users/keyoxide`],
|
||||||
'links': [{
|
links: [{
|
||||||
'rel': 'self',
|
rel: 'self',
|
||||||
'type': 'application/activity+json',
|
type: 'application/activity+json',
|
||||||
'href': `https://${process.env.DOMAIN}/users/keyoxide`
|
href: `https://${process.env.DOMAIN}/users/keyoxide`
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
res.json(body)
|
res.json(body)
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/users/keyoxide', (req, res) => {
|
router.get('/users/keyoxide', (req, res) => {
|
||||||
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
if (!(process.env.DOMAIN && process.env.ACTIVITYPUB_PUBLIC_KEY)) {
|
||||||
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')
|
res.status(404).send('<body><pre>Cannot GET /keyoxide</pre></body>')
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1'
|
||||||
|
],
|
||||||
|
id: `https://${process.env.DOMAIN}/users/keyoxide`,
|
||||||
|
type: 'Application',
|
||||||
|
inbox: `https://${process.env.DOMAIN}/users/keyoxide/inbox`,
|
||||||
|
preferredUsername: `${process.env.DOMAIN}`,
|
||||||
|
publicKey: {
|
||||||
|
id: `https://${process.env.DOMAIN}/users/keyoxide#main-key`,
|
||||||
|
owner: `https://${process.env.DOMAIN}/users/keyoxide`,
|
||||||
|
publicKeyPem: `${process.env.ACTIVITYPUB_PUBLIC_KEY}`
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const body = {
|
res.type('application/activity+json').json(body)
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1'
|
|
||||||
],
|
|
||||||
'id': `https://${process.env.DOMAIN}/users/keyoxide`,
|
|
||||||
'type': 'Application',
|
|
||||||
'inbox': `https://${process.env.DOMAIN}/users/keyoxide/inbox`,
|
|
||||||
'preferredUsername': `${process.env.DOMAIN}`,
|
|
||||||
'publicKey': {
|
|
||||||
'id': `https://${process.env.DOMAIN}/users/keyoxide#main-key`,
|
|
||||||
'owner': `https://${process.env.DOMAIN}/users/keyoxide`,
|
|
||||||
'publicKeyPem': `${process.env.ACTIVITYPUB_PUBLIC_KEY}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.type('application/activity+json').json(body)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -35,49 +35,49 @@ const router = express.Router()
|
||||||
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
||||||
|
|
||||||
router.get('/sig', (req, res) => {
|
router.get('/sig', (req, res) => {
|
||||||
res.render('profile', { isSignature: true, signature: null })
|
res.render('profile', { isSignature: true, signature: null })
|
||||||
})
|
})
|
||||||
|
|
||||||
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.keyData.openpgp4fpr)
|
||||||
res.render('profile', { title: title, data: data, isSignature: true, signature: req.body.signature })
|
res.render('profile', { title, data, isSignature: true, signature: req.body.signature })
|
||||||
})
|
})
|
||||||
|
|
||||||
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.keyData.openpgp4fpr)
|
||||||
res.render('profile', { title: title, data: data })
|
res.render('profile', { title, data })
|
||||||
})
|
})
|
||||||
|
|
||||||
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.keyData.openpgp4fpr)
|
||||||
res.render('profile', { title: title, data: data })
|
res.render('profile', { title, data })
|
||||||
})
|
})
|
||||||
|
|
||||||
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.keyData.openpgp4fpr)
|
||||||
res.render('profile', { title: title, data: data })
|
res.render('profile', { title, data })
|
||||||
})
|
})
|
||||||
|
|
||||||
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.keyData.openpgp4fpr)
|
||||||
res.render('profile', { title: title, data: data })
|
res.render('profile', { title, data })
|
||||||
})
|
})
|
||||||
|
|
||||||
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.keyData.openpgp4fpr)
|
||||||
res.render('profile', { title: title, data: data })
|
res.render('profile', { title, data })
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -31,49 +31,49 @@ import express from 'express'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
|
|
||||||
router.get('/', function(req, res) {
|
router.get('/', function (req, res) {
|
||||||
res.render('util/index')
|
res.render('util/index')
|
||||||
})
|
})
|
||||||
router.get('/profile-url', function(req, res) {
|
router.get('/profile-url', function (req, res) {
|
||||||
res.render('util/profile-url')
|
res.render('util/profile-url')
|
||||||
})
|
})
|
||||||
router.get('/profile-url/:input', function(req, res) {
|
router.get('/profile-url/:input', function (req, res) {
|
||||||
res.render('util/profile-url', { input: req.params.input })
|
res.render('util/profile-url', { input: req.params.input })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/qr', function(req, res) {
|
router.get('/qr', function (req, res) {
|
||||||
res.render('util/qr')
|
res.render('util/qr')
|
||||||
})
|
})
|
||||||
router.get('/qr/:input', function(req, res) {
|
router.get('/qr/:input', function (req, res) {
|
||||||
res.render('util/qr', { input: req.params.input })
|
res.render('util/qr', { input: req.params.input })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/qrfp', function(req, res) {
|
router.get('/qrfp', function (req, res) {
|
||||||
res.render('util/qrfp')
|
res.render('util/qrfp')
|
||||||
})
|
})
|
||||||
router.get('/qrfp/:input', function(req, res) {
|
router.get('/qrfp/:input', function (req, res) {
|
||||||
res.render('util/qrfp', { input: req.params.input })
|
res.render('util/qrfp', { input: req.params.input })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/wkd', function(req, res) {
|
router.get('/wkd', function (req, res) {
|
||||||
res.render('util/wkd')
|
res.render('util/wkd')
|
||||||
})
|
})
|
||||||
router.get('/wkd/:input', function(req, res) {
|
router.get('/wkd/:input', function (req, res) {
|
||||||
res.render('util/wkd', { input: req.params.input })
|
res.render('util/wkd', { input: req.params.input })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/argon2', function(req, res) {
|
router.get('/argon2', function (req, res) {
|
||||||
res.render('util/argon2')
|
res.render('util/argon2')
|
||||||
})
|
})
|
||||||
router.get('/argon2/:input', function(req, res) {
|
router.get('/argon2/:input', function (req, res) {
|
||||||
res.render('util/argon2', { input: req.params.input })
|
res.render('util/argon2', { input: req.params.input })
|
||||||
})
|
})
|
||||||
|
|
||||||
router.get('/bcrypt', function(req, res) {
|
router.get('/bcrypt', function (req, res) {
|
||||||
res.render('util/bcrypt')
|
res.render('util/bcrypt')
|
||||||
})
|
})
|
||||||
router.get('/bcrypt/:input', function(req, res) {
|
router.get('/bcrypt/:input', function (req, res) {
|
||||||
res.render('util/bcrypt', { input: req.params.input })
|
res.render('util/bcrypt', { input: req.params.input })
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -28,54 +28,54 @@ 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/>.
|
||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
"claimVersion": 1,
|
claimVersion: 1,
|
||||||
"uri": "https://fosstodon.org/@keyoxide",
|
uri: 'https://fosstodon.org/@keyoxide',
|
||||||
"fingerprint": "9f0048ac0b23301e1f77e994909f6bd6f80f485d",
|
fingerprint: '9f0048ac0b23301e1f77e994909f6bd6f80f485d',
|
||||||
"status": "verified",
|
status: 'verified',
|
||||||
"matches": [
|
matches: [
|
||||||
{
|
{
|
||||||
"serviceprovider": {
|
serviceprovider: {
|
||||||
"type": "web",
|
type: 'web',
|
||||||
"name": "mastodon (demo)"
|
name: 'mastodon (demo)'
|
||||||
},
|
},
|
||||||
"match": {
|
match: {
|
||||||
"regularExpression": {},
|
regularExpression: {},
|
||||||
"isAmbiguous": true
|
isAmbiguous: true
|
||||||
},
|
},
|
||||||
"profile": {
|
profile: {
|
||||||
"display": "@keyoxide@fosstodon.org",
|
display: '@keyoxide@fosstodon.org',
|
||||||
"uri": "https://fosstodon.org/@keyoxide",
|
uri: 'https://fosstodon.org/@keyoxide',
|
||||||
"qr": null
|
qr: null
|
||||||
},
|
},
|
||||||
"proof": {
|
proof: {
|
||||||
"uri": "https://fosstodon.org/@keyoxide",
|
uri: 'https://fosstodon.org/@keyoxide',
|
||||||
"request": {
|
request: {
|
||||||
"fetcher": "http",
|
fetcher: 'http',
|
||||||
"access": 0,
|
access: 0,
|
||||||
"format": "json",
|
format: 'json',
|
||||||
"data": {
|
data: {
|
||||||
"url": "https://fosstodon.org/@keyoxide",
|
url: 'https://fosstodon.org/@keyoxide',
|
||||||
"format": "json"
|
format: 'json'
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
"claim": {
|
|
||||||
"format": 1,
|
|
||||||
"relation": 0,
|
|
||||||
"path": [
|
|
||||||
"attachment",
|
|
||||||
"value"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"verification": {
|
|
||||||
"result": true,
|
|
||||||
"completed": true,
|
|
||||||
"errors": [],
|
|
||||||
"proof": {
|
|
||||||
"fetcher": "http",
|
|
||||||
"viaProxy": false
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
claim: {
|
||||||
|
format: 1,
|
||||||
|
relation: 0,
|
||||||
|
path: [
|
||||||
|
'attachment',
|
||||||
|
'value'
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
verification: {
|
||||||
|
result: true,
|
||||||
|
completed: true,
|
||||||
|
errors: [],
|
||||||
|
proof: {
|
||||||
|
fetcher: 'http',
|
||||||
|
viaProxy: false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
310
server/index.js
310
server/index.js
|
@ -32,198 +32,198 @@ import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './keys.js'
|
||||||
import libravatar from 'libravatar'
|
import libravatar from 'libravatar'
|
||||||
|
|
||||||
const generateWKDProfile = async (id) => {
|
const generateWKDProfile = async (id) => {
|
||||||
return fetchWKD(id)
|
return fetchWKD(id)
|
||||||
.then(async key => {
|
.then(async key => {
|
||||||
let keyData = await doipjs.keys.process(key.publicKey)
|
let keyData = await doipjs.keys.process(key.publicKey)
|
||||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||||
keyData.key.fetchMethod = 'wkd'
|
keyData.key.fetchMethod = 'wkd'
|
||||||
keyData.key.uri = key.fetchURL
|
keyData.key.uri = key.fetchURL
|
||||||
keyData.key.data = {}
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
let keyoxideData = {}
|
const keyoxideData = {}
|
||||||
keyoxideData.url = `https://${process.env.DOMAIN}/wkd/${id}`
|
keyoxideData.url = `https://${process.env.DOMAIN}/wkd/${id}`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: key,
|
key,
|
||||||
keyData: keyData,
|
keyData,
|
||||||
keyoxide: keyoxideData,
|
keyoxide: keyoxideData,
|
||||||
extra: await computeExtraData(key, keyData),
|
extra: await computeExtraData(key, keyData),
|
||||||
errors: []
|
errors: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: {},
|
|
||||||
keyData: {},
|
|
||||||
keyoxide: {},
|
|
||||||
extra: {},
|
|
||||||
errors: [err.message]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateHKPProfile = async (id, 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)
|
|
||||||
|
|
||||||
let keyoxideData = {}
|
|
||||||
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
|
|
||||||
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${id}`
|
|
||||||
} else {
|
|
||||||
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
keyData: keyData,
|
|
||||||
keyoxide: keyoxideData,
|
|
||||||
extra: await computeExtraData(key, keyData),
|
|
||||||
errors: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
return {
|
|
||||||
key: {},
|
|
||||||
keyData: {},
|
|
||||||
keyoxide: {},
|
|
||||||
extra: {},
|
|
||||||
errors: [err.message]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateAutoProfile = async (id) => {
|
|
||||||
let result
|
|
||||||
|
|
||||||
if (id.includes('@')) {
|
|
||||||
result = await generateWKDProfile(id)
|
|
||||||
|
|
||||||
if (result && result.errors.length === 0) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await generateHKPProfile(id)
|
|
||||||
if (result && result.errors.length === 0) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
key: {},
|
key: {},
|
||||||
keyData: {},
|
keyData: {},
|
||||||
keyoxide: {},
|
keyoxide: {},
|
||||||
extra: {},
|
extra: {},
|
||||||
errors: ["No public keys could be found"]
|
errors: [err.message]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateHKPProfile = async (id, 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 = {}
|
||||||
|
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
|
||||||
|
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${id}`
|
||||||
|
} else {
|
||||||
|
keyoxideData.url = `https://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
keyData,
|
||||||
|
keyoxide: keyoxideData,
|
||||||
|
extra: await computeExtraData(key, keyData),
|
||||||
|
errors: []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
return {
|
||||||
|
key: {},
|
||||||
|
keyData: {},
|
||||||
|
keyoxide: {},
|
||||||
|
extra: {},
|
||||||
|
errors: [err.message]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateAutoProfile = async (id) => {
|
||||||
|
let result
|
||||||
|
|
||||||
|
if (id.includes('@')) {
|
||||||
|
result = await generateWKDProfile(id)
|
||||||
|
|
||||||
|
if (result && result.errors.length === 0) {
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await generateHKPProfile(id)
|
||||||
|
if (result && result.errors.length === 0) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: {},
|
||||||
|
keyData: {},
|
||||||
|
keyoxide: {},
|
||||||
|
extra: {},
|
||||||
|
errors: ['No public keys could be found']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateSignatureProfile = async (signature) => {
|
const generateSignatureProfile = async (signature) => {
|
||||||
return fetchSignature(signature)
|
return fetchSignature(signature)
|
||||||
.then(async key => {
|
.then(async key => {
|
||||||
let keyData = key.keyData
|
let keyData = key.keyData
|
||||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||||
delete key.keyData
|
delete key.keyData
|
||||||
keyData.key.data = {}
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
let keyoxideData = {}
|
const keyoxideData = {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: key,
|
key,
|
||||||
keyData: keyData,
|
keyData,
|
||||||
keyoxide: keyoxideData,
|
keyoxide: keyoxideData,
|
||||||
extra: await computeExtraData(key, keyData),
|
extra: await computeExtraData(key, keyData),
|
||||||
errors: []
|
errors: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: {},
|
key: {},
|
||||||
keyData: {},
|
keyData: {},
|
||||||
keyoxide: {},
|
keyoxide: {},
|
||||||
extra: {},
|
extra: {},
|
||||||
errors: [err.message]
|
errors: [err.message]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateKeybaseProfile = async (username, fingerprint) => {
|
const generateKeybaseProfile = async (username, fingerprint) => {
|
||||||
return fetchKeybase(username, fingerprint)
|
return fetchKeybase(username, fingerprint)
|
||||||
.then(async key => {
|
.then(async key => {
|
||||||
let keyData = await doipjs.keys.process(key.publicKey)
|
let keyData = await doipjs.keys.process(key.publicKey)
|
||||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||||
keyData.key.fetchMethod = 'hkp'
|
keyData.key.fetchMethod = 'hkp'
|
||||||
keyData.key.uri = key.fetchURL
|
keyData.key.uri = key.fetchURL
|
||||||
keyData.key.data = {}
|
keyData.key.data = {}
|
||||||
keyData = processKeyData(keyData)
|
keyData = processKeyData(keyData)
|
||||||
|
|
||||||
let keyoxideData = {}
|
const keyoxideData = {}
|
||||||
keyoxideData.url = `https://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`
|
keyoxideData.url = `https://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key: key,
|
key,
|
||||||
keyData: keyData,
|
keyData,
|
||||||
keyoxide: keyoxideData,
|
keyoxide: keyoxideData,
|
||||||
extra: await computeExtraData(key, keyData),
|
extra: await computeExtraData(key, keyData),
|
||||||
errors: []
|
errors: []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
return {
|
return {
|
||||||
key: {},
|
key: {},
|
||||||
keyData: {},
|
keyData: {},
|
||||||
keyoxide: {},
|
keyoxide: {},
|
||||||
extra: {},
|
extra: {},
|
||||||
errors: [err.message]
|
errors: [err.message]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const processKeyData = (keyData) => {
|
const processKeyData = (keyData) => {
|
||||||
keyData.users.forEach(user => {
|
keyData.users.forEach(user => {
|
||||||
// Remove faulty claims
|
// Remove faulty claims
|
||||||
user.claims = user.claims.filter(claim => {
|
user.claims = user.claims.filter(claim => {
|
||||||
return claim instanceof doipjs.Claim
|
return claim instanceof doipjs.Claim
|
||||||
})
|
|
||||||
|
|
||||||
// Match claims
|
|
||||||
user.claims.forEach(claim => {
|
|
||||||
claim.match()
|
|
||||||
})
|
|
||||||
|
|
||||||
// Sort claims
|
|
||||||
user.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) {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
if (a.matches[0].serviceprovider.name > b.matches[0].serviceprovider.name) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return keyData
|
// Match claims
|
||||||
|
user.claims.forEach(claim => {
|
||||||
|
claim.match()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sort claims
|
||||||
|
user.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) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if (a.matches[0].serviceprovider.name > b.matches[0].serviceprovider.name) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return keyData
|
||||||
}
|
}
|
||||||
|
|
||||||
const computeExtraData = async (key, keyData) => {
|
const computeExtraData = async (key, keyData) => {
|
||||||
// Get the primary user
|
// Get the primary user
|
||||||
const primaryUser = await key.publicKey.getPrimaryUser()
|
const primaryUser = await key.publicKey.getPrimaryUser()
|
||||||
|
|
||||||
// Query libravatar to get the avatar url
|
// Query libravatar to get the avatar url
|
||||||
return {
|
return {
|
||||||
avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true })
|
avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { generateWKDProfile }
|
export { generateWKDProfile }
|
||||||
|
|
320
server/keys.js
320
server/keys.js
|
@ -37,191 +37,199 @@ import Keyv from 'keyv'
|
||||||
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
|
const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
|
||||||
|
|
||||||
const fetchWKD = (id) => {
|
const fetchWKD = (id) => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let output = {
|
(async () => {
|
||||||
publicKey: null,
|
const output = {
|
||||||
fetchURL: null
|
publicKey: null,
|
||||||
}
|
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`))
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, localPart, domain] = /([^\@]*)@(.*)/.exec(id)
|
const [, localPart, domain] = /([^@]*)@(.*)/.exec(id)
|
||||||
if (!localPart || !domain) {
|
if (!localPart || !domain) {
|
||||||
reject(new Error(`The WKD identifier "${id}" is invalid`));
|
reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||||
}
|
}
|
||||||
const localEncoded = await computeWKDLocalPart(localPart)
|
const localEncoded = await computeWKDLocalPart(localPart)
|
||||||
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
|
const urlAdvanced = `https://openpgpkey.${domain}/.well-known/openpgpkey/${domain}/hu/${localEncoded}`
|
||||||
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
|
const urlDirect = `https://${domain}/.well-known/openpgpkey/hu/${localEncoded}`
|
||||||
let plaintext
|
let plaintext
|
||||||
|
|
||||||
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(','))
|
plaintext = Uint8Array.from((await c.get(hash)).split(','))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!plaintext) {
|
||||||
|
try {
|
||||||
|
plaintext = await got(urlAdvanced).then((response) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
output.fetchURL = urlAdvanced
|
||||||
|
return new Uint8Array(response.rawBody)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
try {
|
||||||
|
plaintext = await got(urlDirect).then((response) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
output.fetchURL = urlDirect
|
||||||
|
return new Uint8Array(response.rawBody)
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('No public keys could be fetched using WKD'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!plaintext) {
|
if (!plaintext) {
|
||||||
try {
|
reject(new Error('No public keys could be fetched using WKD'))
|
||||||
plaintext = await got(urlAdvanced).then((response) => {
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
output.fetchURL = urlAdvanced
|
|
||||||
return new Uint8Array(response.rawBody)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
plaintext = await got(urlDirect).then((response) => {
|
|
||||||
if (response.statusCode === 200) {
|
|
||||||
output.fetchURL = urlDirect
|
|
||||||
return new Uint8Array(response.rawBody)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
reject(new Error(`No public keys could be fetched using WKD`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!plaintext) {
|
|
||||||
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 {
|
if (c && plaintext instanceof Uint8Array) {
|
||||||
output.publicKey = await readKey({
|
await c.set(hash, plaintext.toString(), 60 * 1000)
|
||||||
binaryKey: plaintext
|
|
||||||
})
|
|
||||||
} catch(error) {
|
|
||||||
reject(new Error(`No public keys could be read from the data fetched using WKD`))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!output.publicKey) {
|
try {
|
||||||
reject(new Error(`No public keys could be read from the data fetched using WKD`))
|
output.publicKey = await readKey({
|
||||||
}
|
binaryKey: plaintext
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||||
|
}
|
||||||
|
|
||||||
resolve(output)
|
if (!output.publicKey) {
|
||||||
})
|
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(output)
|
||||||
|
})()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchHKP = (id, keyserverDomain) => {
|
const fetchHKP = (id, keyserverDomain) => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let output = {
|
(async () => {
|
||||||
publicKey: null,
|
const output = {
|
||||||
fetchURL: null
|
publicKey: null,
|
||||||
|
fetchURL: null
|
||||||
|
}
|
||||||
|
|
||||||
|
keyserverDomain = keyserverDomain || 'keys.openpgp.org'
|
||||||
|
|
||||||
|
let query = ''
|
||||||
|
if (id.includes('@')) {
|
||||||
|
query = id
|
||||||
|
} else {
|
||||||
|
query = `0x${id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
output.fetchURL = `https://${keyserverDomain}/pks/lookup?op=get&options=mr&search=${query}`
|
||||||
|
|
||||||
|
const hash = createHash('md5').update(`${id}__${keyserverDomain}`).digest('hex')
|
||||||
|
|
||||||
|
if (c && await c.get(hash)) {
|
||||||
|
output.publicKey = await readKey({
|
||||||
|
armoredKey: await c.get(hash)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
output.publicKey = await doipjs.keys.fetchHKP(id, keyserverDomain)
|
||||||
|
} catch (error) {
|
||||||
|
reject(new Error('No public keys could be fetched using HKP'))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
keyserverDomain = keyserverDomain ? keyserverDomain : 'keys.openpgp.org'
|
if (!output.publicKey) {
|
||||||
|
reject(new Error('No public keys could be fetched using HKP'))
|
||||||
|
}
|
||||||
|
|
||||||
let query = ''
|
if (c && output.publicKey instanceof PublicKey) {
|
||||||
if (id.includes('@')) {
|
await c.set(hash, output.publicKey.armor(), 60 * 1000)
|
||||||
query = id
|
}
|
||||||
} else {
|
|
||||||
query = `0x${id}`
|
|
||||||
}
|
|
||||||
|
|
||||||
output.fetchURL = `https://${keyserverDomain}/pks/lookup?op=get&options=mr&search=${query}`
|
resolve(output)
|
||||||
|
})()
|
||||||
const hash = createHash('md5').update(`${id}__${keyserverDomain}`).digest('hex')
|
})
|
||||||
|
|
||||||
if (c && await c.get(hash)) {
|
|
||||||
output.publicKey = await readKey({
|
|
||||||
armoredKey: await c.get(hash)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
output.publicKey = await doipjs.keys.fetchHKP(id, keyserverDomain)
|
|
||||||
} catch(error) {
|
|
||||||
reject(new Error(`No public keys could be fetched using HKP`))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!output.publicKey) {
|
|
||||||
reject(new Error(`No public keys could be fetched using HKP`))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (c && output.publicKey instanceof PublicKey) {
|
|
||||||
await c.set(hash, output.publicKey.armor(), 60 * 1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(output)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchSignature = (signature) => {
|
const fetchSignature = (signature) => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let output = {
|
(async () => {
|
||||||
publicKey: null,
|
const output = {
|
||||||
fetchURL: null,
|
publicKey: null,
|
||||||
keyData: null
|
fetchURL: null,
|
||||||
}
|
keyData: null
|
||||||
|
}
|
||||||
|
|
||||||
// Check validity of signature
|
// Check validity of signature
|
||||||
let signatureData
|
let signatureData
|
||||||
try {
|
try {
|
||||||
signatureData = await readCleartextMessage({
|
signatureData = await readCleartextMessage({
|
||||||
cleartextMessage: signature
|
cleartextMessage: signature
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
reject(new Error(`Signature could not be properly read (${error.message})`))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process the signature
|
|
||||||
try {
|
|
||||||
output.keyData = await doipjs.signatures.process(signature)
|
|
||||||
output.publicKey = output.keyData.key.data
|
|
||||||
// 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`))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check validity of signature
|
|
||||||
const verified = await verify({
|
|
||||||
message: signatureData,
|
|
||||||
verificationKeys: output.publicKey
|
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
if (!await verified.signatures[0].verified) {
|
reject(new Error(`Signature could not be properly read (${error.message})`))
|
||||||
reject(new Error('Signature was invalid'))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
resolve(output)
|
// Process the signature
|
||||||
})
|
try {
|
||||||
|
output.keyData = await doipjs.signatures.process(signature)
|
||||||
|
output.publicKey = output.keyData.key.data
|
||||||
|
// 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'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
})()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchKeybase = (username, fingerprint) => {
|
const fetchKeybase = (username, fingerprint) => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let output = {
|
(async () => {
|
||||||
publicKey: null,
|
const output = {
|
||||||
fetchURL: null
|
publicKey: null,
|
||||||
}
|
fetchURL: null
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint)
|
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint)
|
||||||
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
output.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 (!output.publicKey) {
|
||||||
reject(new Error(`No public keys could be fetched from Keybase`))
|
reject(new Error('No public keys could be fetched from Keybase'))
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(output)
|
resolve(output)
|
||||||
})
|
})()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export { fetchWKD }
|
export { fetchWKD }
|
||||||
|
|
|
@ -29,54 +29,52 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
||||||
*/
|
*/
|
||||||
import { webcrypto as crypto } from 'crypto'
|
import { webcrypto as crypto } from 'crypto'
|
||||||
|
|
||||||
export async function computeWKDLocalPart(localPart) {
|
export async function computeWKDLocalPart (localPart) {
|
||||||
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase());
|
const localPartEncoded = new TextEncoder().encode(localPart.toLowerCase())
|
||||||
const localPartHashed = new Uint8Array(await crypto.subtle.digest('SHA-1', localPartEncoded));
|
const localPartHashed = new Uint8Array(await crypto.subtle.digest('SHA-1', localPartEncoded))
|
||||||
return encodeZBase32(localPartHashed);
|
return encodeZBase32(localPartHashed)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generatePageTitle(type, data) {
|
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.keyData.users[data.keyData.primaryUserIndex].userData.name} - Keyoxide`
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return 'Profile - Keyoxide'
|
return 'Profile - Keyoxide'
|
||||||
}
|
}
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return 'Keyoxide'
|
return 'Keyoxide'
|
||||||
break
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copied from https://github.com/openpgpjs/wkd-client/blob/0d074519e011a5139a8953679cf5f807e4cd2378/src/wkd.js
|
// Copied from https://github.com/openpgpjs/wkd-client/blob/0d074519e011a5139a8953679cf5f807e4cd2378/src/wkd.js
|
||||||
export function encodeZBase32(data) {
|
export function encodeZBase32 (data) {
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
return "";
|
return ''
|
||||||
|
}
|
||||||
|
const ALPHABET = 'ybndrfg8ejkmcpqxot1uwisza345h769'
|
||||||
|
const SHIFT = 5
|
||||||
|
const MASK = 31
|
||||||
|
let buffer = data[0]
|
||||||
|
let index = 1
|
||||||
|
let bitsLeft = 8
|
||||||
|
let result = ''
|
||||||
|
while (bitsLeft > 0 || index < data.length) {
|
||||||
|
if (bitsLeft < SHIFT) {
|
||||||
|
if (index < data.length) {
|
||||||
|
buffer <<= 8
|
||||||
|
buffer |= data[index++] & 0xff
|
||||||
|
bitsLeft += 8
|
||||||
|
} else {
|
||||||
|
const pad = SHIFT - bitsLeft
|
||||||
|
buffer <<= pad
|
||||||
|
bitsLeft += pad
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const ALPHABET = "ybndrfg8ejkmcpqxot1uwisza345h769";
|
bitsLeft -= SHIFT
|
||||||
const SHIFT = 5;
|
result += ALPHABET[MASK & (buffer >> bitsLeft)]
|
||||||
const MASK = 31;
|
}
|
||||||
let buffer = data[0];
|
return result
|
||||||
let index = 1;
|
}
|
||||||
let bitsLeft = 8;
|
|
||||||
let result = '';
|
|
||||||
while (bitsLeft > 0 || index < data.length) {
|
|
||||||
if (bitsLeft < SHIFT) {
|
|
||||||
if (index < data.length) {
|
|
||||||
buffer <<= 8;
|
|
||||||
buffer |= data[index++] & 0xff;
|
|
||||||
bitsLeft += 8;
|
|
||||||
} else {
|
|
||||||
const pad = SHIFT - bitsLeft;
|
|
||||||
buffer <<= pad;
|
|
||||||
bitsLeft += pad;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bitsLeft -= SHIFT;
|
|
||||||
result += ALPHABET[MASK & (buffer >> bitsLeft)];
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
18
src/index.js
18
src/index.js
|
@ -54,21 +54,21 @@ app.set('onion_url', process.env.ONION_URL)
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.setHeader('Permissions-Policy', 'interest-cohort=()')
|
res.setHeader('Permissions-Policy', 'interest-cohort=()')
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (app.get('onion_url')) {
|
if (app.get('onion_url')) {
|
||||||
app.get('/*', (req, res, next) => {
|
app.get('/*', (req, res, next) => {
|
||||||
res.header('Onion-Location', app.get('onion_url'))
|
res.header('Onion-Location', app.get('onion_url'))
|
||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(stringReplace({
|
app.use(stringReplace({
|
||||||
PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || process.env.DOMAIN || 'null'
|
PLACEHOLDER__PROXY_HOSTNAME: process.env.PROXY_HOSTNAME || process.env.DOMAIN || 'null'
|
||||||
}, {
|
}, {
|
||||||
contentTypeFilterRegexp: /application\/javascript/,
|
contentTypeFilterRegexp: /application\/javascript/
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
@ -82,7 +82,7 @@ app.use('/util', utilRoute)
|
||||||
app.use('/', profileRoute)
|
app.use('/', profileRoute)
|
||||||
|
|
||||||
app.listen(app.get('port'), () => {
|
app.listen(app.get('port'), () => {
|
||||||
console.log(`Node server listening at http://localhost:${app.get('port')}`)
|
console.log(`Node server listening at http://localhost:${app.get('port')}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
export default app
|
export default app
|
||||||
|
|
Loading…
Reference in a new issue