2020-11-16 18:13:56 -07:00
|
|
|
/*
|
2021-01-13 05:20:33 -07:00
|
|
|
Copyright 2021 Yarmo Mackenbach
|
2020-11-16 18:13:56 -07:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
const bent = require('bent')
|
|
|
|
const req = bent('GET')
|
|
|
|
const validUrl = require('valid-url')
|
2020-12-09 17:56:34 -07:00
|
|
|
const openpgp = require('openpgp')
|
2021-04-19 03:06:29 -06:00
|
|
|
const Claim = require('./claim')
|
2020-11-16 18:13:56 -07:00
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
|
|
|
* Functions related to the fetching and handling of keys
|
|
|
|
* @module keys
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch a public key using keyservers
|
|
|
|
* @function
|
|
|
|
* @param {string} identifier - Fingerprint or email address
|
|
|
|
* @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver
|
|
|
|
* @returns {openpgp.key.Key}
|
|
|
|
* @example
|
|
|
|
* const key1 = doip.keys.fetchHKP('alice@domain.tld');
|
|
|
|
* const key2 = doip.keys.fetchHKP('123abc123abc');
|
|
|
|
*/
|
2021-07-09 15:44:52 -06:00
|
|
|
exports.fetchHKP = async (identifier, keyserverDomain) => {
|
|
|
|
const keyserverBaseUrl = keyserverDomain
|
|
|
|
? `https://${keyserverDomain}`
|
|
|
|
: 'https://keys.openpgp.org'
|
|
|
|
|
|
|
|
const hkp = new openpgp.HKP(keyserverBaseUrl)
|
|
|
|
const lookupOpts = {
|
|
|
|
query: identifier
|
|
|
|
}
|
|
|
|
|
|
|
|
const publicKey = await hkp.lookup(lookupOpts).catch((error) => {
|
|
|
|
throw new Error(`Key does not exist or could not be fetched (${error})`)
|
|
|
|
})
|
2020-11-16 18:13:56 -07:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
if (!publicKey) {
|
|
|
|
throw new Error('Key does not exist or could not be fetched')
|
|
|
|
}
|
2020-11-16 18:13:56 -07:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
return await openpgp.key
|
|
|
|
.readArmored(publicKey)
|
|
|
|
.then((result) => {
|
|
|
|
return result.keys[0]
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
throw new Error(`Key does not exist or could not be fetched (${error})`)
|
2021-01-09 08:04:07 -07:00
|
|
|
})
|
2020-11-16 18:13:56 -07:00
|
|
|
}
|
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
|
|
|
* Fetch a public key using Web Key Directory
|
|
|
|
* @function
|
|
|
|
* @param {string} identifier - Identifier of format 'username@domain.tld`
|
|
|
|
* @returns {openpgp.key.Key}
|
|
|
|
* @example
|
|
|
|
* const key = doip.keys.fetchWKD('alice@domain.tld');
|
|
|
|
*/
|
2021-07-09 15:44:52 -06:00
|
|
|
exports.fetchWKD = async (identifier) => {
|
|
|
|
const wkd = new openpgp.WKD()
|
|
|
|
const lookupOpts = {
|
|
|
|
email: identifier
|
|
|
|
}
|
|
|
|
|
|
|
|
return await wkd
|
|
|
|
.lookup(lookupOpts)
|
|
|
|
.then((result) => {
|
|
|
|
return result.keys[0]
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
throw new Error(`Key does not exist or could not be fetched (${error})`)
|
|
|
|
})
|
2020-11-16 18:13:56 -07:00
|
|
|
}
|
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
|
|
|
* Fetch a public key from Keybase
|
|
|
|
* @function
|
|
|
|
* @param {string} username - Keybase username
|
|
|
|
* @param {string} fingerprint - Fingerprint of key
|
|
|
|
* @returns {openpgp.key.Key}
|
|
|
|
* @example
|
|
|
|
* const key = doip.keys.fetchKeybase('alice', '123abc123abc');
|
|
|
|
*/
|
2021-07-09 15:44:52 -06:00
|
|
|
exports.fetchKeybase = async (username, fingerprint) => {
|
|
|
|
const keyLink = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
|
|
|
|
let rawKeyContent
|
|
|
|
try {
|
|
|
|
rawKeyContent = await req(keyLink)
|
|
|
|
.then((response) => {
|
|
|
|
if (response.status === 200) {
|
|
|
|
return response
|
|
|
|
}
|
2021-03-01 10:27:29 -07:00
|
|
|
})
|
2021-07-09 15:44:52 -06:00
|
|
|
.then((response) => response.text())
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error(`Error fetching Keybase key: ${e.message}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return await openpgp.key
|
|
|
|
.readArmored(rawKeyContent)
|
|
|
|
.then((result) => {
|
|
|
|
return result.keys[0]
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
throw new Error(`Key does not exist or could not be fetched (${error})`)
|
|
|
|
})
|
2020-11-16 18:13:56 -07:00
|
|
|
}
|
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
|
|
|
* Get a public key from plaintext data
|
|
|
|
* @function
|
|
|
|
* @param {string} rawKeyContent - Plaintext ASCII-formatted public key data
|
|
|
|
* @returns {openpgp.key.Key}
|
|
|
|
* @example
|
|
|
|
* const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
2021-04-22 08:00:37 -06:00
|
|
|
*
|
2021-04-22 07:14:21 -06:00
|
|
|
* mQINBF0mIsIBEADacleiyiV+z6FIunvLWrO6ZETxGNVpqM+WbBQKdW1BVrJBBolg
|
|
|
|
* [...]
|
|
|
|
* =6lib
|
|
|
|
* -----END PGP PUBLIC KEY BLOCK-----`
|
|
|
|
* const key = doip.keys.fetchPlaintext(plainkey);
|
|
|
|
*/
|
2021-07-09 15:44:52 -06:00
|
|
|
exports.fetchPlaintext = async (rawKeyContent) => {
|
|
|
|
const publicKey = (await openpgp.key.readArmored(rawKeyContent)).keys[0]
|
|
|
|
return publicKey
|
2020-11-16 18:13:56 -07:00
|
|
|
}
|
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
|
|
|
* Fetch a public key using an URI
|
|
|
|
* @function
|
|
|
|
* @param {string} uri - URI that defines the location of the key
|
|
|
|
* @returns {openpgp.key.Key}
|
|
|
|
* @example
|
|
|
|
* const key1 = doip.keys.fetchURI('hkp:alice@domain.tld');
|
|
|
|
* const key2 = doip.keys.fetchURI('hkp:123abc123abc');
|
|
|
|
* const key3 = doip.keys.fetchURI('wkd:alice@domain.tld');
|
|
|
|
*/
|
2021-07-09 15:44:52 -06:00
|
|
|
exports.fetchURI = async (uri) => {
|
|
|
|
if (!validUrl.isUri(uri)) {
|
|
|
|
throw new Error('Invalid URI')
|
|
|
|
}
|
|
|
|
|
|
|
|
const re = /([a-zA-Z0-9]*):([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/
|
|
|
|
const match = uri.match(re)
|
|
|
|
|
|
|
|
if (!match[1]) {
|
|
|
|
throw new Error('Invalid URI')
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (match[1]) {
|
|
|
|
case 'hkp':
|
|
|
|
return exports.fetchHKP(
|
|
|
|
match[3] ? match[3] : match[2],
|
|
|
|
match[3] ? match[2] : null
|
|
|
|
)
|
|
|
|
|
|
|
|
case 'wkd':
|
|
|
|
return exports.fetchWKD(match[2])
|
|
|
|
|
|
|
|
case 'kb':
|
|
|
|
return exports.fetchKeybase(match[2], match.length >= 4 ? match[3] : null)
|
|
|
|
|
|
|
|
default:
|
|
|
|
throw new Error('Invalid URI protocol')
|
|
|
|
}
|
2020-11-16 18:13:56 -07:00
|
|
|
}
|
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
|
|
|
* Process a public key to get user data and claims
|
|
|
|
* @function
|
|
|
|
* @param {openpgp.key.Key} publicKey - The public key to process
|
|
|
|
* @returns {object}
|
|
|
|
* @example
|
|
|
|
* const key = doip.keys.fetchURI('hkp:alice@domain.tld');
|
|
|
|
* const data = doip.keys.process(key);
|
|
|
|
* data.users[0].claims.forEach(claim => {
|
|
|
|
* console.log(claim.uri);
|
|
|
|
* });
|
|
|
|
*/
|
2021-07-09 15:44:52 -06:00
|
|
|
exports.process = async (publicKey) => {
|
|
|
|
if (!publicKey || !(publicKey instanceof openpgp.key.Key)) {
|
|
|
|
throw new Error('Invalid public key')
|
|
|
|
}
|
|
|
|
|
|
|
|
const fingerprint = await publicKey.primaryKey.getFingerprint()
|
|
|
|
const primaryUser = await publicKey.getPrimaryUser()
|
|
|
|
const users = publicKey.users
|
|
|
|
const usersOutput = []
|
|
|
|
|
|
|
|
users.forEach((user, i) => {
|
|
|
|
usersOutput[i] = {
|
|
|
|
userData: {
|
|
|
|
id: user.userId ? user.userId.userid : null,
|
|
|
|
name: user.userId ? user.userId.name : null,
|
|
|
|
email: user.userId ? user.userId.email : null,
|
|
|
|
comment: user.userId ? user.userId.comment : null,
|
|
|
|
isPrimary: primaryUser.index === i,
|
|
|
|
isRevoked: false
|
|
|
|
},
|
|
|
|
claims: []
|
2020-12-05 15:13:44 -07:00
|
|
|
}
|
2021-04-19 03:06:29 -06:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
if ('selfCertifications' in user && user.selfCertifications.length > 0) {
|
|
|
|
const selfCertification = user.selfCertifications[0]
|
2020-11-20 14:53:56 -07:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
const notations = selfCertification.rawNotations
|
|
|
|
usersOutput[i].claims = notations
|
|
|
|
.filter(
|
|
|
|
({ name, humanReadable }) =>
|
|
|
|
humanReadable && name === 'proof@metacode.biz'
|
|
|
|
)
|
|
|
|
.map(
|
|
|
|
({ value }) =>
|
|
|
|
new Claim(openpgp.util.decode_utf8(value), fingerprint)
|
|
|
|
)
|
2020-11-16 18:13:56 -07:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
usersOutput[i].userData.isRevoked = selfCertification.revoked
|
|
|
|
}
|
2020-12-05 15:13:44 -07:00
|
|
|
})
|
2021-07-09 15:44:52 -06:00
|
|
|
|
|
|
|
return {
|
|
|
|
fingerprint: fingerprint,
|
|
|
|
users: usersOutput,
|
|
|
|
primaryUserIndex: primaryUser.index,
|
|
|
|
key: {
|
|
|
|
data: publicKey,
|
|
|
|
fetchMethod: null,
|
|
|
|
uri: null
|
|
|
|
}
|
|
|
|
}
|
2021-04-22 08:00:37 -06:00
|
|
|
}
|