Merge branch 'dev' into configurable-scheme initial merge conflict

resolution

Tests not updated yet
This commit is contained in:
Preston Maness 2023-07-14 17:03:30 -05:00
commit 0b9a00c69e
22 changed files with 1587 additions and 1928 deletions

View file

@ -9,7 +9,7 @@
"bent": "^7.3.12", "bent": "^7.3.12",
"body-parser": "^1.19.0", "body-parser": "^1.19.0",
"dialog-polyfill": "^0.5.6", "dialog-polyfill": "^0.5.6",
"doipjs": "^0.18.3", "doipjs": "^1.0.0",
"dotenv": "^16.0.3", "dotenv": "^16.0.3",
"express": "^4.17.1", "express": "^4.17.1",
"express-validator": "^6.13.0", "express-validator": "^6.13.0",
@ -28,14 +28,14 @@
"devDependencies": { "devDependencies": {
"@vercel/ncc": "^0.34.0", "@vercel/ncc": "^0.34.0",
"chai": "^4.3.6", "chai": "^4.3.6",
"copy-webpack-plugin": "^10.2.4", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.6.0", "css-loader": "^6.6.0",
"esmock": "^2.3.1", "esmock": "^2.3.1",
"license-check-and-add": "^4.0.5", "license-check-and-add": "^4.0.5",
"mini-css-extract-plugin": "^2.5.3", "mini-css-extract-plugin": "^2.5.3",
"mocha": "^10.1.0", "mocha": "^10.1.0",
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"rome": "^11.0.0", "rome": "^12.1",
"standard": "^17.0.0", "standard": "^17.0.0",
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"webpack": "^5.75.0", "webpack": "^5.75.0",
@ -46,11 +46,13 @@
"start": "node --experimental-fetch ./", "start": "node --experimental-fetch ./",
"dev": "LOG_LEVEL=debug yarn run watch & yarn run build:static:dev", "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 standard:check && yarn run rome:check && mocha --loader=esmock",
"test": "yarn run lint && mocha --loader=esmock",
"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 -o dist", "build:server": "ncc build ./src/index.js -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",
"lint": "yarn run standard:check && yarn run rome:check",
"standard:check": "./node_modules/.bin/standard ./src", "standard:check": "./node_modules/.bin/standard ./src",
"standard:fix": "./node_modules/.bin/standard --fix ./src", "standard:fix": "./node_modules/.bin/standard --fix ./src",
"rome:check": "./node_modules/.bin/rome check ./src", "rome:check": "./node_modules/.bin/rome check ./src",

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 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 the terms of the GNU Affero General Public License as published by the Free

View 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

View file

@ -51,9 +51,6 @@ const opts = {
telegram: { telegram: {
token: process.env.TELEGRAM_TOKEN || null token: process.env.TELEGRAM_TOKEN || null
}, },
twitter: {
bearerToken: process.env.TWITTER_BEARER_TOKEN || null
},
xmpp: { xmpp: {
service: process.env.XMPP_SERVICE || null, service: process.env.XMPP_SERVICE || null,
username: process.env.XMPP_USERNAME || 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 // Matrix route
router.get( router.get(
'/matrix', '/matrix',

View file

@ -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/>. more information on this, and how to apply and follow the GNU AGPL, see <https://www.gnu.org/licenses/>.
*/ */
import express from 'express' import express from 'express'
import apiRouter0 from '../api/v0/index.js' import apiRouter3 from '../api/v3/index.js'
import apiRouter1 from '../api/v1/index.js'
import apiRouter2 from '../api/v2/index.js'
const router = express.Router() const router = express.Router()
if ((process.env.ENABLE_MAIN_MODULE ?? 'true') === 'true') { router.get('/0', (req, res) => {
router.use('/0', apiRouter0) return res.status(501).send('Proxy v0 API endpoint is no longer supported, please migrate to proxy v3 API endpoint')
} })
router.use('/1', apiRouter1) router.get('/1', (req, res) => {
router.use('/2', apiRouter2) 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 export default router

View file

@ -30,7 +30,6 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import express from 'express' import express from 'express'
import markdownImport from 'markdown-it' import markdownImport from 'markdown-it'
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
import demoData from '../server/demo.js'
const router = express.Router() const router = express.Router()
const md = markdownImport({ typographer: true }) 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) => { router.get('/privacy', (req, res) => {

View file

@ -30,6 +30,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
import express from 'express' import express from 'express'
import bodyParserImport from 'body-parser' import bodyParserImport from 'body-parser'
import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js' import { generateSignatureProfile, utils, generateWKDProfile, generateHKPProfile, generateAutoProfile, generateKeybaseProfile } from '../server/index.js'
import { Profile } from 'doipjs'
const router = express.Router() const router = express.Router()
const bodyParser = bodyParserImport.urlencoded({ extended: false }) const bodyParser = bodyParserImport.urlencoded({ extended: false })
@ -41,10 +42,10 @@ router.get('/sig', (req, res) => {
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.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
isSignature: true, isSignature: true,
signature: req.body.signature, signature: req.body.signature,
enable_message_encryption: false, enable_message_encryption: false,
@ -55,10 +56,10 @@ router.post('/sig', bodyParser, async (req, res) => {
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.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })
@ -67,10 +68,10 @@ router.get('/wkd/:id', async (req, res) => {
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.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })
@ -79,10 +80,10 @@ router.get('/hkp/:id', async (req, res) => {
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.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: 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) => { 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.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })
@ -103,10 +104,10 @@ router.get('/keybase/:username/:fingerprint', async (req, res) => {
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.identifier)
res.render('profile', { res.render('profile', {
title, title,
data, data: data instanceof Profile ? data.toJSON() : data,
enable_message_encryption: false, enable_message_encryption: false,
enable_signature_verification: false enable_signature_verification: false
}) })

375
src/schemas.js Normal file
View 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
}

View file

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

View file

@ -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 logger from '../log.js'
import * as doipjs from 'doipjs' 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' 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) => { const generateWKDProfile = async (id) => {
logger.debug('Generating a WKD profile', logger.debug('Generating a WKD profile',
{ component: 'wkd_profile_generator', action: 'start', profile_id: id }) { component: 'wkd_profile_generator', action: 'start', profile_id: id })
return fetchWKD(id) return fetchWKD(id)
.then(async key => { .then(async profile => {
let keyData = await doipjs.keys.process(key.publicKey) profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/wkd/${id}`)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}` profile = processOpenPgpProfile(profile)
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}`
logger.debug('Generating a WKD profile', logger.debug('Generating a WKD profile',
{ component: 'wkd_profile_generator', action: 'done', profile_id: id }) { component: 'wkd_profile_generator', action: 'done', profile_id: id })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate WKD profile', logger.warn('Failed to generate WKD profile',
{ component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id }) { component: 'wkd_profile_generator', action: 'failure', error: err.message, profile_id: id })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
@ -78,41 +81,27 @@ const generateHKPProfile = async (id, keyserverDomain) => {
{ component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' }) { component: 'hkp_profile_generator', action: 'start', profile_id: id, keyserver_domain: keyserverDomain || '' })
return fetchHKP(id, keyserverDomain) return fetchHKP(id, keyserverDomain)
.then(async key => { .then(async profile => {
let keyData = await doipjs.keys.process(key.publicKey) let keyoxideUrl
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') { if (!keyserverDomain || keyserverDomain === 'keys.openpgp.org') {
keyoxideData.url = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}` keyoxideUrl = `${getScheme()}://${process.env.DOMAIN}/hkp/${id}`
} else { } 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', logger.debug('Generating a HKP profile',
{ component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' }) { component: 'hkp_profile_generator', action: 'done', profile_id: id, keyserver_domain: keyserverDomain || '' })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate HKP profile', logger.warn('Failed to generate HKP profile',
{ component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' }) { component: 'hkp_profile_generator', action: 'failure', error: err.message, profile_id: id, keyserver_domain: keyserverDomain || '' })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
@ -121,25 +110,31 @@ const generateHKPProfile = async (id, keyserverDomain) => {
const generateAutoProfile = async (id) => { const generateAutoProfile = async (id) => {
let result let result
const aspeRe = /aspe:(.*):(.*)/
if (aspeRe.test(id)) {
result = await generateAspeProfile(id)
if (result && !('errors' in result)) {
return result
}
}
if (id.includes('@')) { if (id.includes('@')) {
result = await generateWKDProfile(id) result = await generateWKDProfile(id)
if (result && result.errors.length === 0) { if (result && !('errors' in result)) {
return result return result
} }
} }
result = await generateHKPProfile(id) result = await generateHKPProfile(id)
if (result && result.errors.length === 0) { if (result && !('errors' in result)) {
return result return result
} }
return { return {
key: {}, errors: ['No public profile/keys could be found']
keyData: {},
keyoxide: {},
extra: {},
errors: ['No public keys could be found']
} }
} }
@ -149,34 +144,19 @@ const generateSignatureProfile = async (signature) => {
return fetchSignature(signature) return fetchSignature(signature)
.then(async key => { .then(async key => {
let keyData = key.keyData let profile = await doipjs.signatures.parse(key.publicKey)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}` profile = processOpenPgpProfile(profile)
key.keyData = undefined
keyData.key.data = {}
keyData = processKeyData(keyData)
const keyoxideData = {}
logger.debug('Generating a signature profile', logger.debug('Generating a signature profile',
{ component: 'signature_profile_generator', action: 'done' }) { component: 'signature_profile_generator', action: 'done' })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate a signature profile', logger.warn('Failed to generate a signature profile',
{ component: 'signature_profile_generator', action: 'failure', error: err.message }) { component: 'signature_profile_generator', action: 'failure', error: err.message })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
@ -187,82 +167,91 @@ const generateKeybaseProfile = async (username, fingerprint) => {
{ component: 'keybase_profile_generator', action: 'start', username, fingerprint }) { component: 'keybase_profile_generator', action: 'start', username, fingerprint })
return fetchKeybase(username, fingerprint) return fetchKeybase(username, fingerprint)
.then(async key => { .then(async profile => {
let keyData = await doipjs.keys.process(key.publicKey) profile.addVerifier('keyoxide', `${getScheme()}://${process.env.DOMAIN}/keybase/${username}/${fingerprint}`)
keyData.openpgp4fpr = `openpgp4fpr:${keyData.fingerprint.toLowerCase()}` profile = processOpenPgpProfile(profile)
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}`
logger.debug('Generating a Keybase profile', logger.debug('Generating a Keybase profile',
{ component: 'keybase_profile_generator', action: 'done', username, fingerprint }) { component: 'keybase_profile_generator', action: 'done', username, fingerprint })
return { return profile
key,
keyData,
keyoxide: keyoxideData,
extra: await computeExtraData(key, keyData),
errors: []
}
}) })
.catch(err => { .catch(err => {
logger.warn('Failed to generate a Keybase profile', logger.warn('Failed to generate a Keybase profile',
{ component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint }) { component: 'keybase_profile_generator', action: 'failure', error: err.message, username, fingerprint })
return { return {
key: {},
keyData: {},
keyoxide: {},
extra: {},
errors: [err.message] errors: [err.message]
} }
}) })
} }
const processKeyData = (keyData) => { const processAspProfile = async (/** @type {import('doipjs').Profile */ profile) => {
keyData.users.forEach(user => { profile.personas.forEach(persona => {
// Remove faulty claims // Remove faulty claims
user.claims = user.claims.filter(claim => { persona.claims = persona.claims.filter(claim => {
return claim instanceof doipjs.Claim return claim instanceof doipjs.Claim
}) })
// Match claims // Match claims
user.claims.forEach(claim => { persona.claims.forEach(claim => {
claim.match() claim.match()
}) })
// Sort claims // Sort claims
user.claims.sort((a, b) => { persona.claims.sort((a, b) => {
if (a.matches.length === 0) return 1 if (a.matches.length === 0) return 1
if (b.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 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 1
} }
return 0 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) => { const processOpenPgpProfile = async (/** @type {import('doipjs').Profile */ profile) => {
// Get the primary user profile.personas.forEach(persona => {
const primaryUser = await key.publicKey.getPrimaryUser() // Remove faulty claims
persona.claims = persona.claims.filter(claim => {
return claim instanceof doipjs.Claim
})
// Query libravatar to get the avatar url // Match claims
return { persona.claims.forEach(claim => {
avatarURL: await libravatar.get_avatar_url({ email: primaryUser.user.userID.email, size: 128, default: 'mm', https: true }) 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 = () => { const getScheme = () => {
@ -273,6 +262,7 @@ const getScheme = () => {
: 'https' : 'https'
} }
export { generateAspeProfile }
export { generateWKDProfile } export { generateWKDProfile }
export { generateHKPProfile } export { generateHKPProfile }
export { generateAutoProfile } export { generateAutoProfile }

View file

@ -29,7 +29,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/ */
import got from 'got' import got from 'got'
import * as doipjs from 'doipjs' import * as doipjs from 'doipjs'
import { readKey, readCleartextMessage, verify, PublicKey } from 'openpgp' import { readKey } from 'openpgp'
import { computeWKDLocalPart } from './utils.js' import { computeWKDLocalPart } from './utils.js'
import { createHash } from 'crypto' import { createHash } from 'crypto'
import Keyv from 'keyv' import Keyv from 'keyv'
@ -39,10 +39,9 @@ const c = process.env.ENABLE_EXPERIMENTAL_CACHE ? new Keyv() : null
const fetchWKD = (id) => { const fetchWKD = (id) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let publicKey = null
publicKey: null, let profile = null
fetchURL: null let 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`))
@ -59,14 +58,14 @@ const fetchWKD = (id) => {
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(',')) profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
} }
if (!plaintext) { if (!profile) {
try { try {
plaintext = await got(urlAdvanced).then((response) => { plaintext = await got(urlAdvanced).then((response) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
output.fetchURL = urlAdvanced fetchURL = urlAdvanced
return new Uint8Array(response.rawBody) return new Uint8Array(response.rawBody)
} else { } else {
return null return null
@ -76,7 +75,7 @@ const fetchWKD = (id) => {
try { try {
plaintext = await got(urlDirect).then((response) => { plaintext = await got(urlDirect).then((response) => {
if (response.statusCode === 200) { if (response.statusCode === 200) {
output.fetchURL = urlDirect fetchURL = urlDirect
return new Uint8Array(response.rawBody) return new Uint8Array(response.rawBody)
} else { } else {
return null return null
@ -91,24 +90,29 @@ const fetchWKD = (id) => {
reject(new Error('No public keys could be fetched using WKD')) 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 { try {
output.publicKey = await readKey({ publicKey = await readKey({
binaryKey: plaintext binaryKey: plaintext
}) })
} catch (error) { } catch (error) {
reject(new Error('No public keys could be read from the data fetched using WKD')) reject(new Error('No public keys could be read from the data fetched using WKD'))
} }
if (!output.publicKey) { if (!publicKey) {
reject(new Error('No public keys could be read from the data fetched using WKD')) reject(new Error('No public keys could be read from the data fetched using WKD'))
} }
resolve(output) profile = await doipjs.openpgp.parsePublicKey(publicKey)
profile.publicKey.fetch.method = 'wkd'
profile.publicKey.fetch.query = id
profile.publicKey.fetch.resolvedUrl = fetchURL
}
if (c && plaintext instanceof Uint8Array) {
await c.set(hash, JSON.stringify(profile), 60 * 1000)
}
resolve(profile)
})() })()
}) })
} }
@ -116,12 +120,10 @@ const fetchWKD = (id) => {
const fetchHKP = (id, keyserverDomain) => { const fetchHKP = (id, keyserverDomain) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let profile = null
publicKey: null, let fetchURL = null
fetchURL: null
}
keyserverDomain = keyserverDomain || 'keys.openpgp.org' const keyserverDomainNormalized = keyserverDomain || 'keys.openpgp.org'
let query = '' let query = ''
if (id.includes('@')) { if (id.includes('@')) {
@ -135,31 +137,36 @@ const fetchHKP = (id, keyserverDomain) => {
query = `0x${sanitizedId}` 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)) { if (c && await c.get(hash)) {
output.publicKey = await readKey({ profile = doipjs.Claim.fromJson(JSON.parse(await c.get(hash)))
armoredKey: await c.get(hash) }
})
} else { if (!profile) {
try { try {
output.publicKey = await doipjs.keys.fetchHKP(query, keyserverDomain) profile = await doipjs.openpgp.fetchHKP(query, keyserverDomainNormalized)
} catch (error) { } catch (error) {
profile = null
}
}
if (!profile) {
reject(new Error('No public keys could be fetched using HKP')) reject(new Error('No public keys could be fetched using HKP'))
} return
} }
if (!output.publicKey) { profile.publicKey.fetch.method = 'hkp'
reject(new Error('No public keys could be fetched using 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)
} }
if (c && output.publicKey instanceof PublicKey) { resolve(profile)
await c.set(hash, output.publicKey.armor(), 60 * 1000)
}
resolve(output)
})() })()
}) })
} }
@ -167,48 +174,22 @@ const fetchHKP = (id, keyserverDomain) => {
const fetchSignature = (signature) => { const fetchSignature = (signature) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let profile = null
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})`))
}
// Process the signature // Process the signature
try { try {
output.keyData = await doipjs.signatures.process(signature) profile = await doipjs.signatures.parse(signature)
output.publicKey = output.keyData.key.data
// TODO Find the URL to the key // TODO Find the URL to the key
output.fetchURL = null
} catch (error) { } catch (error) {
reject(new Error(`Signature could not be properly read (${error.message})`)) reject(new Error(`Signature could not be properly read (${error.message})`))
} }
// Check if a key was fetched // Check if a key was fetched
if (!output.publicKey) { if (!profile) {
reject(new Error('No public keys could be fetched')) reject(new Error('No profile could be fetched'))
} }
// Check validity of signature resolve(profile)
const verified = await verify({
message: signatureData,
verificationKeys: output.publicKey
})
if (!await verified.signatures[0].verified) {
reject(new Error('Signature was invalid'))
}
resolve(output)
})() })()
}) })
} }
@ -216,23 +197,24 @@ const fetchSignature = (signature) => {
const fetchKeybase = (username, fingerprint) => { const fetchKeybase = (username, fingerprint) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
(async () => { (async () => {
const output = { let profile = null
publicKey: null, let fetchURL = null
fetchURL: null
}
try { try {
output.publicKey = await doipjs.keys.fetchKeybase(username, fingerprint) profile = await doipjs.openpgp.fetchKeybase(username, fingerprint)
output.fetchURL = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}` 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 (!profile) {
reject(new Error('No public keys could be fetched from Keybase')) 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)
})() })()
}) })
} }

View file

@ -39,7 +39,7 @@ 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.personas[data.primaryPersonaIndex].name} - Keyoxide`
} catch (error) { } catch (error) {
return 'Profile - Keyoxide' return 'Profile - Keyoxide'
} }

View file

@ -45,7 +45,7 @@ export class Claim extends HTMLElement {
} }
async verify() { 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({ await claim.verify({
proxy: { proxy: {
policy: 'adaptive', policy: 'adaptive',
@ -58,24 +58,14 @@ export class Claim extends HTMLElement {
updateContent(value) { updateContent(value) {
const root = this; 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) { root.querySelector('.info .subtitle').innerText = claim.matches[0].about.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 .title').innerText = claim.matches[0].profile.display; root.querySelector('.info .title').innerText = claim.matches[0].profile.display;
try { try {
if (claim.status === 'verified') { if (claim.status >= 200) {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.verification.result ? 'success' : 'failed'); root.querySelector('.icons .verificationStatus').setAttribute('data-value', claim.status < 300 ? 'success' : 'failed');
} else { } else {
root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running'); root.querySelector('.icons .verificationStatus').setAttribute('data-value', 'running');
} }
@ -87,7 +77,7 @@ export class Claim extends HTMLElement {
elContent.innerHTML = ``; elContent.innerHTML = ``;
// Handle failed ambiguous claim // Handle failed ambiguous claim
if (claim.status === 'verified' && !claim.verification.result && claim.isAmbiguous()) { if (claim.status >= 300 && claim.isAmbiguous()) {
root.querySelector('.info .subtitle').innerText = '---'; root.querySelector('.info .subtitle').innerText = '---';
const subsection_alert = elContent.appendChild(document.createElement('div')); 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')); const proof_link = subsection_links_text.appendChild(document.createElement('p'));
if (claim.matches[0].proof.uri) { if (claim.matches[0].proof.request.uri) {
proof_link.innerHTML = `Proof link: <a href="${claim.matches[0].proof.uri}" aria-label="link to profile">${claim.matches[0].proof.uri}</a>`; 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 { } else {
proof_link.innerHTML = `Proof link: not accessible from browser`; 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 subsection_status_text = subsection_status.appendChild(document.createElement('div'));
const verification = subsection_status_text.appendChild(document.createElement('p')); const verification = subsection_status_text.appendChild(document.createElement('p'));
if (claim.status === 'verified') { if (claim.status >= 200) {
verification.innerHTML = `Claim verification has completed.`; verification.innerHTML = `Claim verification has completed.`;
subsection_status_icon.setAttribute('src', '/static/img/check-decagram.svg'); subsection_status_icon.setAttribute('src', '/static/img/check-decagram.svg');
subsection_status_icon.setAttribute('alt', ''); 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 subsection_result_text = subsection_result.appendChild(document.createElement('div'));
const result = subsection_result_text.appendChild(document.createElement('p')); 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 // Additional info
if (claim.verification.proof.viaProxy) { if (claim.status === 201) {
elContent.appendChild(document.createElement('hr')); elContent.appendChild(document.createElement('hr'));
const subsection_info = elContent.appendChild(document.createElement('div')); const subsection_info = elContent.appendChild(document.createElement('div'));

View file

@ -46,7 +46,7 @@ export class Key extends HTMLElement {
const root = this; const root = this;
const data = JSON.parse(value); 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; root.querySelector('.info .title').innerText = data.fingerprint;
const elContent = root.querySelector('.content'); const elContent = root.querySelector('.content');
@ -62,8 +62,9 @@ export class Key extends HTMLElement {
const subsection_links_text = subsection_links.appendChild(document.createElement('div')); const subsection_links_text = subsection_links.appendChild(document.createElement('div'));
const profile_link = subsection_links_text.appendChild(document.createElement('p')); 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>`;
if (data.keyType === 'openpgp') {
elContent.appendChild(document.createElement('hr')); elContent.appendChild(document.createElement('hr'));
// QR Code // QR Code
@ -80,4 +81,5 @@ export class Key extends HTMLElement {
button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`); button_fingerprintQR.setAttribute('onClick', `window.showQR('${data.fingerprint}', 'fingerprint')`);
button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`); button_fingerprintQR.setAttribute('aria-label', `Show QR code for cryptographic fingerprint`);
} }
}
} }

View file

@ -75,33 +75,22 @@ export async function generateProfileURL(data) {
// Fetch OpenPGP key based on information stored in window // Fetch OpenPGP key based on information stored in window
export async function fetchProfileKey() { 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; return;
} }
const rawKeyData = await fetch(window.kx.key.url)
let key, errorMsg let key, errorMsg
try { try {
key = (await openpgp.readKey({ key = (await openpgp.readKey({
binaryKey: new Uint8Array(await rawKeyData.clone().arrayBuffer()) armoredKey: window.kx.publicKey.encodedKey
}))
} catch(error) {
errorMsg = error.message
}
if (!key) {
try {
key = (await openpgp.readKey({
armoredKey: await rawKeyData.clone().text()
})) }))
} catch (error) { } catch (error) {
errorMsg = error.message errorMsg = error.message
} }
}
if (key) { if (key) {
window.kx.key.object = key window.kx.publicKey.key = key
return return
} else { } else {
throw new Error(`Public key could not be fetched (${errorMsg})`) throw new Error(`Public key could not be fetched (${errorMsg})`)

View file

@ -1,20 +1,23 @@
extends templates/base.pug extends templates/base.pug
mixin generateUser(user, isPrimary) mixin generatePersona(persona, isPrimary)
h2 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 if isPrimary
small.primary primary small.primary primary
if user.userData.comment if persona.description
span.p-comment &#9432; #{user.userData.comment} span.p-comment &#9432; #{persona.description}
each claim in user.claims each claim in persona.claims
if claim.matches.length > 0 if claim.matches.length > 0
kx-claim.kx-item(data-claim=claim) kx-claim.kx-item(data-claim=claim)
details(aria-label="Claim") details(aria-label="Claim")
summary summary
.info .info
p.subtitle= claim.matches[0].serviceprovider.name p.subtitle= claim.display.serviceproviderName
p.title= claim.matches[0].profile.display p.title= claim.display.name
.icons .icons
.verificationStatus(data-value='running') .verificationStatus(data-value='running')
.inProgress .inProgress
@ -22,12 +25,12 @@ mixin generateUser(user, isPrimary)
.subsection .subsection
img(src='/static/img/link.png') img(src='/static/img/link.png')
div div
if (claim.matches[0].profile.uri) if (claim.display.url)
p Profile link: 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 else
p Profile link: not accessible from browser 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: p Proof link:
a(href=claim.matches[0].proof.uri aria-label="Link to proof")= claim.matches[0].proof.uri a(href=claim.matches[0].proof.uri aria-label="Link to proof")= claim.matches[0].proof.uri
else else
@ -36,10 +39,7 @@ mixin generateUser(user, isPrimary)
block content block content
script. script.
kx = { kx = {
key: { publicKey: !{JSON.stringify(data.publicKey)}
url: "!{data && data.key && data.key.fetchURL ? data.key.fetchURL : null}",
object: null
}
} }
if (data && 'errors' in data && data.errors.length > 0) if (data && 'errors' in data && data.errors.length > 0)
@ -80,12 +80,7 @@ block content
dialog#dialog--verifySignature dialog#dialog--verifySignature
div div
form(method='post') form(method='post')
label(for="sigVerInput") Signature label(for="sigVerInput") Signature name
textarea#sigVerInput.input(name='signature')
input.no-margin(type='submit' name='submit' value='VERIFY SIGNATURE')
br
br
label(for="sigVerOutput") Verification result
textarea#sigVerOutput.output(name='message' placeholder='Waiting for input' readonly) textarea#sigVerOutput.output(name='message' placeholder='Waiting for input' readonly)
form(method="dialog") form(method="dialog")
input(type="submit" value="Close") input(type="submit" value="Close")
@ -108,9 +103,9 @@ block content
unless (isSignature && !signature) unless (isSignature && !signature)
#profileHeader.card.card--transparent.card--profileHeader #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) if (enable_message_encryption || enable_signature_verification)
.button-wrapper .button-wrapper
@ -119,27 +114,28 @@ block content
if (enable_signature_verification) if (enable_signature_verification)
button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature button(onClick="document.querySelector('#dialog--verifySignature').showModal();") Verify signature
+generateUser(data.keyData.users[data.keyData.primaryUserIndex], true) +generatePersona(data.personas[data.primaryPersonaIndex], true && data.personas.length > 1)
each user, index in data.keyData.users each persona, index in data.personas
unless index == data.keyData.primaryUserIndex unless index == data.primaryPersonaIndex
+generateUser(user, false) +generatePersona(persona, false)
#profileProofs.card.card--transparent #profileProofs.card.card--transparent
h2 Key h2 Key
kx-key.kx-item(data-keydata=data.keyData) kx-key.kx-item(data-keydata=data.publicKey)
details(aria-label="Key") details(aria-label="Key")
summary summary
.info .info
p.subtitle= data.keyData.key.fetchMethod p.subtitle= data.publicKey.fetch.method
p.title= data.keyData.fingerprint p.title= data.identifier
.content .content
.subsection .subsection
img(src='/static/img/link.png') img(src='/static/img/link.png')
div div
p Key link: 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 hr
if (data.profileType === 'openpgp')
.subsection .subsection
img(src='/static/img/qrcode.png') img(src='/static/img/qrcode.png')
div div
button(onClick=`showQR('${data.keyData.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR button(onClick=`showQR('${data.publicKey.fingerprint}', 'fingerprint')` aria-label='Show QR code for cryptographic fingerprint') Show OpenPGP fingerprint QR

View file

@ -18,4 +18,6 @@ html(lang='en')
link(rel='stylesheet' href='/static/main.css') 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/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') script(type='application/javascript' defer src='/static/main.js' charset='utf-8')

View file

@ -13,10 +13,8 @@ export default (env) => {
mode: env.mode, mode: env.mode,
entry: { entry: {
main: { main: {
import: './static-src/index.js', import: './static-src/index.js'
dependOn: 'openpgp', }
},
openpgp: './node_modules/openpgp/dist/openpgp.js',
}, },
output: { output: {
filename: '[name].js', filename: '[name].js',
@ -62,12 +60,19 @@ export default (env) => {
new CopyPlugin({ new CopyPlugin({
patterns: [ patterns: [
{ from: './static-src/files/', to: '../static/' }, { 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: { options: {
concurrency: 10, concurrency: 10,
}, },
}), }),
], ],
externals: {
doipjs: 'doip',
openpgp: 'openpgp'
}
} }
} else { } else {
return {} return {}

1492
yarn.lock

File diff suppressed because it is too large Load diff