feat: refactor keys to openpgp, use Profile class

This commit is contained in:
Yarmo Mackenbach 2023-07-09 12:01:09 +02:00
parent a30339272a
commit 149ac6f71e
No known key found for this signature in database
GPG key ID: 3C57D093219103A3
3 changed files with 146 additions and 120 deletions

View file

@ -18,7 +18,7 @@ export { Persona } from './persona.js'
export { Claim } from './claim.js'
export { ServiceProvider } from './serviceProvider.js'
export * as proofs from './proofs.js'
export * as keys from './keys.js'
export * as openpgp from './openpgp.js'
export * as asp from './asp.js'
export * as signatures from './signatures.js'
export * as enums from './enums.js'

View file

@ -19,10 +19,13 @@ 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'
/**
* Functions related to the fetching and handling of keys
* @module keys
* Functions related to OpenPGP Profiles
* @module openpgp
*/
/**
@ -30,7 +33,7 @@ import { Claim } from './claim.js'
* @function
* @param {string} identifier - Fingerprint or email address
* @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver
* @returns {Promise<PublicKey>}
* @returns {Promise<Profile>}
* @example
* const key1 = doip.keys.fetchHKP('alice@domain.tld');
* const key2 = doip.keys.fetchHKP('123abc123abc');
@ -40,61 +43,79 @@ export async function fetchHKP (identifier, keyserverDomain) {
? `https://${keyserverDomain}`
: 'https://keys.openpgp.org'
// @ts-ignore
const hkp = new HKP(keyserverBaseUrl)
const lookupOpts = {
query: identifier
}
const publicKey = await hkp
const publicKeyArmored = await hkp
.lookup(lookupOpts)
.catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
if (!publicKey) {
if (!publicKeyArmored) {
throw new Error('Key does not exist or could not be fetched')
}
return await readKey({
armoredKey: publicKey
const publicKey = await readKey({
armoredKey: publicKeyArmored
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
profile.publicKey.fetch.method = PublicKeyFetchMethod.HKP
profile.publicKey.fetch.query = identifier
return profile
}
/**
* Fetch a public key using Web Key Directory
* @function
* @param {string} identifier - Identifier of format 'username@domain.tld`
* @returns {Promise<PublicKey>}
* @returns {Promise<Profile>}
* @example
* const key = doip.keys.fetchWKD('alice@domain.tld');
*/
export async function fetchWKD (identifier) {
// @ts-ignore
const wkd = new WKD()
const lookupOpts = {
email: identifier
}
const publicKey = await wkd
const publicKeyBinary = await wkd
.lookup(lookupOpts)
.catch((/** @type {Error} */ error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
if (!publicKey) {
if (!publicKeyBinary) {
throw new Error('Key does not exist or could not be fetched')
}
return await readKey({
binaryKey: publicKey
const publicKey = await readKey({
binaryKey: publicKeyBinary
})
.catch((error) => {
throw new Error(`Key could not be read (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
profile.publicKey.fetch.method = PublicKeyFetchMethod.WKD
profile.publicKey.fetch.query = identifier
return profile
}
/**
@ -102,7 +123,7 @@ export async function fetchWKD (identifier) {
* @function
* @param {string} username - Keybase username
* @param {string} fingerprint - Fingerprint of key
* @returns {Promise<PublicKey>}
* @returns {Promise<Profile>}
* @example
* const key = doip.keys.fetchKeybase('alice', '123abc123abc');
*/
@ -126,19 +147,30 @@ export async function fetchKeybase (username, fingerprint) {
throw new Error(`Error fetching Keybase key: ${e.message}`)
}
return await readKey({
const publicKey = await readKey({
armoredKey: rawKeyContent
})
.catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`)
})
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
profile.publicKey.fetch.method = PublicKeyFetchMethod.HTTP
profile.publicKey.fetch.query = null
profile.publicKey.fetch.resolvedUrl = keyLink
return profile
}
/**
* Get a public key from plaintext data
* @function
* @param {string} rawKeyContent - Plaintext ASCII-formatted public key data
* @returns {Promise<PublicKey>}
* @returns {Promise<Profile>}
* @example
* const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
*
@ -156,14 +188,20 @@ export async function fetchPlaintext (rawKeyContent) {
throw new Error(`Key could not be read (${error})`)
})
return publicKey
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
return profile
}
/**
* Fetch a public key using an URI
* @function
* @param {string} uri - URI that defines the location of the key
* @returns {Promise<PublicKey>}
* @returns {Promise<Profile>}
* @example
* const key1 = doip.keys.fetchURI('hkp:alice@domain.tld');
* const key2 = doip.keys.fetchURI('hkp:123abc123abc');
@ -209,7 +247,7 @@ export async function fetchURI (uri) {
* 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
* @returns {Promise<PublicKey>}
* @returns {Promise<Profile>}
* @example
* const key1 = doip.keys.fetch('alice@domain.tld');
* const key2 = doip.keys.fetch('123abc123abc');
@ -218,42 +256,40 @@ export async function fetch (identifier) {
const re = /([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/
const match = identifier.match(re)
let pubKey = null
let profile = null
// Attempt plaintext
if (!pubKey) {
try {
pubKey = await fetchPlaintext(identifier)
} catch (e) {}
}
try {
profile = await fetchPlaintext(identifier)
} catch (e) {}
// Attempt WKD
if (!pubKey && identifier.includes('@')) {
if (!profile && identifier.includes('@')) {
try {
pubKey = await fetchWKD(match[1])
profile = await fetchWKD(match[1])
} catch (e) {}
}
// Attempt HKP
if (!pubKey) {
pubKey = await fetchHKP(
if (!profile) {
profile = await fetchHKP(
match[2] ? match[2] : match[1],
match[2] ? match[1] : null
)
}
if (!pubKey) {
if (!profile) {
throw new Error('Key does not exist or could not be fetched')
}
return pubKey
return profile
}
/**
* Process a public key to get user data and claims
* @function
* @param {PublicKey} publicKey - The public key to process
* @returns {Promise<object>}
* @returns {Promise<Profile>}
* @example
* const key = doip.keys.fetchURI('hkp:alice@domain.tld');
* const data = doip.keys.process(key);
@ -261,7 +297,7 @@ export async function fetch (identifier) {
* console.log(claim.uri);
* });
*/
export async function process (publicKey) {
async function parsePublicKey (publicKey) {
if (!(publicKey && (publicKey instanceof PublicKey))) {
throw new Error('Invalid public key')
}
@ -269,47 +305,37 @@ export async function process (publicKey) {
const fingerprint = publicKey.getFingerprint()
const primaryUser = await publicKey.getPrimaryUser()
const users = publicKey.users
const usersOutput = []
const personas = []
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: []
}
const pe = new Persona(user.userID.name, [])
pe.setIdentifier(user.userID.userID)
pe.setDescription(user.userID.comment)
pe.setEmailAddress(user.userID.email)
if ('selfCertifications' in user && user.selfCertifications.length > 0) {
const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0]
if (selfCertification.revoked) {
pe.revoke()
}
const notations = selfCertification.rawNotations
usersOutput[i].claims = notations
pe.claims = notations
.filter(
({ name, humanReadable }) =>
humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
)
.map(
({ value }) =>
new Claim(new TextDecoder().decode(value), fingerprint)
new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
)
usersOutput[i].userData.isRevoked = selfCertification.revoked
}
personas.push(pe)
})
return {
fingerprint,
users: usersOutput,
primaryUserIndex: primaryUser.index,
key: {
data: publicKey,
fetchMethod: null,
uri: null
}
}
const pr = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, personas)
pr.primaryPersonaIndex = primaryUser.index
return pr
}

View file

@ -18,7 +18,7 @@ import chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised)
import { PublicKey } from 'openpgp'
import { keys } from '../src/index.js'
import { openpgp, Profile } from '../src/index.js'
const pubKeyFingerprint = "3637202523e7c1309ab79e99ef2dc5827b445f4b"
const pubKeyEmail = "test@doip.rocks"
@ -90,115 +90,115 @@ Q+AZdYCbM0hdBjP4xdKZcpqak8ksb+aQFXjGacDL/XN4VrP+tBGxkqIqreoDcgIb
=tVW7
-----END PGP PUBLIC KEY BLOCK-----`
describe('keys.fetch', () => {
describe('openpgp.fetch', () => {
it('should be a function (1 argument)', () => {
expect(keys.fetch).to.be.a('function')
expect(keys.fetch).to.have.length(1)
expect(openpgp.fetch).to.be.a('function')
expect(openpgp.fetch).to.have.length(1)
})
it('should return a Key object when provided a valid fingerprint', async () => {
expect(
await keys.fetch(pubKeyFingerprint)
).to.be.instanceOf(PublicKey)
await openpgp.fetch(pubKeyFingerprint)
).to.be.instanceOf(Profile)
}).timeout('12s')
it('should return a Key object when provided a valid email address', async () => {
expect(
await keys.fetch(pubKeyEmail)
).to.be.instanceOf(PublicKey)
await openpgp.fetch(pubKeyEmail)
).to.be.instanceOf(Profile)
}).timeout('12s')
it('should reject when provided an invalid email address', () => {
return expect(
keys.fetch('invalid@doip.rocks')
openpgp.fetch('invalid@doip.rocks')
).to.eventually.be.rejectedWith('Key does not exist or could not be fetched')
}).timeout('12s')
})
describe('keys.fetchURI', () => {
describe('openpgp.fetchURI', () => {
it('should be a function (1 argument)', () => {
expect(keys.fetchURI).to.be.a('function')
expect(keys.fetchURI).to.have.length(1)
expect(openpgp.fetchURI).to.be.a('function')
expect(openpgp.fetchURI).to.have.length(1)
})
it('should return a Key object when provided a hkp: uri', async () => {
expect(
await keys.fetchURI(`hkp:${pubKeyFingerprint}`)
).to.be.instanceOf(PublicKey)
await openpgp.fetchURI(`hkp:${pubKeyFingerprint}`)
).to.be.instanceOf(Profile)
}).timeout('12s')
it('should reject when provided an invalid uri', () => {
return expect(
keys.fetchURI(`inv:${pubKeyFingerprint}`)
openpgp.fetchURI(`inv:${pubKeyFingerprint}`)
).to.eventually.be.rejectedWith('Invalid URI protocol')
}).timeout('12s')
})
describe('keys.fetchHKP', () => {
describe('openpgp.fetchHKP', () => {
it('should be a function (2 arguments)', () => {
expect(keys.fetchHKP).to.be.a('function')
expect(keys.fetchHKP).to.have.length(2)
expect(openpgp.fetchHKP).to.be.a('function')
expect(openpgp.fetchHKP).to.have.length(2)
})
it('should return a Key object when provided a valid fingerprint', async () => {
expect(await keys.fetchHKP(pubKeyFingerprint)).to.be.instanceOf(
PublicKey
expect(await openpgp.fetchHKP(pubKeyFingerprint)).to.be.instanceOf(
Profile
)
}).timeout('12s')
it('should return a Key object when provided a valid email address', async () => {
expect(await keys.fetchHKP(pubKeyEmail)).to.be.instanceOf(
PublicKey
expect(await openpgp.fetchHKP(pubKeyEmail)).to.be.instanceOf(
Profile
)
}).timeout('12s')
it('should reject when provided an invalid fingerprint', async () => {
return expect(
keys.fetchHKP('4637202523e7c1309ab79e99ef2dc5827b445f4b')
openpgp.fetchHKP('4637202523e7c1309ab79e99ef2dc5827b445f4b')
).to.eventually.be.rejectedWith(
'Key does not exist or could not be fetched'
)
}).timeout('12s')
it('should reject when provided an invalid email address', async () => {
return expect(
keys.fetchHKP('invalid@doip.rocks')
openpgp.fetchHKP('invalid@doip.rocks')
).to.eventually.be.rejectedWith(
'Key does not exist or could not be fetched'
)
}).timeout('12s')
})
describe('keys.fetchPlaintext', () => {
describe('openpgp.fetchPlaintext', () => {
it('should be a function (1 argument)', () => {
expect(keys.fetchPlaintext).to.be.a('function')
expect(keys.fetchPlaintext).to.have.length(1)
expect(openpgp.fetchPlaintext).to.be.a('function')
expect(openpgp.fetchPlaintext).to.have.length(1)
})
it('should return a Key object', async () => {
expect(await keys.fetchPlaintext(pubKeyPlaintext)).to.be.instanceOf(
PublicKey
expect(await openpgp.fetchPlaintext(pubKeyPlaintext)).to.be.instanceOf(
Profile
)
}).timeout('12s')
})
describe('keys.process', () => {
it('should be a function (1 argument)', () => {
expect(keys.process).to.be.a('function')
expect(keys.process).to.have.length(1)
})
it('should return an object with specific keys', async () => {
const pubKey = await keys.fetchPlaintext(pubKeyPlaintext)
const obj = await keys.process(pubKey)
expect(obj).to.have.keys([
'users',
'fingerprint',
'primaryUserIndex',
'key',
])
})
it('should ignore non-proof notations', async () => {
const pubKey = await keys.fetchPlaintext(pubKeyWithOtherNotations)
const obj = await keys.process(pubKey)
expect(obj.users).to.be.lengthOf(1)
expect(obj.users[0].claims).to.be.lengthOf(1)
expect(obj.users[0].claims[0].uri).to.be.equal('dns:yarmo.eu?type=TXT')
})
it('should properly handle revoked UIDs', async () => {
const pubKey = await keys.fetchPlaintext(pubKeyWithRevokedUID)
const obj = await keys.process(pubKey)
expect(obj.users).to.be.lengthOf(2)
expect(obj.users[0].userData.isRevoked).to.be.true
expect(obj.users[1].userData.isRevoked).to.be.false
})
})
// describe('openpgp.process', () => {
// it('should be a function (1 argument)', () => {
// expect(openpgp.process).to.be.a('function')
// expect(openpgp.process).to.have.length(1)
// })
// it('should return an object with specific openpgp', async () => {
// const pubKey = await openpgp.fetchPlaintext(pubKeyPlaintext)
// const obj = await openpgp.process(pubKey)
// expect(obj).to.have.openpgp([
// 'users',
// 'fingerprint',
// 'primaryUserIndex',
// 'key',
// ])
// })
// it('should ignore non-proof notations', async () => {
// const pubKey = await openpgp.fetchPlaintext(pubKeyWithOtherNotations)
// const obj = await openpgp.process(pubKey)
// expect(obj.users).to.be.lengthOf(1)
// expect(obj.users[0].claims).to.be.lengthOf(1)
// expect(obj.users[0].claims[0].uri).to.be.equal('dns:yarmo.eu?type=TXT')
// })
// it('should properly handle revoked UIDs', async () => {
// const pubKey = await openpgp.fetchPlaintext(pubKeyWithRevokedUID)
// const obj = await openpgp.process(pubKey)
// expect(obj.users).to.be.lengthOf(2)
// expect(obj.users[0].userData.isRevoked).to.be.true
// expect(obj.users[1].userData.isRevoked).to.be.false
// })
// })