forked from Mirrors/keyoxide-web
Merge branch 'dev' into configurable-scheme initial merge conflict
resolution Tests not updated yet
This commit is contained in:
commit
0b9a00c69e
22 changed files with 1587 additions and 1928 deletions
|
@ -9,7 +9,7 @@
|
|||
"bent": "^7.3.12",
|
||||
"body-parser": "^1.19.0",
|
||||
"dialog-polyfill": "^0.5.6",
|
||||
"doipjs": "^0.18.3",
|
||||
"doipjs": "^1.0.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-validator": "^6.13.0",
|
||||
|
@ -28,14 +28,14 @@
|
|||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"chai": "^4.3.6",
|
||||
"copy-webpack-plugin": "^10.2.4",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.6.0",
|
||||
"esmock": "^2.3.1",
|
||||
"license-check-and-add": "^4.0.5",
|
||||
"mini-css-extract-plugin": "^2.5.3",
|
||||
"mocha": "^10.1.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"rome": "^11.0.0",
|
||||
"rome": "^12.1",
|
||||
"standard": "^17.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"webpack": "^5.75.0",
|
||||
|
@ -46,11 +46,13 @@
|
|||
"start": "node --experimental-fetch ./",
|
||||
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev",
|
||||
"test": "yarn run standard:check && yarn run rome:check && mocha --loader=esmock",
|
||||
"test": "yarn run lint && mocha --loader=esmock",
|
||||
"watch": "./node_modules/.bin/nodemon --config nodemon.json ./",
|
||||
"build": "yarn run build:server & yarn run build:static",
|
||||
"build:server": "ncc build ./src/index.js -o dist",
|
||||
"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",
|
||||
"lint": "yarn run standard:check && yarn run rome:check",
|
||||
"standard:check": "./node_modules/.bin/standard ./src",
|
||||
"standard:fix": "./node_modules/.bin/standard --fix ./src",
|
||||
"rome:check": "./node_modules/.bin/rome check ./src",
|
||||
|
|
|
@ -1,400 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { check, validationResult } from 'express-validator'
|
||||
import Ajv from 'ajv'
|
||||
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv({ coerceTypes: true })
|
||||
|
||||
const apiProfileSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fingerprint: {
|
||||
type: 'string'
|
||||
},
|
||||
openpgp4fpr: {
|
||||
type: 'string'
|
||||
},
|
||||
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'
|
||||
},
|
||||
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: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
avatarURL: { type: 'string' }
|
||||
}
|
||||
},
|
||||
errors: {
|
||||
type: 'array'
|
||||
}
|
||||
},
|
||||
required: ['keyData', 'keyoxide', 'extra', 'errors'],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
const apiProfileValidate = ajv.compile(apiProfileSchema)
|
||||
|
||||
const doVerification = async (data) => {
|
||||
const promises = []
|
||||
const results = []
|
||||
const verificationOptions = {
|
||||
proxy: {
|
||||
hostname: process.env.PROXY_HOSTNAME,
|
||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never',
|
||||
scheme: (process.env.PROXY_SCHEME !== '') ? process.env.PROXY_SCHEME : (process.env.SCHEME !== '') ? process.env.SCHEME : 'https'
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||
const user = data.keyData.users[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
await claim.verify(verificationOptions)
|
||||
results.push([iUser, iClaim, claim])
|
||||
resolve()
|
||||
})()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const sanitize = (data) => {
|
||||
const dataClone = JSON.parse(JSON.stringify(data))
|
||||
|
||||
if (dataClone.keyData.users) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const valid = apiProfileValidate(data)
|
||||
if (!valid) {
|
||||
throw new Error('Profile data sanitization error')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const addSummaryToClaims = (data) => {
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
// To be removed when data is added by DOIP library
|
||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
||||
const user = data.keyData.users[userIndex]
|
||||
|
||||
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
||||
const claim = user.claims[claimIndex]
|
||||
|
||||
const isVerificationDone = claim.status === 'verified'
|
||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
||||
const isAmbiguous = isVerified
|
||||
? false
|
||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
||||
|
||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
||||
isVerificationDone,
|
||||
isVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
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 generateWKDProfile(req.query.query)
|
||||
break
|
||||
case 'hkp':
|
||||
data = await generateHKPProfile(req.query.query)
|
||||
break
|
||||
default:
|
||||
data = await generateAutoProfile(req.query.query)
|
||||
break
|
||||
}
|
||||
|
||||
if (data.errors.length > 0) {
|
||||
data.key = undefined
|
||||
res.status(500).send(data)
|
||||
}
|
||||
|
||||
// Return public key
|
||||
if (req.query.returnPublicKey) {
|
||||
data.keyData.key.data = data.key.publicKey
|
||||
}
|
||||
data.key = undefined
|
||||
|
||||
// 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',
|
||||
check('data').exists().isJSON(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
|
@ -1,23 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 Yarmo Mackenbach
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import express from 'express'
|
||||
const router = express.Router()
|
||||
|
||||
router.get('*', (req, res) => {
|
||||
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v2 API endpoint')
|
||||
})
|
||||
|
||||
export default router
|
|
@ -1,371 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2022 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { check, validationResult } from 'express-validator'
|
||||
import Ajv from 'ajv'
|
||||
import { generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||
import * as dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv({ coerceTypes: true })
|
||||
|
||||
const apiProfileSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyData: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fingerprint: {
|
||||
type: 'string'
|
||||
},
|
||||
openpgp4fpr: {
|
||||
type: 'string'
|
||||
},
|
||||
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: 'array',
|
||||
items: {
|
||||
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' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
primaryUserIndex: {
|
||||
type: 'integer'
|
||||
},
|
||||
key: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
data: { type: 'object' },
|
||||
fetchMethod: { type: 'string' },
|
||||
uri: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
keyoxide: {
|
||||
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 doVerification = async (data) => {
|
||||
const promises = []
|
||||
const results = []
|
||||
const verificationOptions = {
|
||||
proxy: {
|
||||
hostname: process.env.PROXY_HOSTNAME,
|
||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never',
|
||||
scheme: (process.env.PROXY_SCHEME !== '') ? process.env.PROXY_SCHEME : (process.env.SCHEME !== '') ? process.env.SCHEME : 'https'
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
for (let iUser = 0; iUser < data.keyData.users.length; iUser++) {
|
||||
const user = data.keyData.users[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
await claim.verify(verificationOptions)
|
||||
results.push([iUser, iClaim, claim])
|
||||
resolve()
|
||||
})()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
data.keyData.users[result[0]].claims[result[1]] = result[2]
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const sanitize = (data) => {
|
||||
const valid = apiProfileValidate(data)
|
||||
if (!valid) {
|
||||
throw new Error('Profile data sanitization error')
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const addSummaryToClaims = (data) => {
|
||||
// Return early if no users in key
|
||||
if (!data.keyData.users) {
|
||||
return data
|
||||
}
|
||||
|
||||
// To be removed when data is added by DOIP library
|
||||
for (let userIndex = 0; userIndex < data.keyData.users.length; userIndex++) {
|
||||
const user = data.keyData.users[userIndex]
|
||||
|
||||
for (let claimIndex = 0; claimIndex < user.claims.length; claimIndex++) {
|
||||
const claim = user.claims[claimIndex]
|
||||
|
||||
const isVerificationDone = claim.status === 'verified'
|
||||
const isVerified = isVerificationDone ? claim.verification.result : false
|
||||
const isAmbiguous = isVerified
|
||||
? false
|
||||
: claim.matches.length > 1 || claim.matches[0].match.isAmbiguous
|
||||
|
||||
data.keyData.users[userIndex].claims[claimIndex].summary = {
|
||||
profileName: !isAmbiguous ? claim.matches[0].profile.display : claim.uri,
|
||||
profileURL: !isAmbiguous ? claim.matches[0].profile.uri : '',
|
||||
serviceProviderName: !isAmbiguous ? claim.matches[0].serviceprovider.name : '',
|
||||
isVerificationDone,
|
||||
isVerified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
router.get('/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 generateWKDProfile(req.query.query)
|
||||
break
|
||||
case 'hkp':
|
||||
data = await generateHKPProfile(req.query.query)
|
||||
break
|
||||
default:
|
||||
data = await generateAutoProfile(req.query.query)
|
||||
break
|
||||
}
|
||||
|
||||
if (data.errors.length > 0) {
|
||||
data.key = undefined
|
||||
res.status(500).send(data)
|
||||
}
|
||||
|
||||
// Return public key
|
||||
if (req.query.returnPublicKey) {
|
||||
data.keyData.key.data = data.key.publicKey
|
||||
}
|
||||
data.key = undefined
|
||||
|
||||
// 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',
|
||||
check('data').exists().isJSON(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright (C) 2022 Yarmo Mackenbach
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
185
src/api/v3/keyoxide_profile.js
Normal file
185
src/api/v3/keyoxide_profile.js
Normal file
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer network,
|
||||
you should also make sure that it provides a way for users to get its source.
|
||||
For example, if your program is a web application, its interface could display
|
||||
a "Source" link that leads users to an archive of the code. There are many
|
||||
ways you could offer source, and different solutions will be better for different
|
||||
programs; see section 13 for the specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
||||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import { check, validationResult } from 'express-validator'
|
||||
import Ajv from 'ajv/dist/2020.js'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { Claim } from 'doipjs'
|
||||
import { generateAspeProfile, generateWKDProfile, generateHKPProfile, generateAutoProfile } from '../../server/index.js'
|
||||
import { claimSchema, personaSchema, profileSchema, serviceProviderSchema } from '../../schemas.js'
|
||||
dotenv.config()
|
||||
|
||||
const router = express.Router()
|
||||
const ajv = new Ajv({
|
||||
schemas: [profileSchema, personaSchema, claimSchema, serviceProviderSchema]
|
||||
})
|
||||
|
||||
const apiProfileValidate = ajv.compile(profileSchema)
|
||||
|
||||
const doVerification = async (profile) => {
|
||||
const promises = []
|
||||
const results = []
|
||||
const verificationOptions = {
|
||||
proxy: {
|
||||
hostname: process.env.PROXY_HOSTNAME,
|
||||
policy: (process.env.PROXY_HOSTNAME !== '') ? 'adaptive' : 'never'
|
||||
}
|
||||
}
|
||||
|
||||
// Return early if no users in key
|
||||
if (!profile.personas) {
|
||||
return profile
|
||||
}
|
||||
|
||||
for (let iUser = 0; iUser < profile.personas.length; iUser++) {
|
||||
const user = profile.personas[iUser]
|
||||
|
||||
for (let iClaim = 0; iClaim < user.claims.length; iClaim++) {
|
||||
const claim = user.claims[iClaim]
|
||||
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
await claim.verify(verificationOptions)
|
||||
results.push([iUser, iClaim, claim])
|
||||
resolve()
|
||||
})()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
await Promise.all(promises)
|
||||
|
||||
results.forEach(result => {
|
||||
profile.personas[result[0]].claims[result[1]] = result[2]
|
||||
})
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
const validate = (profile) => {
|
||||
const valid = apiProfileValidate(profile)
|
||||
if (!valid) {
|
||||
throw new Error(`Profile data validation error: ${apiProfileValidate.errors.map(x => x.message).join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/fetch',
|
||||
check('query').exists(),
|
||||
check('protocol').optional().toLowerCase().isIn(['aspe', 'hkp', 'wkd']),
|
||||
check('doVerification').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 'aspe':
|
||||
data = await generateAspeProfile(req.query.query)
|
||||
break
|
||||
case 'wkd':
|
||||
data = await generateWKDProfile(req.query.query)
|
||||
break
|
||||
case 'hkp':
|
||||
data = await generateHKPProfile(req.query.query)
|
||||
break
|
||||
default:
|
||||
data = await generateAutoProfile(req.query.query)
|
||||
break
|
||||
}
|
||||
|
||||
if ('errors' in data && data.errors.length > 0) {
|
||||
res.status(500).send(data)
|
||||
}
|
||||
|
||||
// Do verification
|
||||
if (req.query.doVerification) {
|
||||
data = await doVerification(data)
|
||||
}
|
||||
|
||||
data = data.toJSON()
|
||||
|
||||
try {
|
||||
// Validate JSON
|
||||
validate(data)
|
||||
} catch (error) {
|
||||
data = {
|
||||
errors: [error.message]
|
||||
}
|
||||
}
|
||||
|
||||
let statusCode = 200
|
||||
if ('errors' in data && data.errors.length > 0) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
router.get('/verify',
|
||||
check('data').exists().isJSON(),
|
||||
async (req, res) => {
|
||||
const valRes = validationResult(req)
|
||||
if (!valRes.isEmpty()) {
|
||||
res.status(400).send(valRes)
|
||||
return
|
||||
}
|
||||
|
||||
const profile = Claim.fromJson(req.query.data)
|
||||
|
||||
// Do verification
|
||||
let data = await doVerification(profile)
|
||||
|
||||
data = data.toJSON()
|
||||
|
||||
try {
|
||||
// Validate JSON
|
||||
validate(data)
|
||||
} catch (error) {
|
||||
data = {
|
||||
errors: [error.message]
|
||||
}
|
||||
}
|
||||
|
||||
let statusCode = 200
|
||||
if ('errors' in data) {
|
||||
statusCode = 500
|
||||
}
|
||||
|
||||
res.status(statusCode).send(data)
|
||||
}
|
||||
)
|
||||
|
||||
export default router
|
|
@ -51,9 +51,6 @@ const opts = {
|
|||
telegram: {
|
||||
token: process.env.TELEGRAM_TOKEN || null
|
||||
},
|
||||
twitter: {
|
||||
bearerToken: process.env.TWITTER_BEARER_TOKEN || null
|
||||
},
|
||||
xmpp: {
|
||||
service: process.env.XMPP_SERVICE || null,
|
||||
username: process.env.XMPP_USERNAME || null,
|
||||
|
@ -138,26 +135,6 @@ router.get(
|
|||
}
|
||||
)
|
||||
|
||||
// Twitter route
|
||||
router.get('/twitter', query('tweetId').isInt(), async (req, res) => {
|
||||
if (!opts.claims.twitter.bearerToken) {
|
||||
return res.status(501).json({ errors: 'Twitter not enabled on server' })
|
||||
}
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() })
|
||||
}
|
||||
|
||||
fetcher.twitter
|
||||
.fn(req.query, opts)
|
||||
.then((data) => {
|
||||
return res.status(200).send(data)
|
||||
})
|
||||
.catch((err) => {
|
||||
return res.status(400).json({ errors: err.message ? err.message : err })
|
||||
})
|
||||
})
|
||||
|
||||
// Matrix route
|
||||
router.get(
|
||||
'/matrix',
|
|
@ -28,16 +28,19 @@ if any, to sign a "copyright disclaimer" for the program, if necessary. For
|
|||
more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import express from 'express'
|
||||
import apiRouter0 from '../api/v0/index.js'
|
||||
import apiRouter1 from '../api/v1/index.js'
|
||||
import apiRouter2 from '../api/v2/index.js'
|
||||
import apiRouter3 from '../api/v3/index.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
if ((process.env.ENABLE_MAIN_MODULE ?? 'true') === 'true') {
|
||||
router.use('/0', apiRouter0)
|
||||
}
|
||||
router.use('/1', apiRouter1)
|
||||
router.use('/2', apiRouter2)
|
||||
router.get('/0', (req, res) => {
|
||||
return res.status(501).send('Proxy v0 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
|
||||
})
|
||||
router.get('/1', (req, res) => {
|
||||
return res.status(501).send('Proxy v1 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
|
||||
})
|
||||
router.get('/2', (req, res) => {
|
||||
return res.status(501).send('Proxy v2 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
|
||||
})
|
||||
router.use('/3', apiRouter3)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -30,7 +30,6 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
import express from 'express'
|
||||
import markdownImport from 'markdown-it'
|
||||
import { readFileSync } from 'fs'
|
||||
import demoData from '../server/demo.js'
|
||||
|
||||
const router = express.Router()
|
||||
const md = markdownImport({ typographer: true })
|
||||
|
@ -48,7 +47,7 @@ router.get('/', (req, res) => {
|
|||
}
|
||||
}
|
||||
|
||||
res.render('index', { highlights, demoData })
|
||||
res.render('index', { highlights })
|
||||
})
|
||||
|
||||
router.get('/privacy', (req, res) => {
|
||||
|
|
|
@ -30,6 +30,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
import express from 'express'
|
||||
import bodyParserImport from 'body-parser'
|
||||
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
|
||||
import { Profile } from 'doipjs'
|
||||
|
||||
const router = express.Router()
|
||||
const bodyParser = bodyParserImport.urlencoded({ extended: false })
|
||||
|
@ -41,10 +42,10 @@ router.get('/sig', (req, res) => {
|
|||
router.post('/sig', bodyParser, async (req, res) => {
|
||||
const data = await generateSignatureProfile(req.body.signature)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
isSignature: true,
|
||||
signature: req.body.signature,
|
||||
enable_message_encryption: false,
|
||||
|
@ -55,10 +56,10 @@ router.post('/sig', bodyParser, async (req, res) => {
|
|||
router.get('/wkd/:id', async (req, res) => {
|
||||
const data = await generateWKDProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -67,10 +68,10 @@ router.get('/wkd/:id', async (req, res) => {
|
|||
router.get('/hkp/:id', async (req, res) => {
|
||||
const data = await generateHKPProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -79,10 +80,10 @@ router.get('/hkp/:id', async (req, res) => {
|
|||
router.get('/hkp/:server/:id', async (req, res) => {
|
||||
const data = await generateHKPProfile(req.params.id, req.params.server)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -91,10 +92,10 @@ router.get('/hkp/:server/:id', async (req, res) => {
|
|||
router.get('/keybase/:username/:fingerprint', async (req, res) => {
|
||||
const data = await generateKeybaseProfile(req.params.username, req.params.fingerprint)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
@ -103,10 +104,10 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => {
|
|||
router.get('/:id', async (req, res) => {
|
||||
const data = await generateAutoProfile(req.params.id)
|
||||
const title = utils.generatePageTitle('profile', data)
|
||||
res.set('ariadne-identity-proof', data.keyData.openpgp4fpr)
|
||||
res.set('ariadne-identity-proof', data.identifier)
|
||||
res.render('profile', {
|
||||
title,
|
||||
data,
|
||||
data: data instanceof Profile ? data.toJSON() : data,
|
||||
enable_message_encryption: false,
|
||||
enable_signature_verification: false
|
||||
})
|
||||
|
|
375
src/schemas.js
Normal file
375
src/schemas.js
Normal file
|
@ -0,0 +1,375 @@
|
|||
/*
|
||||
Copyright (C) 2023 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <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/>.
|
||||
*/
|
||||
export const profileSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/profile.schema.json',
|
||||
title: 'Profile',
|
||||
description: 'Keyoxide profile with personas',
|
||||
type: 'object',
|
||||
properties: {
|
||||
profileVersion: {
|
||||
description: 'The version of the profile',
|
||||
type: 'integer'
|
||||
},
|
||||
profileType: {
|
||||
description: 'The type of the profile [openpgp, asp]',
|
||||
type: 'string'
|
||||
},
|
||||
identifier: {
|
||||
description: 'Identifier of the profile (email, fingerprint, URI)',
|
||||
type: 'string'
|
||||
},
|
||||
personas: {
|
||||
description: 'The personas inside the profile',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: 'https://spec.keyoxide.org/2/persona.schema.json'
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
primaryPersonaIndex: {
|
||||
description: 'The index of the primary persona',
|
||||
type: 'integer'
|
||||
},
|
||||
publicKey: {
|
||||
description: 'The cryptographic key associated with the profile',
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyType: {
|
||||
description: 'The type of cryptographic key [eddsa, es256, openpgp, none]',
|
||||
type: 'string'
|
||||
},
|
||||
encoding: {
|
||||
description: 'The encoding of the cryptographic key [pem, jwk, armored_pgp, none]',
|
||||
type: 'string'
|
||||
},
|
||||
encodedKey: {
|
||||
description: 'The encoded cryptographic key (PEM, stringified JWK, ...)',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
fetch: {
|
||||
description: 'Details on how to fetch the public key',
|
||||
type: 'object',
|
||||
properties: {
|
||||
method: {
|
||||
description: 'The method to fetch the key [aspe, hkp, wkd, http, none]',
|
||||
type: 'string'
|
||||
},
|
||||
query: {
|
||||
description: 'The query to fetch the key',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
resolvedUrl: {
|
||||
description: 'The URL the method eventually resolved to',
|
||||
type: ['string', 'null']
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'keyType',
|
||||
'fetch'
|
||||
]
|
||||
},
|
||||
verifiers: {
|
||||
description: 'A list of links to verifiers',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
description: 'Name of the verifier site',
|
||||
type: 'string'
|
||||
},
|
||||
url: {
|
||||
description: 'URL to the profile page on the verifier site',
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'profileVersion',
|
||||
'profileType',
|
||||
'identifier',
|
||||
'personas',
|
||||
'primaryPersonaIndex',
|
||||
'publicKey',
|
||||
'verifiers'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
export const personaSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/persona.schema.json',
|
||||
title: 'Profile',
|
||||
description: 'Keyoxide persona with identity claims',
|
||||
type: 'object',
|
||||
properties: {
|
||||
identifier: {
|
||||
description: 'Identifier of the persona',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
name: {
|
||||
description: 'Name of the persona',
|
||||
type: 'string'
|
||||
},
|
||||
email: {
|
||||
description: 'Email address of the persona',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
description: {
|
||||
description: 'Description of the persona',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
avatarUrl: {
|
||||
description: 'URL to an avatar image',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
isRevoked: {
|
||||
type: 'boolean'
|
||||
},
|
||||
claims: {
|
||||
description: 'A list of identity claims',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: 'https://spec.keyoxide.org/2/claim.schema.json'
|
||||
},
|
||||
uniqueItems: true
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'name',
|
||||
'claims'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
export const claimSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/claim.schema.json',
|
||||
title: 'Identity claim',
|
||||
description: 'Verifiable online identity claim',
|
||||
type: 'object',
|
||||
properties: {
|
||||
claimVersion: {
|
||||
description: 'The version of the claim',
|
||||
type: 'integer'
|
||||
},
|
||||
uri: {
|
||||
description: 'The claim URI',
|
||||
type: 'string'
|
||||
},
|
||||
proofs: {
|
||||
description: 'The proofs that would verify the claim',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
minItems: 1,
|
||||
uniqueItems: true
|
||||
},
|
||||
matches: {
|
||||
description: 'Service providers matched to the claim',
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: 'https://spec.keyoxide.org/2/serviceprovider.schema.json'
|
||||
},
|
||||
uniqueItems: true
|
||||
},
|
||||
status: {
|
||||
type: 'integer',
|
||||
description: 'Claim status code'
|
||||
},
|
||||
display: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Account name to display in the user interface'
|
||||
},
|
||||
url: {
|
||||
type: ['string', 'null'],
|
||||
description: 'URL to link to in the user interface'
|
||||
},
|
||||
serviceProviderName: {
|
||||
type: ['string', 'null'],
|
||||
description: 'Name of the service provider to display in the user interface'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'claimVersion',
|
||||
'uri',
|
||||
'proofs',
|
||||
'status',
|
||||
'display'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
||||
|
||||
export const serviceProviderSchema = {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
$id: 'https://spec.keyoxide.org/2/serviceprovider.schema.json',
|
||||
title: 'Service provider',
|
||||
description: 'A service provider that can be matched to identity claims',
|
||||
type: 'object',
|
||||
properties: {
|
||||
about: {
|
||||
description: 'Details about the service provider',
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
description: 'Full name of the service provider',
|
||||
type: 'string'
|
||||
},
|
||||
id: {
|
||||
description: 'Identifier of the service provider (no whitespace or symbols, lowercase)',
|
||||
type: 'string'
|
||||
},
|
||||
homepage: {
|
||||
description: 'URL to the homepage of the service provider',
|
||||
type: ['string', 'null']
|
||||
}
|
||||
}
|
||||
},
|
||||
profile: {
|
||||
description: 'What the profile would look like if the match is correct',
|
||||
type: 'object',
|
||||
properties: {
|
||||
display: {
|
||||
description: 'Profile name to be displayed',
|
||||
type: 'string'
|
||||
},
|
||||
uri: {
|
||||
description: 'URI or URL for public access to the profile',
|
||||
type: 'string'
|
||||
},
|
||||
qr: {
|
||||
description: 'URI or URL associated with the profile usually served as a QR code',
|
||||
type: ['string', 'null']
|
||||
}
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
description: 'Details from the claim matching process',
|
||||
type: 'object',
|
||||
properties: {
|
||||
uriRegularExpression: {
|
||||
description: 'Regular expression used to parse the URI',
|
||||
type: 'string'
|
||||
},
|
||||
uriIsAmbiguous: {
|
||||
description: 'Whether this match automatically excludes other matches',
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
},
|
||||
proof: {
|
||||
description: 'Information for the proof verification process',
|
||||
type: 'object',
|
||||
properties: {
|
||||
request: {
|
||||
description: 'Details to request the potential proof',
|
||||
type: 'object',
|
||||
properties: {
|
||||
uri: {
|
||||
description: 'Location of the proof',
|
||||
type: ['string', 'null']
|
||||
},
|
||||
accessRestriction: {
|
||||
description: 'Type of access restriction [none, nocors, granted, server]',
|
||||
type: 'string'
|
||||
},
|
||||
fetcher: {
|
||||
description: 'Name of the fetcher to use',
|
||||
type: 'string'
|
||||
},
|
||||
data: {
|
||||
description: 'Data needed by the fetcher or proxy to request the proof',
|
||||
type: 'object',
|
||||
additionalProperties: true
|
||||
}
|
||||
}
|
||||
},
|
||||
response: {
|
||||
description: 'Details about the expected response',
|
||||
type: 'object',
|
||||
properties: {
|
||||
format: {
|
||||
description: 'Expected format of the proof [text, json]',
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
target: {
|
||||
description: 'Details about the target located in the response',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
format: {
|
||||
description: 'How is the proof formatted [uri, fingerprint]',
|
||||
type: 'string'
|
||||
},
|
||||
encoding: {
|
||||
description: 'How is the proof encoded [plain, html, xml]',
|
||||
type: 'string'
|
||||
},
|
||||
relation: {
|
||||
description: 'How are the response and the target related [contains, equals]',
|
||||
type: 'string'
|
||||
},
|
||||
path: {
|
||||
description: 'Path to the target location if the response is JSON',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
required: [
|
||||
'about',
|
||||
'profile',
|
||||
'claim',
|
||||
'proof'
|
||||
],
|
||||
additionalProperties: false
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
Copyright (C) 2021 Yarmo Mackenbach
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under
|
||||
the terms of the GNU Affero General Public License as published by the Free
|
||||
Software Foundation, either version 3 of the License, or (at your option)
|
||||
any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License along
|
||||
with this program. If not, see <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/>.
|
||||
*/
|
||||
export default {
|
||||
claimVersion: 1,
|
||||
uri: 'https://fosstodon.org/@keyoxide',
|
||||
fingerprint: '9f0048ac0b23301e1f77e994909f6bd6f80f485d',
|
||||
status: 'verified',
|
||||
matches: [
|
||||
{
|
||||
serviceprovider: {
|
||||
type: 'web',
|
||||
name: 'mastodon (demo)'
|
||||
},
|
||||
match: {
|
||||
regularExpression: {},
|
||||
isAmbiguous: true
|
||||
},
|
||||
profile: {
|
||||
display: '@keyoxide@fosstodon.org',
|
||||
uri: 'https://fosstodon.org/@keyoxide',
|
||||
qr: null
|
||||
},
|
||||
proof: {
|
||||
uri: 'https://fosstodon.org/@keyoxide',
|
||||
request: {
|
||||
fetcher: 'http',
|
||||
access: 0,
|
||||
format: 'json',
|
||||
data: {
|
||||
url: 'https://fosstodon.org/@keyoxide',
|
||||
format: 'json'
|
||||
}
|
||||
}
|
||||
},
|
||||
claim: {
|
||||
format: 1,
|
||||
relation: 0,
|
||||
path: [
|
||||
'attachment',
|
||||
'value'
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
verification: {
|
||||
result: true,
|
||||
completed: true,
|
||||
errors: [],
|
||||
proof: {
|
||||
fetcher: 'http',
|
||||
viaProxy: false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -29,45 +29,48 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
*/
|
||||
import logger from '../log.js'
|
||||
import * as doipjs from 'doipjs'
|
||||
import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './keys.js'
|
||||
import { fetchWKD, fetchHKP, fetchSignature, fetchKeybase } from './openpgpProfiles.js'
|
||||
import libravatar from 'libravatar'
|
||||
|
||||
const generateAspeProfile = async (id) => {
|
||||
logger.debug('Generating an ASPE profile',
|
||||
{ component: 'aspe_profile_generator', action: 'start', profile_id: id })
|
||||
|
||||
return doipjs.asp.fetchASPE(id)
|
||||
.then(profile => {
|
||||
profile.addVerifier('keyoxide', `https://${process.env.DOMAIN}/${id}`)
|
||||
profile = processAspProfile(profile)
|
||||
return profile
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate ASPE profile',
|
||||
{ component: 'aspe_profile_generator', action: 'failure', error: err.message, profile_id: id })
|
||||
|
||||
return {
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const generateWKDProfile = async (id) => {
|
||||
logger.debug('Generating a WKD profile',
|
||||
{ component: 'wkd_profile_generator', action: 'start', profile_id: id })
|
||||
|
||||
return fetchWKD(id)
|
||||
.then(async key => {
|
||||
let keyData = await doipjs.keys.process(key.publicKey)
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
keyData.key.fetchMethod = 'wkd'
|
||||
keyData.key.uri = key.fetchURL
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
keyoxideData.url = `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`
|
||||
.then(async profile => {
|
||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
|
||||
profile = processOpenPgpProfile(profile)
|
||||
|
||||
logger.debug('Generating a WKD profile',
|
||||
{ component: 'wkd_profile_generator', action: 'done', profile_id: id })
|
||||
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
return profile
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate WKD profile',
|
||||
{ component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
|
@ -78,41 +81,27 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
|||
{ component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
return fetchHKP(id, keyserverDomain)
|
||||
.then(async key => {
|
||||
let keyData = await doipjs.keys.process(key.publicKey)
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
keyData.key.fetchMethod = 'hkp'
|
||||
keyData.key.uri = key.fetchURL
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
.then(async profile => {
|
||||
let keyoxideUrl
|
||||
if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
|
||||
keyoxideData.url = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}`
|
||||
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}`
|
||||
} else {
|
||||
keyoxideData.url = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||
keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${keyserverDomain}/${id}`
|
||||
}
|
||||
|
||||
profile.addVerifier('keyoxide', keyoxideUrl)
|
||||
profile = processOpenPgpProfile(profile)
|
||||
|
||||
logger.debug('Generating a HKP profile',
|
||||
{ component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
return profile
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate HKP profile',
|
||||
{ component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
|
@ -121,25 +110,31 @@ const generateHKPProfile = async (id, keyserverDomain) => {
|
|||
const generateAutoProfile = async (id) => {
|
||||
let result
|
||||
|
||||
const aspeRe = /aspe:(.*):(.*)/
|
||||
|
||||
if (aspeRe.test(id)) {
|
||||
result = await generateAspeProfile(id)
|
||||
|
||||
if (result && !('errors' in result)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if (id.includes('@')) {
|
||||
result = await generateWKDProfile(id)
|
||||
|
||||
if (result && result.errors.length === 0) {
|
||||
if (result && !('errors' in result)) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result = await generateHKPProfile(id)
|
||||
if (result && result.errors.length === 0) {
|
||||
if (result && !('errors' in result)) {
|
||||
return result
|
||||
}
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: ['No public keys could be found']
|
||||
errors: ['No public profile/keys could be found']
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,34 +144,19 @@ const generateSignatureProfile = async (signature) => {
|
|||
|
||||
return fetchSignature(signature)
|
||||
.then(async key => {
|
||||
let keyData = key.keyData
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
key.keyData = undefined
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
let profile = await doipjs.signatures.parse(key.publicKey)
|
||||
profile = processOpenPgpProfile(profile)
|
||||
|
||||
logger.debug('Generating a signature profile',
|
||||
{ component: 'signature_profile_generator', action: 'done' })
|
||||
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
return profile
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate a signature profile',
|
||||
{ component: 'signature_profile_generator', action: 'failure', error: err.message })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
|
@ -187,82 +167,91 @@ const generateKeybaseProfile = async (username, fingerprint) => {
|
|||
{ component: 'keybase_profile_generator', action: 'start', username, fingerprint })
|
||||
|
||||
return fetchKeybase(username, fingerprint)
|
||||
.then(async key => {
|
||||
let keyData = await doipjs.keys.process(key.publicKey)
|
||||
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}`
|
||||
keyData.key.fetchMethod = 'hkp'
|
||||
keyData.key.uri = key.fetchURL
|
||||
keyData.key.data = {}
|
||||
keyData = processKeyData(keyData)
|
||||
|
||||
const keyoxideData = {}
|
||||
keyoxideData.url = `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`
|
||||
.then(async profile => {
|
||||
profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
|
||||
profile = processOpenPgpProfile(profile)
|
||||
|
||||
logger.debug('Generating a Keybase profile',
|
||||
{ component: 'keybase_profile_generator', action: 'done', username, fingerprint })
|
||||
|
||||
return {
|
||||
key,
|
||||
keyData,
|
||||
keyoxide: keyoxideData,
|
||||
extra: await computeExtraData(key, keyData),
|
||||
errors: []
|
||||
}
|
||||
return profile
|
||||
})
|
||||
.catch(err => {
|
||||
logger.warn('Failed to generate a Keybase profile',
|
||||
{ component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint })
|
||||
|
||||
return {
|
||||
key: {},
|
||||
keyData: {},
|
||||
keyoxide: {},
|
||||
extra: {},
|
||||
errors: [err.message]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const processKeyData = (keyData) => {
|
||||
keyData.users.forEach(user => {
|
||||
const processAspProfile = async (/** @type {import('doipjs').Profile */ profile) => {
|
||||
profile.personas.forEach(persona => {
|
||||
// Remove faulty claims
|
||||
user.claims = user.claims.filter(claim => {
|
||||
persona.claims = persona.claims.filter(claim => {
|
||||
return claim instanceof doipjs.Claim
|
||||
})
|
||||
|
||||
// Match claims
|
||||
user.claims.forEach(claim => {
|
||||
persona.claims.forEach(claim => {
|
||||
claim.match()
|
||||
})
|
||||
|
||||
// Sort claims
|
||||
user.claims.sort((a, b) => {
|
||||
persona.claims.sort((a, b) => {
|
||||
if (a.matches.length === 0) return 1
|
||||
if (b.matches.length === 0) return -1
|
||||
|
||||
if (a.matches[0].serviceprovider.name < b.matches[0].serviceprovider.name) {
|
||||
if (a.matches[0].about.name < b.matches[0].about.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.matches[0].serviceprovider.name > b.matches[0].serviceprovider.name) {
|
||||
if (a.matches[0].about.name > b.matches[0].about.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
})
|
||||
|
||||
keyData.primaryUserIndex ||= 0
|
||||
// Overwrite avatarUrl
|
||||
// TODO: don't overwrite avatarUrl once it's fully supported
|
||||
profile.personas[profile.primaryPersonaIndex].avatarUrl = `https://api.dicebear.com/6.x/shapes/svg?seed=${profile.publicKey.fingerprint}&size=128`
|
||||
|
||||
return keyData
|
||||
return profile
|
||||
}
|
||||
|
||||
const computeExtraData = async (key, keyData) => {
|
||||
// Get the primary user
|
||||
const primaryUser = await key.publicKey.getPrimaryUser()
|
||||
const processOpenPgpProfile = async (/** @type {import('doipjs').Profile */ profile) => {
|
||||
profile.personas.forEach(persona => {
|
||||
// Remove faulty claims
|
||||
persona.claims = persona.claims.filter(claim => {
|
||||
return claim instanceof doipjs.Claim
|
||||
})
|
||||
|
||||
// Query libravatar to get the avatar url
|
||||
return {
|
||||
avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true })
|
||||
}
|
||||
// Match claims
|
||||
persona.claims.forEach(claim => {
|
||||
claim.match()
|
||||
})
|
||||
|
||||
// Sort claims
|
||||
persona.claims.sort((a, b) => {
|
||||
if (a.matches.length === 0) return 1
|
||||
if (b.matches.length === 0) return -1
|
||||
|
||||
if (a.matches[0].about.name < b.matches[0].about.name) {
|
||||
return -1
|
||||
}
|
||||
if (a.matches[0].about.name > b.matches[0].about.name) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
})
|
||||
|
||||
// Overwrite avatarUrl
|
||||
// TODO: don't overwrite avatarUrl once it's fully supported
|
||||
profile.personas[profile.primaryPersonaIndex].avatarUrl = await libravatar.get_avatar_url({ email: profile.personas[profile.primaryPersonaIndex].email, size: 128, default: 'mm', https: true })
|
||||
|
||||
return profile
|
||||
}
|
||||
|
||||
const getScheme = () => {
|
||||
|
@ -273,6 +262,7 @@ const getScheme = () => {
|
|||
: 'https'
|
||||
}
|
||||
|
||||
export { generateAspeProfile }
|
||||
export { generateWKDProfile }
|
||||
export { generateHKPProfile }
|
||||
export { generateAutoProfile }
|
||||
|
|
|
@ -29,7 +29,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
|
|||
*/
|
||||
import got from 'got'
|
||||
import * as doipjs from 'doipjs'
|
||||
import { readKey, readCleartextMessage, verify, PublicKey } from 'openpgp'
|
||||
import { readKey } from 'openpgp'
|
||||
import { computeWKDLocalPart } from './utils.js'
|
||||
import { createHash } from 'crypto'
|
||||
import Keyv from 'keyv'
|
||||
|
@ -39,10 +39,9 @@ const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
|
|||
const fetchWKD = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
let publicKey = null
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
if (!id.includes('@')) {
|
||||
reject(new Error(`The WKD identifier "${id}" is invalid`))
|
||||
|
@ -59,14 +58,14 @@ const fetchWKD = (id) => {
|
|||
|
||||
const hash = createHash('md5').update(id).digest('hex')
|
||||
if (c && await c.get(hash)) {
|
||||
plaintext = Uint8Array.from((await c.get(hash)).split(','))
|
||||
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
|
||||
}
|
||||
|
||||
if (!plaintext) {
|
||||
if (!profile) {
|
||||
try {
|
||||
plaintext = await got(urlAdvanced).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
output.fetchURL = urlAdvanced
|
||||
fetchURL = urlAdvanced
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
|
@ -76,7 +75,7 @@ const fetchWKD = (id) => {
|
|||
try {
|
||||
plaintext = await got(urlDirect).then((response) => {
|
||||
if (response.statusCode === 200) {
|
||||
output.fetchURL = urlDirect
|
||||
fetchURL = urlDirect
|
||||
return new Uint8Array(response.rawBody)
|
||||
} else {
|
||||
return null
|
||||
|
@ -91,24 +90,29 @@ const fetchWKD = (id) => {
|
|||
reject(new Error('No public keys could be fetched using WKD'))
|
||||
}
|
||||
|
||||
if (c && plaintext instanceof Uint8Array) {
|
||||
await c.set(hash, plaintext.toString(), 60 * 1000)
|
||||
try {
|
||||
publicKey = await readKey({
|
||||
binaryKey: plaintext
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
if (!publicKey) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
profile = await doipjs.openpgp.parsePublicKey(publicKey)
|
||||
profile.publicKey.fetch.method = 'wkd'
|
||||
profile.publicKey.fetch.query = id
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
}
|
||||
|
||||
try {
|
||||
output.publicKey = await readKey({
|
||||
binaryKey: plaintext
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
if (c && plaintext instanceof Uint8Array) {
|
||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be read from the data fetched using WKD'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
@ -116,12 +120,10 @@ const fetchWKD = (id) => {
|
|||
const fetchHKP = (id, keyserverDomain) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
keyserverDomain = keyserverDomain || 'keys.openpgp.org'
|
||||
const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org'
|
||||
|
||||
let query = ''
|
||||
if (id.includes('@')) {
|
||||
|
@ -135,31 +137,36 @@ const fetchHKP = (id, keyserverDomain) => {
|
|||
query = `0x${sanitizedId}`
|
||||
}
|
||||
|
||||
output.fetchURL = `https://${keyserverDomain}/pks/lookup?op=get&options=mr&search=${query}`
|
||||
fetchURL = `https://${keyserverDomainNormalized}/pks/lookup?op=get&options=mr&search=${query}`
|
||||
|
||||
const hash = createHash('md5').update(`${query}__${keyserverDomain}`).digest('hex')
|
||||
const hash = createHash('md5').update(`${query}__${keyserverDomainNormalized}`).digest('hex')
|
||||
|
||||
if (c && await c.get(hash)) {
|
||||
output.publicKey = await readKey({
|
||||
armoredKey: await c.get(hash)
|
||||
})
|
||||
} else {
|
||||
profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
try {
|
||||
output.publicKey = await doipjs.keys.fetchHKP(query, keyserverDomain)
|
||||
profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be fetched using HKP'))
|
||||
profile = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
if (!profile) {
|
||||
reject(new Error('No public keys could be fetched using HKP'))
|
||||
return
|
||||
}
|
||||
|
||||
if (c && output.publicKey instanceof PublicKey) {
|
||||
await c.set(hash, output.publicKey.armor(), 60 * 1000)
|
||||
profile.publicKey.fetch.method = 'hkp'
|
||||
profile.publicKey.fetch.query = id
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
|
||||
if (c && profile instanceof doipjs.Profile) {
|
||||
await c.set(hash, JSON.stringify(profile), 60 * 1000)
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
@ -167,48 +174,22 @@ const fetchHKP = (id, keyserverDomain) => {
|
|||
const fetchSignature = (signature) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null,
|
||||
keyData: null
|
||||
}
|
||||
|
||||
// Check validity of signature
|
||||
let signatureData
|
||||
try {
|
||||
signatureData = await readCleartextMessage({
|
||||
cleartextMessage: signature
|
||||
})
|
||||
} catch (error) {
|
||||
reject(new Error(`Signature could not be properly read (${error.message})`))
|
||||
}
|
||||
let profile = null
|
||||
|
||||
// Process the signature
|
||||
try {
|
||||
output.keyData = await doipjs.signatures.process(signature)
|
||||
output.publicKey = output.keyData.key.data
|
||||
profile = await doipjs.signatures.parse(signature)
|
||||
// TODO Find the URL to the key
|
||||
output.fetchURL = null
|
||||
} catch (error) {
|
||||
reject(new Error(`Signature could not be properly read (${error.message})`))
|
||||
}
|
||||
|
||||
// Check if a key was fetched
|
||||
if (!output.publicKey) {
|
||||
reject(new Error('No public keys could be fetched'))
|
||||
if (!profile) {
|
||||
reject(new Error('No profile could be fetched'))
|
||||
}
|
||||
|
||||
// Check validity of signature
|
||||
const verified = await verify({
|
||||
message: signatureData,
|
||||
verificationKeys: output.publicKey
|
||||
})
|
||||
|
||||
if (!await verified.signatures[0].verified) {
|
||||
reject(new Error('Signature was invalid'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
||||
|
@ -216,23 +197,24 @@ const fetchSignature = (signature) => {
|
|||
const fetchKeybase = (username, fingerprint) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
(async () => {
|
||||
const output = {
|
||||
publicKey: null,
|
||||
fetchURL: null
|
||||
}
|
||||
let profile = null
|
||||
let fetchURL = null
|
||||
|
||||
try {
|
||||
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint)
|
||||
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
||||
profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
|
||||
fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
||||
} catch (error) {
|
||||
reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
if (!output.publicKey) {
|
||||
if (!profile) {
|
||||
reject(new Error('No public keys could be fetched from Keybase'))
|
||||
}
|
||||
|
||||
resolve(output)
|
||||
profile.publicKey.fetch.method = 'http'
|
||||
profile.publicKey.fetch.resolvedUrl = fetchURL
|
||||
|
||||
resolve(profile)
|
||||
})()
|
||||
})
|
||||
}
|
|
@ -39,7 +39,7 @@ export function generatePageTitle (type, data) {
|
|||
switch (type) {
|
||||
case 'profile':
|
||||
try {
|
||||
return `${data.keyData.users[data.keyData.primaryUserIndex].userData.name} - Keyoxide`
|
||||
return `${data.personas[data.primaryPersonaIndex].name} - Keyoxide`
|
||||
} catch (error) {
|
||||
return 'Profile - Keyoxide'
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ export class Claim extends HTMLElement {
|
|||
}
|
||||
|
||||
async verify() {
|
||||
const claim = new doipjs.Claim(JSON.parse(this.getAttribute('data-claim')));
|
||||
const claim = doipjs.Claim.fromJson(JSON.parse(this.getAttribute('data-claim')));
|
||||
await claim.verify({
|
||||
proxy: {
|
||||
policy: 'adaptive',
|
||||
|
@ -58,24 +58,14 @@ export class Claim extends HTMLElement {
|
|||
|
||||
updateContent(value) {
|
||||
const root = this;
|
||||
const claim = new doipjs.Claim(JSON.parse(value));
|
||||
const claim = doipjs.Claim.fromJson(JSON.parse(value));
|
||||
|
||||
switch (claim.matches[0].serviceprovider.name) {
|
||||
case 'dns':
|
||||
case 'xmpp':
|
||||
case 'irc':
|
||||
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name.toUpperCase();
|
||||
break;
|
||||
|
||||
default:
|
||||
root.querySelector('.info .subtitle').innerText = claim.matches[0].serviceprovider.name;
|
||||
break;
|
||||
}
|
||||
root.querySelector('.info .subtitle').innerText = claim.matches[0].about.name;
|
||||
root.querySelector('.info .title').innerText = claim.matches[0].profile.display;
|
||||
|
||||
try {
|
||||
if (claim.status === 'verified') {
|
||||
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.verification.result ? 'success' : 'failed');
|
||||
if (claim.status >= 200) {
|
||||
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.status < 300 ? 'success' : 'failed');
|
||||
} else {
|
||||
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
|
||||
}
|
||||
|
@ -87,7 +77,7 @@ export class Claim extends HTMLElement {
|
|||
elContent.innerHTML = ``;
|
||||
|
||||
// Handle failed ambiguous claim
|
||||
if (claim.status === 'verified' && !claim.verification.result && claim.isAmbiguous()) {
|
||||
if (claim.status >= 300 && claim.isAmbiguous()) {
|
||||
root.querySelector('.info .subtitle').innerText = '---';
|
||||
|
||||
const subsection_alert = elContent.appendChild(document.createElement('div'));
|
||||
|
@ -120,8 +110,8 @@ export class Claim extends HTMLElement {
|
|||
}
|
||||
|
||||
const proof_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||
if (claim.matches[0].proof.uri) {
|
||||
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.uri}" aria-label="link to profile">${claim.matches[0].proof.uri}</a>`;
|
||||
if (claim.matches[0].proof.request.uri) {
|
||||
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.request.uri}" aria-label="link to profile">${claim.matches[0].proof.request.uri}</a>`;
|
||||
} else {
|
||||
proof_link.innerHTML = `Proof link: not accessible from browser`;
|
||||
}
|
||||
|
@ -156,7 +146,7 @@ export class Claim extends HTMLElement {
|
|||
const subsection_status_text = subsection_status.appendChild(document.createElement('div'));
|
||||
|
||||
const verification = subsection_status_text.appendChild(document.createElement('p'));
|
||||
if (claim.status === 'verified') {
|
||||
if (claim.status >= 200) {
|
||||
verification.innerHTML = `Claim verification has completed.`;
|
||||
subsection_status_icon.setAttribute('src', '/static/img/check-decagram.svg');
|
||||
subsection_status_icon.setAttribute('alt', '');
|
||||
|
@ -178,10 +168,10 @@ export class Claim extends HTMLElement {
|
|||
const subsection_result_text = subsection_result.appendChild(document.createElement('div'));
|
||||
|
||||
const result = subsection_result_text.appendChild(document.createElement('p'));
|
||||
result.innerHTML = `The claim <strong>${claim.verification.result ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
|
||||
result.innerHTML = `The claim <strong>${claim.status >= 200 && claim.status < 300 ? 'HAS BEEN' : 'COULD NOT BE'}</strong> verified by the proof.`;
|
||||
|
||||
// Additional info
|
||||
if (claim.verification.proof.viaProxy) {
|
||||
if (claim.status === 201) {
|
||||
elContent.appendChild(document.createElement('hr'));
|
||||
|
||||
const subsection_info = elContent.appendChild(document.createElement('div'));
|
||||
|
|
|
@ -46,7 +46,7 @@ export class Key extends HTMLElement {
|
|||
const root = this;
|
||||
const data = JSON.parse(value);
|
||||
|
||||
root.querySelector('.info .subtitle').innerText = data.key.fetchMethod;
|
||||
root.querySelector('.info .subtitle').innerText = `${data.keyType} / ${data.fetch.method}`;
|
||||
root.querySelector('.info .title').innerText = data.fingerprint;
|
||||
|
||||
const elContent = root.querySelector('.content');
|
||||
|
@ -62,22 +62,24 @@ export class Key extends HTMLElement {
|
|||
const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
|
||||
|
||||
const profile_link = subsection_links_text.appendChild(document.createElement('p'));
|
||||
profile_link.innerHTML = `Key link: <a class="u-key" rel="pgpkey" href="${data.key.uri}" aria-label="Link to cryptographic key">${data.key.uri}</a>`;
|
||||
profile_link.innerHTML = `Key link: <a class="u-key" rel="pgpkey" href="${data.fetch.resolvedUrl}" aria-label="Link to cryptographic key">${data.fetch.resolvedUrl}</a>`;
|
||||
|
||||
elContent.appendChild(document.createElement('hr'));
|
||||
|
||||
// QR Code
|
||||
const subsection_qr = elContent.appendChild(document.createElement('div'));
|
||||
subsection_qr.setAttribute('class', 'subsection');
|
||||
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
|
||||
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.svg');
|
||||
subsection_qr_icon.setAttribute('alt', '');
|
||||
subsection_qr_icon.setAttribute('aria-hidden', 'true');
|
||||
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
|
||||
|
||||
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
|
||||
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
|
||||
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
|
||||
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
|
||||
if (data.keyType === 'openpgp') {
|
||||
elContent.appendChild(document.createElement('hr'));
|
||||
|
||||
// QR Code
|
||||
const subsection_qr = elContent.appendChild(document.createElement('div'));
|
||||
subsection_qr.setAttribute('class', 'subsection');
|
||||
const subsection_qr_icon = subsection_qr.appendChild(document.createElement('img'));
|
||||
subsection_qr_icon.setAttribute('src', '/static/img/qrcode.svg');
|
||||
subsection_qr_icon.setAttribute('alt', '');
|
||||
subsection_qr_icon.setAttribute('aria-hidden', 'true');
|
||||
const subsection_qr_text = subsection_qr.appendChild(document.createElement('div'));
|
||||
|
||||
const button_fingerprintQR = subsection_qr_text.appendChild(document.createElement('button'));
|
||||
button_fingerprintQR.innerText = `Show OpenPGP fingerprint QR`;
|
||||
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
|
||||
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,33 +75,22 @@ export async function generateProfileURL(data) {
|
|||
|
||||
// Fetch OpenPGP key based on information stored in window
|
||||
export async function fetchProfileKey() {
|
||||
if (window.kx.key.object && window.kx.key.object instanceof openpgp.PublicKey) {
|
||||
if (window.kx.publicKey.key && window.kx.publicKey.key instanceof openpgp.PublicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rawKeyData = await fetch(window.kx.key.url)
|
||||
let key, errorMsg
|
||||
|
||||
try {
|
||||
key = (await openpgp.readKey({
|
||||
binaryKey: new Uint8Array(await rawKeyData.clone().arrayBuffer())
|
||||
armoredKey: window.kx.publicKey.encodedKey
|
||||
}))
|
||||
} catch(error) {
|
||||
} catch (error) {
|
||||
errorMsg = error.message
|
||||
}
|
||||
|
||||
if (!key) {
|
||||
try {
|
||||
key = (await openpgp.readKey({
|
||||
armoredKey: await rawKeyData.clone().text()
|
||||
}))
|
||||
} catch (error) {
|
||||
errorMsg = error.message
|
||||
}
|
||||
}
|
||||
|
||||
if (key) {
|
||||
window.kx.key.object = key
|
||||
window.kx.publicKey.key = key
|
||||
return
|
||||
} else {
|
||||
throw new Error(`Public key could not be fetched (${errorMsg})`)
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
extends templates/base.pug
|
||||
|
||||
mixin generateUser(user, isPrimary)
|
||||
mixin generatePersona(persona, isPrimary)
|
||||
h2
|
||||
span.p-email #{user.userData.email}
|
||||
if persona.email
|
||||
span.p-email Identity claims (#{persona.email})
|
||||
else
|
||||
span.p-email Identity claims
|
||||
if isPrimary
|
||||
small.primary primary
|
||||
if user.userData.comment
|
||||
span.p-comment ⓘ #{user.userData.comment}
|
||||
each claim in user.claims
|
||||
if persona.description
|
||||
span.p-comment ⓘ #{persona.description}
|
||||
each claim in persona.claims
|
||||
if claim.matches.length > 0
|
||||
kx-claim.kx-item(data-claim=claim)
|
||||
details(aria-label="Claim")
|
||||
summary
|
||||
.info
|
||||
p.subtitle= claim.matches[0].serviceprovider.name
|
||||
p.title= claim.matches[0].profile.display
|
||||
p.subtitle= claim.display.serviceproviderName
|
||||
p.title= claim.display.name
|
||||
.icons
|
||||
.verificationStatus(data-value='running')
|
||||
.inProgress
|
||||
|
@ -22,12 +25,12 @@ mixin generateUser(user, isPrimary)
|
|||
.subsection
|
||||
img(src='/static/img/link.png')
|
||||
div
|
||||
if (claim.matches[0].profile.uri)
|
||||
if (claim.display.url)
|
||||
p Profile link:
|
||||
a(rel='me' href=claim.matches[0].profile.uri aria-label="Link to profile")= claim.matches[0].profile.uri
|
||||
a(rel='me' href=claim.display.url aria-label="Link to profile")= claim.display.url
|
||||
else
|
||||
p Profile link: not accessible from browser
|
||||
if (claim.matches[0].proof.uri)
|
||||
if (claim.matches.length === 1 && claim.matches[0].proof.uri)
|
||||
p Proof link:
|
||||
a(href=claim.matches[0].proof.uri aria-label="Link to proof")= claim.matches[0].proof.uri
|
||||
else
|
||||
|
@ -36,10 +39,7 @@ mixin generateUser(user, isPrimary)
|
|||
block content
|
||||
script.
|
||||
kx = {
|
||||
key: {
|
||||
url: "!{data && data.key && data.key.fetchURL ? data.key.fetchURL : null}",
|
||||
object: null
|
||||
}
|
||||
publicKey: !{JSON.stringify(data.publicKey)}
|
||||
}
|
||||
|
||||
if (data && 'errors' in data && data.errors.length > 0)
|
||||
|
@ -80,12 +80,7 @@ block content
|
|||
dialog#dialog--verifySignature
|
||||
div
|
||||
form(method='post')
|
||||
label(for="sigVerInput") Signature
|
||||
textarea#sigVerInput.input(name='signature')
|
||||
input.no-margin(type='submit' name='submit' value='VERIFY SIGNATURE')
|
||||
br
|
||||
br
|
||||
label(for="sigVerOutput") Verification result
|
||||
label(for="sigVerInput") Signature name
|
||||
textarea#sigVerOutput.output(name='message' placeholder='Waiting for input' readonly)
|
||||
form(method="dialog")
|
||||
input(type="submit" value="Close")
|
||||
|
@ -108,9 +103,9 @@ block content
|
|||
|
||||
unless (isSignature && !signature)
|
||||
#profileHeader.card.card--transparent.card--profileHeader
|
||||
img#profileAvatar.u-logo(src=data.extra.avatarURL alt="avatar")
|
||||
img#profileAvatar.u-logo(src=data.personas[data.primaryPersonaIndex].avatarUrl alt="avatar")
|
||||
|
||||
p#profileName.p-name= data.keyData.users[data.keyData.primaryUserIndex].userData.name
|
||||
p#profileName.p-name= data.personas[data.primaryPersonaIndex].name
|
||||
|
||||
if (enable_message_encryption || enable_signature_verification)
|
||||
.button-wrapper
|
||||
|
@ -119,27 +114,28 @@ block content
|
|||
if (enable_signature_verification)
|
||||
button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature
|
||||
|
||||
+generateUser(data.keyData.users[data.keyData.primaryUserIndex], true)
|
||||
each user, index in data.keyData.users
|
||||
unless index == data.keyData.primaryUserIndex
|
||||
+generateUser(user, false)
|
||||
+generatePersona(data.personas[data.primaryPersonaIndex], true && data.personas.length > 1)
|
||||
each persona, index in data.personas
|
||||
unless index == data.primaryPersonaIndex
|
||||
+generatePersona(persona, false)
|
||||
|
||||
#profileProofs.card.card--transparent
|
||||
h2 Key
|
||||
kx-key.kx-item(data-keydata=data.keyData)
|
||||
kx-key.kx-item(data-keydata=data.publicKey)
|
||||
details(aria-label="Key")
|
||||
summary
|
||||
.info
|
||||
p.subtitle= data.keyData.key.fetchMethod
|
||||
p.title= data.keyData.fingerprint
|
||||
p.subtitle= data.publicKey.fetch.method
|
||||
p.title= data.identifier
|
||||
.content
|
||||
.subsection
|
||||
img(src='/static/img/link.png')
|
||||
div
|
||||
p Key link:
|
||||
a.u-key(href=data.keyData.key.uri rel="pgpkey" aria-label="Link to cryptographic key")= data.keyData.key.uri
|
||||
a.u-key(href=data.publicKey.fetch.resolvedUrl rel="pgpkey" aria-label="Link to cryptographic key")= data.publicKey.fetch.resolvedUrl
|
||||
hr
|
||||
.subsection
|
||||
img(src='/static/img/qrcode.png')
|
||||
div
|
||||
button(onClick=`showQR('${data.keyData.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR
|
||||
if (data.profileType === 'openpgp')
|
||||
.subsection
|
||||
img(src='/static/img/qrcode.png')
|
||||
div
|
||||
button(onClick=`showQR('${data.publicKey.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR
|
|
@ -18,4 +18,6 @@ html(lang='en')
|
|||
|
||||
link(rel='stylesheet' href='/static/main.css')
|
||||
script(type='application/javascript' defer src='/static/openpgp.js' charset='utf-8')
|
||||
script(type='application/javascript' defer src='/static/doipFetchers.js' charset='utf-8')
|
||||
script(type='application/javascript' defer src='/static/doip.js' charset='utf-8')
|
||||
script(type='application/javascript' defer src='/static/main.js' charset='utf-8')
|
|
@ -13,10 +13,8 @@ export default (env) => {
|
|||
mode: env.mode,
|
||||
entry: {
|
||||
main: {
|
||||
import: './static-src/index.js',
|
||||
dependOn: 'openpgp',
|
||||
},
|
||||
openpgp: './node_modules/openpgp/dist/openpgp.js',
|
||||
import: './static-src/index.js'
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
|
@ -62,12 +60,19 @@ export default (env) => {
|
|||
new CopyPlugin({
|
||||
patterns: [
|
||||
{ from: './static-src/files/', to: '../static/' },
|
||||
{ from: './node_modules/openpgp/dist/openpgp.js', to: '../static/openpgp.js' },
|
||||
{ from: './node_modules/doipjs/dist/doip.core.js', to: '../static/doip.js' },
|
||||
{ from: './node_modules/doipjs/dist/doip.fetchers.minimal.js', to: '../static/doipFetchers.js' },
|
||||
],
|
||||
options: {
|
||||
concurrency: 10,
|
||||
},
|
||||
}),
|
||||
],
|
||||
externals: {
|
||||
doipjs: 'doip',
|
||||
openpgp: 'openpgp'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return {}
|
||||
|
|
Loading…
Reference in a new issue