mirror of
https://codeberg.org/keyoxide/doipjs.git
synced 2024-12-22 14:39:28 -07:00
feat: refactor keys to openpgp, use Profile class
This commit is contained in:
parent
a30339272a
commit
149ac6f71e
3 changed files with 146 additions and 120 deletions
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
// })
|
||||
// })
|
Loading…
Reference in a new issue