You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

86 lines
3.0 KiB

11 months ago
  1. const crypto = require("crypto");
  2. const fs = require("fs");
  3. const path = require("path");
  4. const keyPath =
  5. process.env.NODE_ENV === "development"
  6. ? path.resolve(__dirname, `../../storage/comkey`)
  7. : path.resolve(
  8. process.env.STORAGE_DIR ?? path.resolve(__dirname, `../../storage`),
  9. `comkey`
  10. );
  11. // What does this class do?
  12. // This class generates a hashed version of some text (typically a JSON payload) using a rolling RSA key
  13. // that can then be appended as a header value to do integrity checking on a payload. Given the
  14. // nature of this class and that keys are rolled constantly, this protects the request
  15. // integrity of requests sent to the collector as only the server can sign these requests.
  16. // This keeps accidental misconfigurations of AnythingLLM that leaving port 8888 open from
  17. // being abused or SSRF'd by users scraping malicious sites who have a loopback embedded in a <script>, for example.
  18. // Since each request to the collector must be signed to be valid, unsigned requests directly to the collector
  19. // will be dropped and must go through the /server endpoint directly.
  20. class CommunicationKey {
  21. #privKeyName = "ipc-priv.pem";
  22. #pubKeyName = "ipc-pub.pem";
  23. #storageLoc = keyPath;
  24. // Init the class and determine if keys should be rolled.
  25. // This typically occurs on boot up so key is fresh each boot.
  26. constructor(generate = false) {
  27. if (generate) this.#generate();
  28. }
  29. log(text, ...args) {
  30. console.log(`\x1b[36m[CommunicationKey]\x1b[0m ${text}`, ...args);
  31. }
  32. #readPrivateKey() {
  33. return fs.readFileSync(path.resolve(this.#storageLoc, this.#privKeyName));
  34. }
  35. #generate() {
  36. const keyPair = crypto.generateKeyPairSync("rsa", {
  37. modulusLength: 2048,
  38. publicKeyEncoding: {
  39. type: "pkcs1",
  40. format: "pem",
  41. },
  42. privateKeyEncoding: {
  43. type: "pkcs1",
  44. format: "pem",
  45. },
  46. });
  47. if (!fs.existsSync(this.#storageLoc))
  48. fs.mkdirSync(this.#storageLoc, { recursive: true });
  49. fs.writeFileSync(
  50. `${path.resolve(this.#storageLoc, this.#privKeyName)}`,
  51. keyPair.privateKey
  52. );
  53. fs.writeFileSync(
  54. `${path.resolve(this.#storageLoc, this.#pubKeyName)}`,
  55. keyPair.publicKey
  56. );
  57. this.log(
  58. "RSA key pair generated for signed payloads within AnythingLLM services."
  59. );
  60. }
  61. // This instance of ComKey on server is intended for generation of Priv/Pub key for signing and decoding.
  62. // this resource is shared with /collector/ via a class of the same name in /utils which does decoding/verification only
  63. // while this server class only does signing with the private key.
  64. sign(textData = "") {
  65. return crypto
  66. .sign("RSA-SHA256", Buffer.from(textData), this.#readPrivateKey())
  67. .toString("hex");
  68. }
  69. // Use the rolling priv-key to encrypt arbitrary data that is text
  70. // returns the encrypted content as a base64 string.
  71. encrypt(textData = "") {
  72. return crypto
  73. .privateEncrypt(this.#readPrivateKey(), Buffer.from(textData, "utf-8"))
  74. .toString("base64");
  75. }
  76. }
  77. module.exports = { CommunicationKey };