| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							|  |  |  | const { createHash, createSign, createVerify } = require('crypto'); | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | const validator = require('validator'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | const request = require('../request'); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | const db = require('../database'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const user = require('../user'); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const ttl = require('../cache/ttl'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours
 | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | const ActivityPub = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | ActivityPub.helpers = require('./helpers'); | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | ActivityPub.inbox = require('./inbox'); | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | ActivityPub.getActor = async (input) => { | 
					
						
							|  |  |  | 	// Can be a webfinger id, uri, or object, handle as appropriate
 | 
					
						
							|  |  |  | 	let uri; | 
					
						
							|  |  |  | 	if (validator.isURL(input, { | 
					
						
							|  |  |  | 		require_protocol: true, | 
					
						
							|  |  |  | 		require_host: true, | 
					
						
							|  |  |  | 		protocols: ['https'], | 
					
						
							|  |  |  | 		require_valid_protocol: true, | 
					
						
							|  |  |  | 	})) { | 
					
						
							|  |  |  | 		uri = input; | 
					
						
							|  |  |  | 	} else if (input.indexOf('@') !== -1) { // Webfinger
 | 
					
						
							|  |  |  | 		({ actorUri: uri } = await ActivityPub.helpers.query(input)); | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		throw new Error('[[error:invalid-data]]'); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-14 13:47:28 -05:00
										 |  |  | 	if (!uri) { | 
					
						
							|  |  |  | 		throw new Error('[[error:invalid-uid]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	if (actorCache.has(uri)) { | 
					
						
							|  |  |  | 		return actorCache.get(uri); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 	const { body: actor } = await request.get(uri, { | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 		headers: { | 
					
						
							|  |  |  | 			Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	actor.hostname = new URL(uri).hostname; | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	actorCache.set(uri, actor); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 	return actor; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | ActivityPub.resolveInboxes = async ids => await Promise.all(ids.map(async (id) => { | 
					
						
							|  |  |  | 	const actor = await ActivityPub.getActor(id); | 
					
						
							|  |  |  | 	return actor.inbox; | 
					
						
							|  |  |  | })); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | ActivityPub.getPublicKey = async (uid) => { | 
					
						
							|  |  |  | 	let publicKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		({ publicKey } = await db.getObject(`uid:${uid}:keys`)); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		({ publicKey } = await ActivityPub.helpers.generateKeys(uid)); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return publicKey; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | ActivityPub.getPrivateKey = async (uid) => { | 
					
						
							|  |  |  | 	let privateKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		({ privateKey } = await db.getObject(`uid:${uid}:keys`)); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		({ privateKey } = await ActivityPub.helpers.generateKeys(uid)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return privateKey; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.fetchPublicKey = async (uri) => { | 
					
						
							|  |  |  | 	// Used for retrieving the public key from the passed-in keyId uri
 | 
					
						
							| 
									
										
										
										
											2023-12-21 14:38:16 -05:00
										 |  |  | 	const { body } = await request.get(uri, { | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		headers: { | 
					
						
							|  |  |  | 			Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 	return body.publicKey; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.sign = async (uid, url, payload) => { | 
					
						
							|  |  |  | 	// Returns string for use in 'Signature' header
 | 
					
						
							|  |  |  | 	const { host, pathname } = new URL(url); | 
					
						
							|  |  |  | 	const date = new Date().toUTCString(); | 
					
						
							|  |  |  | 	const key = await ActivityPub.getPrivateKey(uid); | 
					
						
							|  |  |  | 	const userslug = await user.getUserField(uid, 'userslug'); | 
					
						
							|  |  |  | 	const keyId = `${nconf.get('url')}/user/${userslug}#key`; | 
					
						
							|  |  |  | 	let digest = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	let headers = '(request-target) host date'; | 
					
						
							|  |  |  | 	let signed_string = `(request-target): ${payload ? 'post' : 'get'} ${pathname}\nhost: ${host}\ndate: ${date}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Calculate payload hash if payload present
 | 
					
						
							|  |  |  | 	if (payload) { | 
					
						
							|  |  |  | 		const payloadHash = createHash('sha256'); | 
					
						
							|  |  |  | 		payloadHash.update(JSON.stringify(payload)); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 		digest = `sha-256=${payloadHash.digest('base64')}`; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		headers += ' digest'; | 
					
						
							|  |  |  | 		signed_string += `\ndigest: ${digest}`; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Sign string using private key
 | 
					
						
							|  |  |  | 	const signatureHash = createHash('sha256'); | 
					
						
							|  |  |  | 	signatureHash.update(signed_string); | 
					
						
							|  |  |  | 	const signatureDigest = signatureHash.digest('hex'); | 
					
						
							|  |  |  | 	let signature = createSign('sha256'); | 
					
						
							|  |  |  | 	signature.update(signatureDigest); | 
					
						
							|  |  |  | 	signature.end(); | 
					
						
							|  |  |  | 	signature = signature.sign(key, 'hex'); | 
					
						
							|  |  |  | 	signature = btoa(signature); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Construct signature header
 | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		date, | 
					
						
							|  |  |  | 		digest, | 
					
						
							|  |  |  | 		signature: `keyId="${keyId}",headers="${headers}",signature="${signature}"`, | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.verify = async (req) => { | 
					
						
							|  |  |  | 	// Break the signature apart
 | 
					
						
							|  |  |  | 	const { keyId, headers, signature } = req.headers.signature.split(',').reduce((memo, cur) => { | 
					
						
							|  |  |  | 		const split = cur.split('="'); | 
					
						
							|  |  |  | 		const key = split.shift(); | 
					
						
							|  |  |  | 		const value = split.join('="'); | 
					
						
							|  |  |  | 		memo[key] = value.slice(0, -1); | 
					
						
							|  |  |  | 		return memo; | 
					
						
							|  |  |  | 	}, {}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Retrieve public key from remote instance
 | 
					
						
							|  |  |  | 	const { publicKeyPem } = await ActivityPub.fetchPublicKey(keyId); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Re-construct signature string
 | 
					
						
							|  |  |  | 	const signed_string = headers.split(' ').reduce((memo, cur) => { | 
					
						
							|  |  |  | 		if (cur === '(request-target)') { | 
					
						
							| 
									
										
										
										
											2023-08-08 15:33:35 -04:00
										 |  |  | 			memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.baseUrl}${req.path}`); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		} else if (req.headers.hasOwnProperty(cur)) { | 
					
						
							|  |  |  | 			memo.push(`${cur}: ${req.headers[cur]}`); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return memo; | 
					
						
							|  |  |  | 	}, []).join('\n'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Verify the signature string via public key
 | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		const signatureHash = createHash('sha256'); | 
					
						
							|  |  |  | 		signatureHash.update(signed_string); | 
					
						
							|  |  |  | 		const signatureDigest = signatureHash.digest('hex'); | 
					
						
							|  |  |  | 		const verify = createVerify('sha256'); | 
					
						
							|  |  |  | 		verify.update(signatureDigest); | 
					
						
							|  |  |  | 		verify.end(); | 
					
						
							|  |  |  | 		const verified = verify.verify(publicKeyPem, atob(signature), 'hex'); | 
					
						
							|  |  |  | 		return verified; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | ActivityPub.send = async (uid, targets, payload) => { | 
					
						
							|  |  |  | 	if (!Array.isArray(targets)) { | 
					
						
							|  |  |  | 		targets = [targets]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const userslug = await user.getUserField(uid, 'userslug'); | 
					
						
							|  |  |  | 	const inboxes = await ActivityPub.resolveInboxes(targets); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	payload = { | 
					
						
							| 
									
										
										
										
											2023-12-13 13:15:03 -05:00
										 |  |  | 		'@context': 'https://www.w3.org/ns/activitystreams', | 
					
						
							|  |  |  | 		actor: { | 
					
						
							|  |  |  | 			type: 'Person', | 
					
						
							|  |  |  | 			name: `${userslug}@${nconf.get('url_parsed').host}`, | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 		...payload, | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	await Promise.all(inboxes.map(async (uri) => { | 
					
						
							|  |  |  | 		const { date, digest, signature } = await ActivityPub.sign(uid, uri, payload); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 		const response = await request.post(uri, { | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 			headers: { | 
					
						
							|  |  |  | 				date, | 
					
						
							|  |  |  | 				digest, | 
					
						
							|  |  |  | 				signature, | 
					
						
							| 
									
										
										
										
											2023-06-23 15:25:00 -04:00
										 |  |  | 				'content-type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 				accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 			}, | 
					
						
							|  |  |  | 			body: payload, | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-26 16:15:25 -04:00
										 |  |  | 		if (response.statusCode !== 201) { | 
					
						
							|  |  |  | 			// todo: i18n this
 | 
					
						
							|  |  |  | 			throw new Error('activity-failed'); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	})); | 
					
						
							|  |  |  | }; |