api: move hmac secrets to single file

This commit is contained in:
jj 2024-11-01 12:16:53 +00:00
parent 40d6a02b61
commit 66cb8d360d
No known key found for this signature in database
6 changed files with 79 additions and 51 deletions

View file

@ -20,7 +20,7 @@ app.disable("x-powered-by");
if (env.apiURL) { if (env.apiURL) {
const { runAPI } = await import("./core/api.js"); const { runAPI } = await import("./core/api.js");
if (cluster.isPrimary && isCluster) { if (isCluster) {
initCluster(); initCluster();
} }

View file

@ -11,7 +11,7 @@ import match from "../processing/match.js";
import { env, setTunnelPort } from "../config.js"; import { env, setTunnelPort } from "../config.js";
import { extract } from "../processing/url.js"; import { extract } from "../processing/url.js";
import { Green, Bright, Cyan } from "../misc/console-text.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 { randomizeCiphers } from "../misc/randomize-ciphers.js";
import { verifyTurnstileToken } from "../security/turnstile.js"; import { verifyTurnstileToken } from "../security/turnstile.js";
import { friendlyServiceName } from "../processing/service-alias.js"; import { friendlyServiceName } from "../processing/service-alias.js";
@ -30,7 +30,6 @@ const version = await getVersion();
const acceptRegex = /^application\/json(; charset=utf-8)?$/; const acceptRegex = /^application\/json(; charset=utf-8)?$/;
const ipSalt = generateSalt();
const corsConfig = env.corsWildcard ? {} : { const corsConfig = env.corsWildcard ? {} : {
origin: env.corsURL, origin: env.corsURL,
optionsSuccessStatus: 200 optionsSuccessStatus: 200
@ -74,7 +73,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => {
max: 10, max: 10,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: req => generateHmac(getIP(req), ipSalt), keyGenerator: req => hashHmac(getIP(req), 'rate'),
handler: handleRateExceeded handler: handleRateExceeded
}); });
@ -83,7 +82,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => {
max: (req) => req.rateLimitMax || env.rateLimitMax, max: (req) => req.rateLimitMax || env.rateLimitMax,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: req => req.rateLimitKey || generateHmac(getIP(req), ipSalt), keyGenerator: req => req.rateLimitKey || hashHmac(getIP(req), 'rate'),
handler: handleRateExceeded handler: handleRateExceeded
}) })
@ -92,7 +91,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => {
max: (req) => req.rateLimitMax || env.rateLimitMax, max: (req) => req.rateLimitMax || env.rateLimitMax,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: req => req.rateLimitKey || generateHmac(getIP(req), ipSalt), keyGenerator: req => req.rateLimitKey || hashHmac(getIP(req), 'rate'),
handler: (req, res) => { handler: (req, res) => {
return res.sendStatus(429) return res.sendStatus(429)
} }
@ -172,7 +171,7 @@ export const runAPI = (express, app, __dirname, isPrimary = true) => {
return fail(res, "error.api.auth.jwt.invalid"); return fail(res, "error.api.auth.jwt.invalid");
} }
req.rateLimitKey = generateHmac(token, ipSalt); req.rateLimitKey = hashHmac(token, 'rate');
} catch { } catch {
return fail(res, "error.api.generic"); return fail(res, "error.api.generic");
} }

View file

@ -1,6 +1,7 @@
import net from "node:net";
import cluster from "node:cluster"; 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 () => { export const supportsReusePort = async () => {
try { try {
@ -17,13 +18,11 @@ export const supportsReusePort = async () => {
} }
export const initCluster = async () => { export const initCluster = async () => {
const { getSalt } = await import("../stream/manage.js"); if (cluster.isPrimary) {
const salt = getSalt();
for (let i = 1; i < env.instanceCount; ++i) { for (let i = 1; i < env.instanceCount; ++i) {
const worker = cluster.fork(); cluster.fork();
worker.once('message', () => {
worker.send({ salt });
});
} }
} }
await syncSecrets();
}

View file

@ -1,15 +1,7 @@
import { createHmac, createCipheriv, createDecipheriv, randomBytes } from "crypto"; import { createCipheriv, createDecipheriv } from "crypto";
const algorithm = "aes256"; 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) { export function encryptStream(plaintext, iv, secret) {
const buff = Buffer.from(JSON.stringify(plaintext)); const buff = Buffer.from(JSON.stringify(plaintext));
const key = Buffer.from(secret, "base64url"); const key = Buffer.from(secret, "base64url");

View file

@ -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();
}

View file

@ -4,11 +4,11 @@ import { nanoid } from "nanoid";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import { setMaxListeners } from "node:events"; 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 { 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 // optional dependency
const freebind = env.freebindCIDR && await import('freebind').catch(() => {}); 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 streamCache = new Store('streams');
const internalStreamCache = new Map(); 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) { export function createStream(obj) {
const streamID = nanoid(), const streamID = nanoid(),
iv = randomBytes(16).toString('base64url'), iv = randomBytes(16).toString('base64url'),
secret = randomBytes(32).toString('base64url'), secret = randomBytes(32).toString('base64url'),
exp = new Date().getTime() + env.streamLifespan * 1000, exp = new Date().getTime() + env.streamLifespan * 1000,
hmac = generateHmac(`${streamID},${exp},${iv},${secret}`, hmacSalt), hmac = hashHmac(`${streamID},${exp},${iv},${secret}`, 'stream').toString('base64url'),
streamData = { streamData = {
exp: exp, exp: exp,
type: obj.type, type: obj.type,
@ -167,7 +147,7 @@ function wrapStream(streamInfo) {
export async function verifyStream(id, hmac, exp, secret, iv) { export async function verifyStream(id, hmac, exp, secret, iv) {
try { 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()); const cache = await streamCache.get(id.toString());
if (ghmac !== String(hmac)) return { status: 401 }; if (ghmac !== String(hmac)) return { status: 401 };