2023-05-17 13:13:30 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
2023-05-29 17:42:44 -04:00
|
|
|
const request = require('request-promise-native');
|
2023-06-19 17:29:22 -04:00
|
|
|
const url = require('url');
|
|
|
|
|
const nconf = require('nconf');
|
|
|
|
|
const { createHash, createSign, createVerify } = require('crypto');
|
2023-05-17 13:13:30 -04:00
|
|
|
|
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-05-17 13:13:30 -04:00
|
|
|
|
|
|
|
|
const ActivityPub = module.exports;
|
|
|
|
|
|
2023-06-02 14:22:43 -04:00
|
|
|
ActivityPub.helpers = require('./helpers');
|
|
|
|
|
|
2023-05-29 17:42:44 -04:00
|
|
|
ActivityPub.getActor = async (id) => {
|
2023-06-02 14:22:43 -04:00
|
|
|
const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id);
|
2023-05-29 17:42:44 -04:00
|
|
|
if (!uri) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const actor = await request({
|
|
|
|
|
uri,
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
|
|
|
},
|
|
|
|
|
json: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
actor.hostname = hostname;
|
|
|
|
|
|
|
|
|
|
return actor;
|
|
|
|
|
};
|
|
|
|
|
|
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
|
|
|
|
|
const { publicKey } = await request({
|
|
|
|
|
uri,
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
2023-05-17 13:13:30 -04:00
|
|
|
},
|
2023-06-19 17:29:22 -04:00
|
|
|
json: true,
|
2023-05-17 13:13:30 -04:00
|
|
|
});
|
|
|
|
|
|
2023-06-19 17:29:22 -04:00
|
|
|
return publicKey;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
digest = payloadHash.digest('hex');
|
|
|
|
|
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)') {
|
|
|
|
|
memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.path}`);
|
|
|
|
|
} 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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* This is just some code to test signing and verification. This should really be in the test suite.
|
|
|
|
|
*/
|
|
|
|
|
// setTimeout(async () => {
|
|
|
|
|
// const payload = {
|
|
|
|
|
// foo: 'bar',
|
|
|
|
|
// };
|
|
|
|
|
// const signature = await ActivityPub.sign(1, 'http://127.0.0.1:4567/user/julian/inbox', payload);
|
|
|
|
|
|
|
|
|
|
// const res = await request({
|
|
|
|
|
// uri: 'http://127.0.0.1:4567/user/julian/inbox',
|
|
|
|
|
// method: 'post',
|
|
|
|
|
// headers: {
|
|
|
|
|
// Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
|
|
|
// ...signature,
|
|
|
|
|
// },
|
|
|
|
|
// json: true,
|
|
|
|
|
// body: payload,
|
|
|
|
|
// simple: false,
|
|
|
|
|
// });
|
|
|
|
|
|
|
|
|
|
// console.log(res);
|
|
|
|
|
// }, 1000);
|