api: move hmac secrets to single file
This commit is contained in:
parent
40d6a02b61
commit
66cb8d360d
6 changed files with 79 additions and 51 deletions
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
58
api/src/security/secrets.js
Normal file
58
api/src/security/secrets.js
Normal 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();
|
||||
}
|
|
@ -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 };
|
||||
|
|
Loading…
Reference in a new issue