doipjs/src/openpgp.js

333 lines
9.5 KiB
JavaScript
Raw Normal View History

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.
*/
2024-01-27 09:57:46 -07:00
import axios, * as axiosMod from 'axios'
2023-07-08 00:17:13 -06:00
import { isUri } from 'valid-url'
import { readKey, PublicKey } from 'openpgp'
import HKP from '@openpgp/hkp-client'
import WKD from '@openpgp/wkd-client'
import { Claim } from './claim.js'
import { ProfileType, PublicKeyEncoding, PublicKeyFetchMethod, PublicKeyType } from './enums.js'
import { Profile } from './profile.js'
import { Persona } from './persona.js'
2020-11-16 18:13:56 -07:00
2021-04-22 07:14:21 -06:00
/**
* Functions related to OpenPGP Profiles
* @module openpgp
2021-04-22 07:14:21 -06:00
*/
/**
* Fetch a public key using keyservers
* @function
2024-01-27 10:00:55 -07:00
* @param {string} identifier - Fingerprint or email address
* @param {string} [keyserverDomain] - Domain of the keyserver
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
2021-04-22 07:14:21 -06:00
* @example
* const key1 = doip.keys.fetchHKP('alice@domain.tld');
* const key2 = doip.keys.fetchHKP('123abc123abc');
2024-01-27 10:00:55 -07:00
* const key3 = doip.keys.fetchHKP('123abc123abc', 'pgpkeys.eu');
2021-04-22 07:14:21 -06:00
*/
2024-01-27 10:00:55 -07:00
export async function fetchHKP (identifier, keyserverDomain = 'keys.openpgp.org') {
const keyserverBaseUrl = `https://${keyserverDomain ?? 'keys.openpgp.org'}`
2021-07-09 15:44:52 -06:00
2021-11-17 07:54:48 -07:00
const hkp = new HKP(keyserverBaseUrl)
2021-07-09 15:44:52 -06:00
const lookupOpts = {
query: identifier
}
const publicKeyArmored = await hkp
2021-11-17 07:54:48 -07:00
.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
if (!publicKeyArmored) {
2021-07-09 15:44:52 -06:00
throw new Error('Key does not exist or could not be fetched')
}
2020-11-16 18:13:56 -07:00
const publicKey = await readKey({
armoredKey: publicKeyArmored
2021-11-17 07:54:48 -07:00
})
2021-07-09 15:44:52 -06:00
.catch((error) => {
2021-11-17 07:54:48 -07:00
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.fetch.method = PublicKeyFetchMethod.HKP
profile.publicKey.fetch.query = identifier
return profile
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`
2024-01-27 10:00:55 -07:00
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
2021-04-22 07:14:21 -06:00
* @example
* const key = doip.keys.fetchWKD('alice@domain.tld');
*/
2023-07-08 00:17:13 -06:00
export async function fetchWKD (identifier) {
2021-11-17 07:54:48 -07:00
const wkd = new WKD()
2021-07-09 15:44:52 -06:00
const lookupOpts = {
email: identifier
}
const publicKeyBinary = await wkd
2021-07-09 15:44:52 -06:00
.lookup(lookupOpts)
2023-05-03 07:31:13 -06:00
.catch((/** @type {Error} */ error) => {
2021-07-09 15:44:52 -06:00
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
2021-11-17 07:54:48 -07:00
if (!publicKeyBinary) {
2021-11-17 07:54:48 -07:00
throw new Error('Key does not exist or could not be fetched')
}
const publicKey = await readKey({
binaryKey: publicKeyBinary
2021-11-17 07:54:48 -07:00
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.fetch.method = PublicKeyFetchMethod.WKD
profile.publicKey.fetch.query = identifier
return profile
2020-11-16 18:13:56 -07:00
}
2021-04-22 07:14:21 -06:00
/**
* Fetch a public key from Keybase
* @function
2024-01-27 10:00:55 -07:00
* @param {string} username - Keybase username
* @param {string} fingerprint - Fingerprint of key
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
2021-04-22 07:14:21 -06:00
* @example
* const key = doip.keys.fetchKeybase('alice', '123abc123abc');
*/
2023-07-08 00:17:13 -06:00
export async function fetchKeybase (username, fingerprint) {
2021-07-09 15:44:52 -06:00
const keyLink = `https://keybase.io/${username}/pgp_keys.asc?fingerprint=${fingerprint}`
let rawKeyContent
try {
2022-02-08 11:10:23 -07:00
rawKeyContent = await axios.get(
keyLink,
{
responseType: 'text'
}
)
2024-01-27 09:57:46 -07:00
.then((/** @type {axiosMod.AxiosResponse} */ response) => {
2021-07-09 15:44:52 -06:00
if (response.status === 200) {
return response
}
2021-03-01 10:27:29 -07:00
})
2024-01-27 09:57:46 -07:00
.then((/** @type {axiosMod.AxiosResponse} */ response) => response.data)
2021-07-09 15:44:52 -06:00
} catch (e) {
throw new Error(`Error fetching Keybase key: ${e.message}`)
}
const publicKey = await readKey({
2021-11-17 07:54:48 -07:00
armoredKey: rawKeyContent
})
2021-07-09 15:44:52 -06:00
.catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.fetch.method = PublicKeyFetchMethod.HTTP
profile.publicKey.fetch.query = null
profile.publicKey.fetch.resolvedUrl = keyLink
return profile
2020-11-16 18:13:56 -07:00
}
2021-04-22 07:14:21 -06:00
/**
2024-01-27 10:00:55 -07:00
* Get a public key from armored public key text data
2021-04-22 07:14:21 -06:00
* @function
* @param {string} rawKeyContent - Plaintext ASCII-formatted public key data
2024-01-27 10:00:55 -07:00
* @returns {Promise<Profile>} The profile from the armored public key
2021-04-22 07:14:21 -06:00
* @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);
*/
2023-07-08 00:17:13 -06:00
export async function fetchPlaintext (rawKeyContent) {
const publicKey = await readKey({
2021-11-17 07:54:48 -07:00
armoredKey: rawKeyContent
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
return profile
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
2024-01-27 10:00:55 -07:00
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
2021-04-22 07:14:21 -06:00
* @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');
*/
2023-07-08 00:17:13 -06:00
export async function fetchURI (uri) {
if (!isUri(uri)) {
2021-07-09 15:44:52 -06:00
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':
2022-03-27 15:04:16 -06:00
return await fetchHKP(
2021-07-09 15:44:52 -06:00
match[3] ? match[3] : match[2],
match[3] ? match[2] : null
)
case 'wkd':
2022-03-27 15:04:16 -06:00
return await fetchWKD(match[2])
2021-07-09 15:44:52 -06:00
case 'kb':
2022-03-27 15:04:16 -06:00
return await fetchKeybase(match[2], match.length >= 4 ? match[3] : null)
2021-07-09 15:44:52 -06:00
default:
throw new Error('Invalid URI protocol')
}
2020-11-16 18:13:56 -07:00
}
2022-03-27 15:04:16 -06:00
/**
* Fetch a public key
*
* This function will attempt to detect the identifier and fetch the key
* accordingly. If the identifier is an email address, it will first try and
* fetch the key using WKD and then HKP. Otherwise, it will try HKP only.
*
* This function will also try and parse the input as a plaintext key
* @function
* @param {string} identifier - URI that defines the location of the key
2024-01-27 10:00:55 -07:00
* @returns {Promise<Profile>} The profile from the fetched OpenPGP key
2022-03-27 15:04:16 -06:00
* @example
* const key1 = doip.keys.fetch('alice@domain.tld');
* const key2 = doip.keys.fetch('123abc123abc');
*/
2023-07-08 00:17:13 -06:00
export async function fetch (identifier) {
2022-03-27 15:04:16 -06:00
const re = /([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/
const match = identifier.match(re)
let profile = null
2022-03-27 15:04:16 -06:00
// Attempt plaintext
try {
profile = await fetchPlaintext(identifier)
} catch (e) {}
2022-03-27 15:04:16 -06:00
// Attempt WKD
if (!profile && identifier.includes('@')) {
2022-03-27 15:04:16 -06:00
try {
profile = await fetchWKD(match[1])
2022-03-27 15:04:16 -06:00
} catch (e) {}
}
// Attempt HKP
if (!profile) {
profile = await fetchHKP(
2022-03-27 15:04:16 -06:00
match[2] ? match[2] : match[1],
match[2] ? match[1] : null
)
}
if (!profile) {
2022-03-27 15:04:16 -06:00
throw new Error('Key does not exist or could not be fetched')
}
return profile
2022-03-27 15:04:16 -06:00
}
2021-04-22 07:14:21 -06:00
/**
* Process a public key to get a profile
2021-04-22 07:14:21 -06:00
* @function
* @param {PublicKey} publicKey - The public key to parse
2024-01-27 10:00:55 -07:00
* @returns {Promise<Profile>} The profile from the processed OpenPGP key
2021-04-22 07:14:21 -06:00
* @example
* const key = doip.keys.fetchURI('hkp:alice@domain.tld');
* const profile = doip.keys.parsePublicKey(key);
* profile.personas[0].claims.forEach(claim => {
2021-04-22 07:14:21 -06:00
* console.log(claim.uri);
* });
*/
export async function parsePublicKey (publicKey) {
2023-07-08 00:17:13 -06:00
if (!(publicKey && (publicKey instanceof PublicKey))) {
2021-07-09 15:44:52 -06:00
throw new Error('Invalid public key')
}
2021-11-17 07:54:48 -07:00
const fingerprint = publicKey.getFingerprint()
2021-07-09 15:44:52 -06:00
const primaryUser = await publicKey.getPrimaryUser()
const users = publicKey.users
const personas = []
2021-07-09 15:44:52 -06:00
users.forEach((user, i) => {
if (!user.userID) return
const pe = new Persona(user.userID.name, [])
pe.setIdentifier(user.userID.userID)
pe.setDescription(user.userID.comment)
pe.setEmailAddress(user.userID.email)
2021-04-19 03:06:29 -06:00
2021-07-09 15:44:52 -06:00
if ('selfCertifications' in user && user.selfCertifications.length > 0) {
2023-05-03 07:33:45 -06:00
const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0]
if (selfCertification.revoked) {
pe.revoke()
}
2021-07-09 15:44:52 -06:00
const notations = selfCertification.rawNotations
pe.claims = notations
2021-07-09 15:44:52 -06:00
.filter(
({ name, humanReadable }) =>
2021-11-06 11:38:22 -06:00
humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
2021-07-09 15:44:52 -06:00
)
.map(
({ value }) =>
new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
2021-07-09 15:44:52 -06:00
)
}
personas.push(pe)
2020-12-05 15:13:44 -07:00
})
2021-07-09 15:44:52 -06:00
const profile = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, personas)
profile.primaryPersonaIndex = primaryUser.index
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.fingerprint = fingerprint
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
return profile
2021-04-22 08:00:37 -06:00
}