doipjs/src/claim.js

338 lines
8.5 KiB
JavaScript
Raw Normal View History

2021-04-16 05:11:27 -06:00
/*
Copyright 2021 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.
*/
const openpgp = require('openpgp')
const validator = require('validator')
const validUrl = require('valid-url')
const mergeOptions = require('merge-options')
const proofs = require('./proofs')
const verifications = require('./verifications')
const claimDefinitions = require('./claimDefinitions')
const defaults = require('./defaults')
const E = require('./enums')
/**
* OpenPGP-based identity claim
* @class
*/
class Claim {
/**
* Initialize a Claim object
* @constructor
* @param {string} [uri] - The URI of the identity claim
* @param {string} [fingerprint] - The fingerprint of the OpenPGP key
*/
constructor(uri, fingerprint) {
2021-04-19 03:41:40 -06:00
// Import JSON
if ('claimVersion' in uri) {
switch (data.claimVersion) {
case 1:
this.uri = data.uri
this.fingerprint = data.fingerprint
this.state = data.state
this.dataMatches = data.dataMatches
this.verification = data.verification
break
2021-04-19 03:44:30 -06:00
2021-04-19 03:41:40 -06:00
default:
throw new Error('Invalid claim version')
break
}
return
}
2021-04-16 05:11:27 -06:00
// Verify validity of URI
if (uri && !validUrl.isUri(uri)) {
throw new Error('Invalid URI')
}
// Verify validity of fingerprint
if (fingerprint) {
try {
validator.isAlphanumeric(fingerprint)
} catch (err) {
throw new Error(`Invalid fingerprint`)
}
}
2021-04-19 03:41:40 -06:00
this.uri = uri ? uri : null
this.fingerprint = fingerprint ? fingerprint : null
this.state = E.ClaimState.INIT
this.dataMatches = null
this.verification = null
2021-04-16 05:11:27 -06:00
}
/**
* Get the claim's URI
* @function
* @returns {string}
*/
get uri() {
2021-04-19 03:41:40 -06:00
return this.uri
2021-04-16 05:11:27 -06:00
}
/**
* Get the fingerprint the claim is supposed to acknowledge
* @function
* @returns {string}
*/
get fingerprint() {
2021-04-19 03:41:40 -06:00
return this.fingerprint
2021-04-16 05:11:27 -06:00
}
/**
* Get the current state of the claim's verification process
* @function
* @returns {string}
*/
get state() {
2021-04-19 03:41:40 -06:00
return this.state
2021-04-16 05:11:27 -06:00
}
/**
2021-04-19 03:44:30 -06:00
* Get the candidate claim definitions the URI matched against
2021-04-16 05:11:27 -06:00
* @function
* @returns {object}
*/
get matches() {
2021-04-19 03:41:40 -06:00
if (this.state === E.ClaimState.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has not yet been matched')
}
2021-04-19 03:41:40 -06:00
return this.dataMatches
2021-04-16 05:11:27 -06:00
}
/**
* Get the result of the verification process
* @function
* @returns {object}
*/
get result() {
2021-04-19 03:41:40 -06:00
if (this.state !== E.ClaimState.VERIFIED) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has not yet been verified')
}
2021-04-19 03:41:40 -06:00
return this.verification
2021-04-16 05:11:27 -06:00
}
/**
* Set the claim's URI
* @function
* @param {string} uri - The new claim URI
*/
set uri(uri) {
2021-04-19 03:41:40 -06:00
if (this.state !== E.ClaimState.INIT) {
2021-04-19 03:44:30 -06:00
throw new Error(
'Cannot change the URI, this claim has already been matched'
)
2021-04-16 05:11:27 -06:00
}
// Verify validity of URI
if (uri && !validUrl.isUri(uri)) {
throw new Error('The URI was invalid')
}
// Remove leading and trailing spaces
uri = uri.replace(/^\s+|\s+$/g, '')
2021-04-19 03:41:40 -06:00
this.uri = uri
2021-04-16 05:11:27 -06:00
}
/**
* Set the claim's fingerprint to verify against
* @function
2021-04-19 03:41:40 -06:00
* @param {string} fingerprint - The new fingerprint
2021-04-16 05:11:27 -06:00
*/
set fingerprint(fingerprint) {
2021-04-19 03:41:40 -06:00
if (this.state === E.ClaimState.VERIFIED) {
2021-04-19 03:44:30 -06:00
throw new Error(
'Cannot change the fingerprint, this claim has already been verified'
)
2021-04-16 05:11:27 -06:00
}
2021-04-19 03:41:40 -06:00
this.fingerprint = fingerprint
}
/**
* Throw error when attempting to alter the state
* @function
* @param anything - Anything will throw an error
*/
set state(anything) {
2021-04-19 03:44:30 -06:00
throw new Error("Cannot change a claim's state")
2021-04-19 03:41:40 -06:00
}
/**
* Throw error when attempting to alter the dataMatches
* @function
* @param anything - Anything will throw an error
*/
set dataMatches(anything) {
2021-04-19 03:44:30 -06:00
throw new Error("Cannot change a claim's dataMatches")
2021-04-19 03:41:40 -06:00
}
/**
* Throw error when attempting to alter the verification data
* @function
* @param anything - Anything will throw an error
*/
set verification(anything) {
2021-04-19 03:44:30 -06:00
throw new Error("Cannot change a claim's verification data")
2021-04-16 05:11:27 -06:00
}
/**
* Match the claim's URI to candidate definitions
* @function
*/
match() {
2021-04-19 03:41:40 -06:00
if (this.state !== E.ClaimState.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim was already matched')
}
2021-04-19 03:41:40 -06:00
if (this.uri === null) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has no URI')
}
2021-04-19 03:41:40 -06:00
this.dataMatches = []
2021-04-16 05:11:27 -06:00
claimDefinitions.list.every((name, i) => {
const def = claimDefinitions.data[name]
// If the candidate is invalid, continue matching
2021-04-19 03:41:40 -06:00
if (!def.reURI.test(this.uri)) {
2021-04-16 05:11:27 -06:00
return true
}
2021-04-19 03:41:40 -06:00
const candidate = def.processURI(this.uri)
2021-04-16 05:11:27 -06:00
if (candidate.match.isAmbiguous) {
// Add to the possible candidates
2021-04-19 03:41:40 -06:00
this.dataMatches.push(candidate)
2021-04-16 05:11:27 -06:00
} else {
// Set a single candidate and stop
2021-04-19 03:44:30 -06:00
this.dataMatches = [candidate]
2021-04-16 05:11:27 -06:00
return false
}
// Continue matching
return true
})
2021-04-19 03:41:40 -06:00
this.state = E.ClaimState.MATCHED
2021-04-16 05:11:27 -06:00
}
/**
* Verify the claim. The proof for each candidate is sequentially fetched and
* checked for the fingerprint. The verification stops when either a positive
* result was obtained, or an unambiguous claim definition was processed
* regardless of the result.
* @async
* @function
* @param {object} [opts] - Options for proxy, fetchers
*/
async verify(opts) {
2021-04-19 03:41:40 -06:00
if (this.state === E.ClaimState.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has not yet been matched')
}
2021-04-19 03:41:40 -06:00
if (this.state === E.ClaimState.VERIFIED) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has already been verified')
}
2021-04-19 03:41:40 -06:00
if (this.fingerprint === null) {
2021-04-16 05:11:27 -06:00
throw new Error('This claim has no fingerprint')
}
// Handle options
opts = mergeOptions(defaults.opts, opts ? opts : {})
// For each match
2021-04-19 03:41:40 -06:00
for (let index = 0; index < this.dataMatches.length; index++) {
const claimData = this.dataMatches[index]
2021-04-19 03:44:30 -06:00
2021-04-16 05:11:27 -06:00
let verificationResult,
proofData = null,
proofFetchError
try {
proofData = await proofs.fetch(claimData, opts)
} catch (err) {
proofFetchError = err
}
if (proofData) {
// Run the verification process
2021-04-19 03:44:30 -06:00
verificationResult = verifications.run(
proofData.result,
claimData,
this.fingerprint
)
2021-04-16 05:11:27 -06:00
verificationResult.proof = {
fetcher: proofData.fetcher,
viaProxy: proofData.viaProxy,
}
} else {
if (this.isAmbiguous()) {
// Assume a wrong match and continue
continue
}
// Consider the proof completed but with a negative result
verificationResult = {
result: false,
completed: true,
proof: {},
errors: [proofFetchError],
}
}
if (verificationResult.completed) {
// Store the result, keep a single match and stop verifying
2021-04-19 03:41:40 -06:00
this.verification = verificationResult
2021-04-19 03:44:30 -06:00
this.dataMatches = [claimData]
2021-04-19 03:41:40 -06:00
index = this.dataMatches.length
2021-04-16 05:11:27 -06:00
}
}
2021-04-19 03:41:40 -06:00
this.state = E.ClaimState.VERIFIED
2021-04-16 05:11:27 -06:00
}
/**
* Get the ambiguity of the claim. A claim is only unambiguous if any
* of the candidates is unambiguous. An ambiguous claim should never be
* displayed in an user interface when its result is negative.
* @function
* @returns {boolean}
*/
isAmbiguous() {
2021-04-19 03:41:40 -06:00
if (this.state === E.ClaimState.INIT) {
2021-04-16 05:11:27 -06:00
throw new Error('The claim has not been matched yet')
}
2021-04-19 03:41:40 -06:00
if (this.dataMatches.length === 0) {
2021-04-16 05:11:27 -06:00
throw new Error('The claim has no matches')
2021-04-19 03:44:30 -06:00
}
2021-04-19 03:41:40 -06:00
return this.dataMatches.length > 1 || this.dataMatches[0].match.isAmbiguous
2021-04-16 05:11:27 -06:00
}
/**
2021-04-19 03:44:30 -06:00
* Get a JSON representation of the Claim object. Useful when transferring
2021-04-16 05:11:27 -06:00
* data between instances/machines.
* @function
* @returns {object}
*/
toJSON() {
return {
claimVersion: 1,
2021-04-19 03:41:40 -06:00
uri: this.uri,
fingerprint: this.fingerprint,
state: this.state,
dataMatches: this.dataMatches,
verification: this.verification,
2021-04-16 05:11:27 -06:00
}
}
}
2021-04-19 03:44:30 -06:00
module.exports = Claim