@musakui/fedi
Advanced tools
Comparing version 0.0.2 to 0.0.3
@@ -9,4 +9,4 @@ const HS2019 = 'hs2019' | ||
const numericFields = new Set([ | ||
'created', | ||
'expires', | ||
'created', | ||
'expires', | ||
]) | ||
@@ -18,9 +18,9 @@ | ||
const getSigInfo = (valid) => { | ||
const created = algorithm === HS2019 | ||
? Math.floor(new Date() / 1000) | ||
: null | ||
return { | ||
created, | ||
expires: (created && valid) ? (created + valid) : null, | ||
} | ||
const created = algorithm === HS2019 | ||
? Math.floor(new Date() / 1000) | ||
: null | ||
return { | ||
created, | ||
expires: (created && valid) ? (created + valid) : null, | ||
} | ||
} | ||
@@ -39,27 +39,27 @@ | ||
export const createString = (req, signHeaders = ['host']) => { | ||
const info = getSigInfo() | ||
const toSign = [ | ||
TARGET, | ||
info?.created ? CREATED : '', | ||
info?.expires ? EXPIRES : '', | ||
req.headers?.date ? 'date' : '', | ||
req.headers?.digest ? 'digest' : '', | ||
...signHeaders.map((i) => i.toLowerCase()), | ||
].filter((i) => i) | ||
const info = getSigInfo() | ||
const toSign = [ | ||
TARGET, | ||
info?.created ? CREATED : '', | ||
info?.expires ? EXPIRES : '', | ||
req.headers?.date ? 'date' : '', | ||
req.headers?.digest ? 'digest' : '', | ||
...signHeaders.map((i) => i.toLowerCase()), | ||
].filter((i) => i) | ||
const url = new URL(req.url) | ||
const head = new Map([ | ||
[TARGET, `${req.method.toLowerCase()} ${url.pathname}`], | ||
[CREATED, info?.created], | ||
[EXPIRES, info?.expires], | ||
...Object.entries({ | ||
host: url.host, | ||
...req.headers | ||
}).map(([k, v]) => [k.toLowerCase(), v]), | ||
]) | ||
const url = new URL(req.url) | ||
const head = new Map([ | ||
[TARGET, `${req.method.toLowerCase()} ${url.pathname}`], | ||
[CREATED, info?.created], | ||
[EXPIRES, info?.expires], | ||
...Object.entries({ | ||
host: url.host, | ||
...req.headers | ||
}).map(([k, v]) => [k.toLowerCase(), v]), | ||
]) | ||
return [ | ||
{ headers: toSign.join(' '), ...info }, | ||
toSign.map((h) => `${h}: ${head.get(h)}`).join('\n'), | ||
] | ||
return [ | ||
{ headers: toSign.join(' '), ...info }, | ||
toSign.map((h) => `${h}: ${head.get(h)}`).join('\n'), | ||
] | ||
} | ||
@@ -73,10 +73,10 @@ | ||
export const getHeader = (fields) => Object.entries({ | ||
algorithm, | ||
keyId, | ||
...fields | ||
algorithm, | ||
keyId, | ||
...fields | ||
}).map(([k, v]) => { | ||
if (!v) return null | ||
return numericFields.has(k) | ||
? `${k}=${v}` | ||
: `${k}="${v}"` | ||
if (!v) return null | ||
return numericFields.has(k) | ||
? `${k}=${v}` | ||
: `${k}="${v}"` | ||
}).filter((i) => i).join(',') | ||
@@ -99,64 +99,64 @@ | ||
export const parse = (req) => { | ||
if (!req.headers) throw new Error('no headers on request') | ||
if (!req.headers.signature) throw new Error('missing signature header') | ||
if (!req.headers) throw new Error('no headers on request') | ||
if (!req.headers.signature) throw new Error('missing signature header') | ||
const now = new Date() | ||
const now = new Date() | ||
if (req.headers.date) { | ||
const hd = new Date(req.headers.date) | ||
if (isNaN(hd)) throw new Error(`invalid date header: ${req.headers.date}`) | ||
const dt = Math.abs(now - hd) | ||
if (dt > 1e5) throw new Error(`date too far: ${dt}`) | ||
} | ||
if (req.headers.date) { | ||
const hd = new Date(req.headers.date) | ||
if (isNaN(hd)) throw new Error(`invalid date header: ${req.headers.date}`) | ||
const dt = Math.abs(now - hd) | ||
if (dt > 1e5) throw new Error(`date too far: ${dt}`) | ||
} | ||
/** | ||
* @type {SignatureField} | ||
*/ | ||
const sig = Object.fromEntries(req.headers.signature.split(',').map((s) => { | ||
const m = SIG_REGEX.exec(s.trim()) | ||
return m ? [m[1], m[2] || parseFloat(m[3])] : ['', ''] | ||
})) | ||
/** | ||
* @type {SignatureField} | ||
*/ | ||
const sig = Object.fromEntries(req.headers.signature.split(',').map((s) => { | ||
const m = SIG_REGEX.exec(s.trim()) | ||
return m ? [m[1], m[2] || parseFloat(m[3])] : ['', ''] | ||
})) | ||
if (!sig.keyId || !sig.signature) { | ||
throw new Error(`invalid signature header: ${req.headers.signature}`) | ||
} | ||
if (!sig.keyId || !sig.signature) { | ||
throw new Error(`invalid signature header: ${req.headers.signature}`) | ||
} | ||
if (sig.headers === '') throw new Error('empty headers field') | ||
if (sig.headers === '') throw new Error('empty headers field') | ||
if (sig.created) { | ||
const created = new Date(sig.created * 1000) | ||
if (isNaN(created)) throw new Error(`invalid created: ${sig.created}`) | ||
if (created > now) throw new Error(`created in future: ${created}`) | ||
} | ||
if (sig.created) { | ||
const created = new Date(sig.created * 1000) | ||
if (isNaN(created)) throw new Error(`invalid created: ${sig.created}`) | ||
if (created > now) throw new Error(`created in future: ${created}`) | ||
} | ||
if (sig.expires) { | ||
const expires = new Date(sig.expires * 1000) | ||
if (isNaN(expires)) throw new Error(`invalid expires: ${sig.expires}`) | ||
if (expires < now) throw new Error(`signature expired: ${expires}`) | ||
} | ||
if (sig.expires) { | ||
const expires = new Date(sig.expires * 1000) | ||
if (isNaN(expires)) throw new Error(`invalid expires: ${sig.expires}`) | ||
if (expires < now) throw new Error(`signature expired: ${expires}`) | ||
} | ||
const algo = sig.algorithm || HS2019 | ||
const isHs = algo === HS2019 | ||
const head = sig.headers ? sig.headers.split(' ') : [CREATED] | ||
const algo = sig.algorithm || HS2019 | ||
const isHs = algo === HS2019 | ||
const head = sig.headers ? sig.headers.split(' ') : [CREATED] | ||
const getValue = (h) => { | ||
switch (h) { | ||
case TARGET: | ||
return `${req.method.toLowerCase()} ${req.path}` | ||
case CREATED: | ||
if (isHs && parseInt(sig.created)) return sig.created | ||
break | ||
case EXPIRES: | ||
if (isHs && parseInt(sig.expires)) return sig.expires | ||
break | ||
default: | ||
return req.headers[h] | ||
} | ||
throw new Error(`invalid field: ${h}`) | ||
} | ||
const getValue = (h) => { | ||
switch (h) { | ||
case TARGET: | ||
return `${req.method.toLowerCase()} ${req.path}` | ||
case CREATED: | ||
if (isHs && parseInt(sig.created)) return sig.created | ||
break | ||
case EXPIRES: | ||
if (isHs && parseInt(sig.expires)) return sig.expires | ||
break | ||
default: | ||
return req.headers[h] | ||
} | ||
throw new Error(`invalid field: ${h}`) | ||
} | ||
return { | ||
...sig, | ||
data: head.map((h) => `${h}: ${getValue(h)}`).join('\n'), | ||
} | ||
return { | ||
...sig, | ||
data: head.map((h) => `${h}: ${getValue(h)}`).join('\n'), | ||
} | ||
} | ||
@@ -168,3 +168,3 @@ | ||
export const useAlgorithm = (algo) => { | ||
algorithm = algo | ||
algorithm = algo | ||
} | ||
@@ -176,3 +176,3 @@ | ||
export const useKeyId = (id) => { | ||
keyId = id | ||
keyId = id | ||
} |
@@ -8,52 +8,55 @@ import * as core from './core.js' | ||
const toB64 = (buf) => { | ||
const arr = Array.from(new Uint8Array(buf)) | ||
return btoa(arr.reduce((s, c) => s + String.fromCharCode(c), '')) | ||
const arr = Array.from(new Uint8Array(buf)) | ||
return btoa(arr.reduce((s, c) => s + String.fromCharCode(c), '')) | ||
} | ||
const getDigest = async (data, algo = 'SHA-256') => { | ||
const dg = await crypto.subtle.digest(algo, encoder.encode(data)) | ||
return `${algo}=${toB64(dg)}` | ||
const dg = await crypto.subtle.digest(algo, encoder.encode(data)) | ||
return `${algo}=${toB64(dg)}` | ||
} | ||
export const signRequest = async (req) => { | ||
if (!core.keyId || !sign) throw new Error('key not set') | ||
const [info, data] = core.createString(req) | ||
const signature = core.getHeader({ ...info, signature: await sign(data) }) | ||
return { | ||
...req, | ||
headers: { | ||
...req.headers, | ||
signature, | ||
}, | ||
} | ||
if (!core.keyId || !sign) throw new Error('key not set') | ||
const [info, data] = core.createString(req) | ||
const signature = core.getHeader({ ...info, signature: await sign(data) }) | ||
return { | ||
...req, | ||
headers: { | ||
...req.headers, | ||
signature, | ||
}, | ||
} | ||
} | ||
export const sendRequest = async (req) => { | ||
const signed = await signRequest({ | ||
...req, | ||
method: req.body ? 'POST' : 'GET', | ||
headers: { | ||
date: (new Date()).toUTCString(), | ||
...(req.body ? { digest: await getDigest(req.body) } : {}), | ||
...req.headers, | ||
}, | ||
}) | ||
const signed = await signRequest({ | ||
...req, | ||
method: req.body ? 'POST' : 'GET', | ||
headers: { | ||
date: (new Date()).toUTCString(), | ||
...(req.body ? { digest: await getDigest(req.body) } : {}), | ||
...req.headers, | ||
}, | ||
}) | ||
const resp = await fetch(req.url, signed) | ||
try { | ||
return await resp.json() | ||
} catch (err) { | ||
return await resp.text() | ||
} | ||
const resp = await fetch(req.url, signed) | ||
if (!resp.ok) { | ||
throw new Error(`${resp.status} ${resp.statusText}`) | ||
} | ||
try { | ||
return await resp.json() | ||
} catch (err) { | ||
return await resp.text() | ||
} | ||
} | ||
export const useKey = (id, key) => { | ||
core.useKeyId(id) | ||
// TODO: implement browser signing | ||
sign = (data) => '' | ||
core.useKeyId(id) | ||
// TODO: implement browser signing | ||
sign = (data) => '' | ||
} | ||
export { | ||
algorithm, | ||
useAlgorithm, | ||
algorithm, | ||
useAlgorithm, | ||
} from './core.js' |
@@ -19,14 +19,14 @@ import { createSign, createVerify, createHash } from 'crypto' | ||
const getDigest = (data, algo = S256) => { | ||
let hasher = null | ||
switch (algo) { | ||
case S256: | ||
hasher = createHash('sha256') | ||
break | ||
case 'SHA-512': | ||
hasher = createHash('sha512') | ||
break | ||
default: | ||
return undefined | ||
} | ||
return `${algo}=${hasher.update(data).digest(B64)}` | ||
let hasher = null | ||
switch (algo) { | ||
case S256: | ||
hasher = createHash('sha256') | ||
break | ||
case 'SHA-512': | ||
hasher = createHash('sha512') | ||
break | ||
default: | ||
return undefined | ||
} | ||
return `${algo}=${hasher.update(data).digest(B64)}` | ||
} | ||
@@ -38,12 +38,12 @@ | ||
export const signRequest = (req) => { | ||
if (!sign) throw new Error('key not set') | ||
const [info, data] = core.createString(req) | ||
const signature = core.getHeader({ ...info, signature: sign(data) }) | ||
return { | ||
...req, | ||
headers: { | ||
...req.headers, | ||
signature, | ||
}, | ||
} | ||
if (!sign) throw new Error('key not set') | ||
const [info, data] = core.createString(req) | ||
const signature = core.getHeader({ ...info, signature: sign(data) }) | ||
return { | ||
...req, | ||
headers: { | ||
...req.headers, | ||
signature, | ||
}, | ||
} | ||
} | ||
@@ -55,13 +55,13 @@ | ||
export const sendRequest = async (req) => { | ||
if (!myFetch) throw new Error('fetch not set') | ||
if (!myFetch) throw new Error('fetch not set') | ||
return await myFetch(req.url, signRequest({ | ||
...req, | ||
method: req.body ? 'POST' : 'GET', | ||
headers: { | ||
date: (new Date()).toUTCString(), | ||
...(req.body ? { digest: getDigest(req.body) } : {}), | ||
...req.headers, | ||
}, | ||
})) | ||
return await myFetch(req.url, signRequest({ | ||
...req, | ||
method: req.body ? 'POST' : 'GET', | ||
headers: { | ||
date: (new Date()).toUTCString(), | ||
...(req.body ? { digest: getDigest(req.body) } : {}), | ||
...req.headers, | ||
}, | ||
})) | ||
} | ||
@@ -74,14 +74,14 @@ | ||
export const getKey = async (id, accept = appJSON) => { | ||
const headers = { accept } | ||
try { | ||
const resp = await myFetch(id, { headers }) | ||
return await resp.json() | ||
} catch (err) { | ||
// some sites require signed GET | ||
const resp = await sendRequest({ | ||
url: id, | ||
headers, | ||
}) | ||
return await resp.json() | ||
} | ||
const headers = { accept } | ||
try { | ||
const resp = await myFetch(id, { headers }) | ||
return await resp.json() | ||
} catch (err) { | ||
// some sites require signed GET | ||
const resp = await sendRequest({ | ||
url: id, | ||
headers, | ||
}) | ||
return await resp.json() | ||
} | ||
} | ||
@@ -94,23 +94,23 @@ | ||
export const verifyRequest = async (req) => { | ||
if (req.body && req.headers.digest) { | ||
const dg = req.headers.digest | ||
const ha = dg.split('=')[0] | ||
if (dg !== getDigest(req.body, ha)) throw new Error(`invalid digest: ${dg}`) | ||
} | ||
if (req.body && req.headers.digest) { | ||
const dg = req.headers.digest | ||
const ha = dg.split('=')[0] | ||
if (dg !== getDigest(req.body, ha)) throw new Error(`invalid digest: ${dg}`) | ||
} | ||
const sig = core.parse(req) | ||
const { publicKey: pk, ...rest } = await getKey(sig.keyId) | ||
const sig = core.parse(req) | ||
const { publicKey: pk, ...rest } = await getKey(sig.keyId) | ||
if (!pk || !pk.publicKeyPem) { | ||
throw new Error(`failed to get key: ${sig.keyId}`) | ||
} | ||
if (!pk || !pk.publicKeyPem) { | ||
throw new Error(`failed to get key: ${sig.keyId}`) | ||
} | ||
if (pk.id && pk.id !== sig.keyId) { | ||
throw new Error(`invalid keyId: ${sig.keyId} != ${pk.id}`) | ||
} | ||
if (pk.id && pk.id !== sig.keyId) { | ||
throw new Error(`invalid keyId: ${sig.keyId} != ${pk.id}`) | ||
} | ||
const veri = createVerify(sig.algorithm).update(sig.data) | ||
if (veri.verify(pk.publicKeyPem, sig.signature, B64)) return rest | ||
const veri = createVerify(sig.algorithm).update(sig.data) | ||
if (veri.verify(pk.publicKeyPem, sig.signature, B64)) return rest | ||
throw new Error('verification failed') | ||
throw new Error('verification failed') | ||
} | ||
@@ -124,12 +124,12 @@ | ||
export const useKey = (id, key, algo = 'sha256') => { | ||
core.useKeyId(id) | ||
sign = (data) => createSign(algo).update(data).sign(key).toString(B64) | ||
core.useKeyId(id) | ||
sign = (data) => createSign(algo).update(data).sign(key).toString(B64) | ||
} | ||
export const useFetch = (fn) => { | ||
myFetch = fn | ||
myFetch = fn | ||
} | ||
try { | ||
myFetch = fetch | ||
myFetch = fetch | ||
} catch (err) { | ||
@@ -136,0 +136,0 @@ } |
import * as Keys from './keys.js' | ||
import * as Signatures from './hs/index.js' | ||
import * as WebFinger from './webfinger.js' | ||
import * as ActivityPub from './activitypub/index.js' | ||
import * as ActivityStreams from './activitystreams/index.js' | ||
export { | ||
Keys, | ||
Signatures, | ||
Keys, | ||
WebFinger, | ||
ActivityPub, | ||
ActivityStreams, | ||
} |
{ | ||
"name": "@musakui/fedi", | ||
"description": "tools for the fediverse", | ||
"keywords": [ | ||
"ActivityPub" | ||
], | ||
"author": "musakui", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/musakui/fedi" | ||
}, | ||
"version": "0.0.2", | ||
"type": "module", | ||
"files": [ | ||
"lib" | ||
], | ||
"exports": { | ||
"./hs": { | ||
"node": "./lib/hs/node.js", | ||
"default": "./lib/hs/index.js" | ||
}, | ||
".": "./lib/index.js" | ||
}, | ||
"scripts": { | ||
}, | ||
"devDependencies": { | ||
} | ||
"name": "@musakui/fedi", | ||
"description": "tools for the fediverse", | ||
"keywords": [ | ||
"ActivityPub" | ||
], | ||
"author": "musakui", | ||
"license": "MIT", | ||
"repository": "github:musakui/fedi", | ||
"version": "0.0.3", | ||
"type": "module", | ||
"files": [ | ||
"lib" | ||
], | ||
"exports": { | ||
"./activitypub": "./lib/activitypub/index.js", | ||
"./activitystreams": "./lib/activitystreams/index.js", | ||
"./hs": { | ||
"node": "./lib/hs/node.js", | ||
"default": "./lib/hs/index.js" | ||
}, | ||
"./webfinger": "./lib/webfinger.js", | ||
".": "./lib/index.js" | ||
}, | ||
"scripts": { | ||
}, | ||
"devDependencies": { | ||
} | ||
} |
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
11220
10
364
1