Deprecate claims.js, introduce proofs.js

This commit is contained in:
Yarmo Mackenbach 2021-04-16 10:56:33 +02:00
parent e99a0307bf
commit 2afb30d3e4
No known key found for this signature in database
GPG key ID: 37367F4AF4087AD1
3 changed files with 171 additions and 338 deletions

View file

@ -1,337 +0,0 @@
/*
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 mergeOptions = require('merge-options')
const validUrl = require('valid-url')
const openpgp = require('openpgp')
const serviceproviders = require('./serviceproviders')
const keys = require('./keys')
const utils = require('./utils')
// Promise.allSettled polyfill
Promise.allSettled =
Promise.allSettled ||
((promises) =>
Promise.all(
promises.map((p) =>
p
.then((v) => ({
status: 'fulfilled',
value: v,
}))
.catch((e) => ({
status: 'rejected',
reason: e,
}))
)
))
const runVerificationJson = (
res,
proofData,
checkPath,
checkClaim,
checkRelation
) => {
let re
if (res.isVerified || !proofData) {
return res
}
if (Array.isArray(proofData)) {
proofData.forEach((item, i) => {
res = runVerificationJson(res, item, checkPath, checkClaim, checkRelation)
})
return res
}
if (checkPath.length == 0) {
switch (checkRelation) {
default:
case E.ClaimRelation.CONTAINS:
re = new RegExp(checkClaim, 'gi')
res.isVerified = re.test(proofData.replace(/\r?\n|\r|\\/g, ''))
break
case E.ClaimRelation.EQUALS:
res.isVerified =
proofData.replace(/\r?\n|\r|\\/g, '').toLowerCase() ==
checkClaim.toLowerCase()
break
case E.ClaimRelation.ONEOF:
re = new RegExp(checkClaim, 'gi')
res.isVerified = re.test(proofData.join('|'))
break
}
return res
}
try {
checkPath[0] in proofData
} catch (e) {
res.errors.push('err_data_structure_incorrect')
return res
}
res = runVerificationJson(
res,
proofData[checkPath[0]],
checkPath.slice(1),
checkClaim,
checkRelation
)
return res
}
const runVerification = (proofData, spData) => {
let res = {
isVerified: false,
errors: [],
}
switch (spData.proof.format) {
case E.ProofFormat.JSON:
res = runVerificationJson(
res,
proofData,
spData.claim.path,
utils.generateClaim(spData.claim.fingerprint, spData.claim.format),
spData.claim.relation
)
break
case E.ProofFormat.TEXT:
re = new RegExp(
utils
.generateClaim(spData.claim.fingerprint, spData.claim.format)
.replace('[', '\\[')
.replace(']', '\\]'),
'gi'
)
res.isVerified = re.test(proofData.replace(/\r?\n|\r/, ''))
break
}
return res
}
const verify = async (input, fingerprint, opts) => {
if (input instanceof openpgp.key.Key) {
const fingerprintFromKey = await keys.getFingerprint(input)
const userData = await keys.getUserData(input)
const promises = userData.map(async (user, i) => {
return new Promise(async (resolve, reject) => {
try {
const res = await verify(user.notations, fingerprintFromKey, opts)
resolve(res)
} catch (e) {
reject(e)
}
})
})
return Promise.allSettled(promises).then((values) => {
return values.map((obj, i) => {
if (obj.status == 'fulfilled') {
return obj.value
} else {
return obj.reason
}
})
})
}
if (input instanceof Array) {
const promises = input.map(async (uri, i) => {
return new Promise(async (resolve, reject) => {
try {
const res = await verify(uri, fingerprint, opts)
resolve(res)
} catch (e) {
reject(e)
}
})
})
return Promise.allSettled(promises).then((values) => {
return values.map((obj, i) => {
if (obj.status == 'fulfilled') {
return obj.value
} else {
return obj.reason
}
})
})
}
const promiseClaim = new Promise(async (resolve, reject) => {
let objResult = {
isVerified: false,
errors: [],
serviceproviderData: undefined,
}
const uri = input.replace(/^\s+|\s+$/g, '')
if (!fingerprint) {
fingerprint = null
}
const defaultOpts = {
returnMatchesOnly: false,
proxyPolicy: 'adaptive',
doipProxyHostname: 'proxy.keyoxide.org',
twitterBearerToken: null,
nitterInstance: null,
}
opts = mergeOptions(defaultOpts, opts ? opts : {})
if (!validUrl.isUri(uri)) {
objResult.errors.push('invalid_uri')
reject(objResult)
return
}
const spMatches = serviceproviders.match(uri, opts)
if ('returnMatchesOnly' in opts && opts.returnMatchesOnly) {
resolve(spMatches)
return
}
let claimVerificationDone = false,
claimVerificationResult,
sp,
iSp = 0,
res,
proofData,
spData
while (!claimVerificationDone && iSp < spMatches.length) {
spData = spMatches[iSp]
spData.claim.fingerprint = fingerprint
res = null
if (spData.customRequestHandler instanceof Function) {
try {
proofData = await spData.customRequestHandler(spData, opts)
} catch (e) {
objResult.errors.push('custom_request_handler_failed')
}
} else {
switch (opts.proxyPolicy) {
case 'adaptive':
if (spData.proof.useProxy) {
try {
proofData = await serviceproviders.proxyRequestHandler(
spData,
opts
)
} catch (er) {}
} else {
try {
proofData = await serviceproviders.directRequestHandler(
spData,
opts
)
} catch (er) {}
if (!proofData) {
try {
proofData = await serviceproviders.proxyRequestHandler(
spData,
opts
)
} catch (er) {}
}
}
break
case 'fallback':
try {
proofData = await serviceproviders.directRequestHandler(
spData,
opts
)
} catch (er) {}
if (!proofData) {
try {
proofData = await serviceproviders.proxyRequestHandler(
spData,
opts
)
} catch (er) {}
}
break
case 'always':
try {
proofData = await serviceproviders.proxyRequestHandler(
spData,
opts
)
} catch (er) {}
break
case 'never':
try {
proofData = await serviceproviders.directRequestHandler(
spData,
opts
)
} catch (er) {}
break
default:
objResult.errors.push('invalid_proxy_policy')
}
}
if (proofData) {
claimVerificationResult = runVerification(proofData, spData)
if (claimVerificationResult.errors.length == 0) {
claimVerificationDone = true
}
} else {
objResult.errors.push('unsuccessful_claim_verification')
}
iSp++
}
if (!claimVerificationResult) {
claimVerificationResult = {
isVerified: false,
}
}
objResult.isVerified = claimVerificationResult.isVerified
objResult.serviceproviderData = spData
resolve(objResult)
return
})
const promiseTimeout = new Promise((resolve) => {
const objResult = {
isVerified: false,
errors: ['verification_timed_out'],
serviceproviderData: undefined,
}
setTimeout(() => {
resolve(objResult)
return
}, 10000)
})
return await Promise.race([promiseClaim, promiseTimeout])
}
exports.verify = verify

View file

@ -13,7 +13,7 @@ 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 claims = require('./claims')
const proofs = require('./proofs')
const keys = require('./keys')
const signatures = require('./signatures')
const serviceproviders = require('./serviceproviders')

170
src/proofs.js Normal file
View file

@ -0,0 +1,170 @@
/*
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 jsEnv = require('browser-or-node')
const fetcher = require('./fetcher')
const utils = require('./utils')
const E = require('./enums')
const fetch = (data, opts) => {
switch (data.proof.request.fetcher) {
case E.Fetcher.HTTP:
data.proof.request.data.format = data.proof.request.format
break
default:
break
}
if (jsEnv.isNode) {
return handleNodeRequests(data, opts)
}
return handleBrowserRequests(data, opts)
}
const handleBrowserRequests = (data, opts) => {
switch (opts.proxy.policy) {
case E.ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts)
break
case E.ProxyPolicy.NEVER:
switch (data.proof.request.access) {
case E.ProofAccess.GENERIC:
case E.ProofAccess.GRANTED:
return createDefaultRequestPromise(data, opts)
break
case E.ProofAccess.NOCORS:
case E.ProofAccess.SERVER:
throw new Error('Impossible to fetch proof (bad combination of service access and proxy policy)')
break
default:
throw new Error('Invalid proof access value')
break
}
break
case E.ProxyPolicy.ADAPTIVE:
switch (data.proof.request.access) {
case E.ProofAccess.GENERIC:
return createDefaultRequestPromise(data, opts)
break
case E.ProofAccess.NOCORS:
return createProxyRequestPromise(data, opts)
break
case E.ProofAccess.GRANTED:
return createFallbackRequestPromise(data, opts)
break
case E.ProofAccess.SERVER:
throw new Error('Impossible to fetch proof (bad combination of service access and proxy policy)')
break
default:
throw new Error('Invalid proof access value')
break
}
break
default:
throw new Error('Invalid proxy policy')
break
}
}
const handleNodeRequests = (data, opts) => {
switch (opts.proxy.policy) {
case E.ProxyPolicy.ALWAYS:
return createProxyRequestPromise(data, opts)
break
case E.ProxyPolicy.NEVER:
return createDefaultRequestPromise(data, opts)
break
case E.ProxyPolicy.ADAPTIVE:
return createFallbackRequestPromise(data, opts)
break
default:
throw new Error('Invalid proxy policy')
break
}
}
const createDefaultRequestPromise = (data, opts) => {
return new Promise((resolve, reject) => {
fetcher[data.proof.request.fetcher].fn(data.proof.request.data, opts)
.then(res => {
return resolve({
fetcher: data.proof.request.fetcher,
data: data,
viaProxy: false,
result: res
})
})
.catch(err => {
return reject(err)
})
})
}
const createProxyRequestPromise = (data, opts) => {
return new Promise((resolve, reject) => {
let proxyUrl
try {
proxyUrl = utils.generateProxyURL(data.proof.request.fetcher, data.proof.request.data, opts);
} catch (err) {
reject(err)
}
const requestData = {
url: proxyUrl,
format: data.proof.request.format,
fetcherTimeout: fetcher[data.proof.request.fetcher].timeout
}
fetcher.http.fn(requestData, opts)
.then(res => {
return resolve({
fetcher: 'http',
data: data,
viaProxy: true,
result: res
})
})
.catch(err => {
return reject(err)
})
})
}
const createFallbackRequestPromise = (data, opts) => {
return new Promise((resolve, reject) => {
createDefaultRequestPromise(data, opts)
.then(res => {
return resolve(res)
})
.catch(err1 => {
createProxyRequestPromise(data, opts)
.then(res => {
return resolve(res)
})
.catch(err2 => {
return reject([err1, err2])
})
})
})
}
exports.fetch = fetch