From 66cb8d360dd88b3bd137e4fb1e3155e156d3f550 Mon Sep 17 00:00:00 2001 From: jj Date: Fri, 1 Nov 2024 12:16:53 +0000 Subject: [PATCH] api: move hmac secrets to single file --- api/src/cobalt.js | 2 +- api/src/core/api.js | 11 ++++--- api/src/misc/cluster.js | 19 ++++++------ api/src/misc/crypto.js | 10 +------ api/src/security/secrets.js | 58 +++++++++++++++++++++++++++++++++++++ api/src/stream/manage.js | 30 ++++--------------- 6 files changed, 79 insertions(+), 51 deletions(-) create mode 100644 api/src/security/secrets.js diff --git a/api/src/cobalt.js b/api/src/cobalt.js index 4c4d3c4e..d4396ca5 100644 --- a/api/src/cobalt.js +++ b/api/src/cobalt.js @@ -20,7 +20,7 @@ app.disable("x-powered-by"); if (env.apiURL) { const { runAPI } = await import("./core/api.js"); - if (cluster.isPrimary && isCluster) { + if (isCluster) { initCluster(); } diff --git a/api/src/core/api.js b/api/src/core/api.js index 9ef0c691..2fa30cea 100644 --- a/api/src/core/api.js +++ b/api/src/core/api.js @@ -11,7 +11,7 @@ import match from "../processing/match.js"; import { env, setTunnelPort } from "../config.js"; import { extract } from "../processing/url.js"; import { Green, Bright, Cyan } from "../misc/console-text.js"; -import { generateHmac, generateSalt } from "../misc/crypto.js"; +import { hashHmac } from "../security/secrets.js"; import { randomizeCiphers } from "../misc/randomize-ciphers.js"; import { verifyTurnstileToken } from "../security/turnstile.js"; import { friendlyServiceName } from "../processing/service-alias.js"; @@ -30,7 +30,6 @@ const version = await getVersion(); const acceptRegex = /^application\/json(; charset=utf-8)?$/; -const ipSalt = generateSalt(); const corsConfig = env.corsWildcard ? {} : { origin: env.corsURL, optionsSuccessStatus: 200 @@ -74,7 +73,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { max: 10, standardHeaders: true, legacyHeaders: false, - keyGenerator: req => generateHmac(getIP(req), ipSalt), + keyGenerator: req => hashHmac(getIP(req), 'rate'), handler: handleRateExceeded }); @@ -83,7 +82,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { max: (req) => req.rateLimitMax || env.rateLimitMax, standardHeaders: true, legacyHeaders: false, - keyGenerator: req => req.rateLimitKey || generateHmac(getIP(req), ipSalt), + keyGenerator: req => req.rateLimitKey || hashHmac(getIP(req), 'rate'), handler: handleRateExceeded }) @@ -92,7 +91,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { max: (req) => req.rateLimitMax || env.rateLimitMax, standardHeaders: true, legacyHeaders: false, - keyGenerator: req => req.rateLimitKey || generateHmac(getIP(req), ipSalt), + keyGenerator: req => req.rateLimitKey || hashHmac(getIP(req), 'rate'), handler: (req, res) => { return res.sendStatus(429) } @@ -172,7 +171,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => { return fail(res, "error.api.auth.jwt.invalid"); } - req.rateLimitKey = generateHmac(token, ipSalt); + req.rateLimitKey = hashHmac(token, 'rate'); } catch { return fail(res, "error.api.generic"); } diff --git a/api/src/misc/cluster.js b/api/src/misc/cluster.js index 86ef604d..145c5588 100644 --- a/api/src/misc/cluster.js +++ b/api/src/misc/cluster.js @@ -1,6 +1,7 @@ -import net from "node:net"; import cluster from "node:cluster"; -import { isCluster } from "../config.js"; +import net from "node:net"; +import { syncSecrets } from "../security/secrets.js"; +import { env } from "../config.js"; export const supportsReusePort = async () => { try { @@ -17,13 +18,11 @@ export const supportsReusePort = async () => { } export const initCluster = async () => { - const { getSalt } = await import("../stream/manage.js"); - const salt = getSalt(); - - for (let i = 1; i < env.instanceCount; ++i) { - const worker = cluster.fork(); - worker.once('message', () => { - worker.send({ salt }); - }); + if (cluster.isPrimary) { + for (let i = 1; i < env.instanceCount; ++i) { + cluster.fork(); + } } + + await syncSecrets(); } diff --git a/api/src/misc/crypto.js b/api/src/misc/crypto.js index 70903d30..e0f8858b 100644 --- a/api/src/misc/crypto.js +++ b/api/src/misc/crypto.js @@ -1,15 +1,7 @@ -import { createHmac, createCipheriv, createDecipheriv, randomBytes } from "crypto"; +import { createCipheriv, createDecipheriv } from "crypto"; const algorithm = "aes256"; -export function generateSalt() { - return randomBytes(64); -} - -export function generateHmac(str, salt) { - return createHmac("sha256", salt).update(str).digest("base64url"); -} - export function encryptStream(plaintext, iv, secret) { const buff = Buffer.from(JSON.stringify(plaintext)); const key = Buffer.from(secret, "base64url"); diff --git a/api/src/security/secrets.js b/api/src/security/secrets.js new file mode 100644 index 00000000..2973043b --- /dev/null +++ b/api/src/security/secrets.js @@ -0,0 +1,58 @@ +import cluster from "node:cluster"; +import { createHmac, randomBytes } from "node:crypto"; + +const generateSalt = () => { + if (cluster.isPrimary) + return randomBytes(64); + + return null; +} + +let rateSalt = generateSalt(); +let streamSalt = generateSalt(); + +export const syncSecrets = () => { + return new Promise((resolve, reject) => { + if (cluster.isPrimary) { + let remaining = Object.values(cluster.workers).length; + + for (const worker of Object.values(cluster.workers)) { + worker.once('message', (m) => { + if (m.ready) + worker.send({ rateSalt, streamSalt }); + + if (!--remaining) + resolve(); + }); + } + } else if (cluster.isWorker) { + if (rateSalt || streamSalt) + return reject(); + + process.send({ ready: true }); + process.once('message', (message) => { + if (rateSalt || streamSalt) + return reject(); + + if (message.rateSalt && message.streamSalt) { + streamSalt = Buffer.from(message.streamSalt); + rateSalt = Buffer.from(message.rateSalt); + resolve(); + } + }); + } else reject(); + }); +} + + +export const hashHmac = (value, type) => { + let salt; + if (type === 'rate') + salt = rateSalt; + else if (type === 'stream') + salt = streamSalt; + else + throw "unknown salt"; + + return createHmac("sha256", salt).update(value).digest(); +} diff --git a/api/src/stream/manage.js b/api/src/stream/manage.js index 46fff483..fc6ffe2d 100644 --- a/api/src/stream/manage.js +++ b/api/src/stream/manage.js @@ -4,11 +4,11 @@ import { nanoid } from "nanoid"; import { randomBytes } from "crypto"; import { strict as assert } from "assert"; import { setMaxListeners } from "node:events"; -import cluster from "node:cluster"; -import { env, tunnelPort, isCluster } from "../config.js"; +import { env, tunnelPort } from "../config.js"; import { closeRequest } from "./shared.js"; -import { decryptStream, encryptStream, generateHmac, generateSalt } from "../misc/crypto.js"; +import { decryptStream, encryptStream } from "../misc/crypto.js"; +import { hashHmac } from "../security/secrets.js"; // optional dependency const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); @@ -16,33 +16,13 @@ const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); const streamCache = new Store('streams'); const internalStreamCache = new Map(); -let hmacSalt = cluster.isPrimary ? generateSalt() : null; -let _saltRead = false; - -export const getSalt = () => { - if (!isCluster) throw "salt can only be read on multi-process instances"; - if (!cluster.isPrimary) throw "only primary cluster can read salt"; - if (_saltRead) throw "salt was already read"; - - _saltRead = true; - return hmacSalt; -} - -if (cluster.isWorker) { - process.send({ ready: true }); - process.once('message', (message) => { - if (message.salt && !hmacSalt) { - hmacSalt = message.salt; - } - }); -} export function createStream(obj) { const streamID = nanoid(), iv = randomBytes(16).toString('base64url'), secret = randomBytes(32).toString('base64url'), exp = new Date().getTime() + env.streamLifespan * 1000, - hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt), + hmac = hashHmac(`${streamID},${exp},${iv},${secret}`, 'stream').toString('base64url'), streamData = { exp: exp, type: obj.type, @@ -167,7 +147,7 @@ function wrapStream(streamInfo) { export async function verifyStream(id, hmac, exp, secret, iv) { try { - const ghmac = generateHmac(`${id},${exp},${iv},${secret}`, hmacSalt); + const ghmac = hashHmac(`${id},${exp},${iv},${secret}`, 'stream').toString('base64url'); const cache = await streamCache.get(id.toString()); if (ghmac !== String(hmac)) return { status: 401 };