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) {
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
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 { 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 };
|
||||||
|
|
Loading…
Reference in a new issue