minor adjustments to HMAC signing

This commit is contained in:
nai-degen 2024-08-22 19:53:53 -05:00
parent 5000e59a61
commit ce490efd7d
6 changed files with 35 additions and 27 deletions

View File

@ -17,7 +17,7 @@ import {
} from "../../shared/users/schema"; } from "../../shared/users/schema";
import { getLastNImages } from "../../shared/file-storage/image-history"; import { getLastNImages } from "../../shared/file-storage/image-history";
import { blacklists, parseCidrs, whitelists } from "../../shared/cidr"; import { blacklists, parseCidrs, whitelists } from "../../shared/cidr";
import { invalidatePowHmacKey } from "../../user/web/pow-captcha"; import { invalidatePowChallenges } from "../../user/web/pow-captcha";
const router = Router(); const router = Router();
@ -323,7 +323,7 @@ router.post("/maintenance", (req, res) => {
user.disabledReason = "Admin forced expiration."; user.disabledReason = "Admin forced expiration.";
userStore.upsertUser(user); userStore.upsertUser(user);
}); });
invalidatePowHmacKey(); invalidatePowChallenges();
flash.type = "success"; flash.type = "success";
flash.message = `${temps.length} temporary users marked for expiration.`; flash.message = `${temps.length} temporary users marked for expiration.`;
break; break;
@ -348,6 +348,7 @@ router.post("/maintenance", (req, res) => {
throw new HttpError(400, "Invalid difficulty" + selected); throw new HttpError(400, "Invalid difficulty" + selected);
} }
config.powDifficultyLevel = selected; config.powDifficultyLevel = selected;
invalidatePowChallenges();
break; break;
} }
case "generateTempIpReport": { case "generateTempIpReport": {

View File

@ -519,7 +519,7 @@ function generateSigningKey() {
} }
const signingKey = generateSigningKey(); const signingKey = generateSigningKey();
export const COOKIE_SECRET = signingKey; export const SECRET_SIGNING_KEY = signingKey;
export async function assertConfigIsValid() { export async function assertConfigIsValid() {
if (process.env.MODEL_RATE_LIMIT !== undefined) { if (process.env.MODEL_RATE_LIMIT !== undefined) {

View File

@ -0,0 +1,18 @@
/** Module for generating and verifying HMAC signatures. */
import crypto from "crypto";
import { SECRET_SIGNING_KEY } from "../config";
/**
* Generates a HMAC signature for the given message. Optionally salts the
* key with a provided string.
*/
export function signMessage(msg: any, salt: string = ""): string {
const hmac = crypto.createHmac("sha256", SECRET_SIGNING_KEY + salt);
if (typeof msg === "object") {
hmac.update(JSON.stringify(msg));
} else {
hmac.update(msg);
}
return hmac.digest("hex");
}

View File

@ -1,9 +1,9 @@
import { doubleCsrf } from "csrf-csrf"; import { doubleCsrf } from "csrf-csrf";
import express from "express"; import express from "express";
import { config, COOKIE_SECRET } from "../config"; import { config, SECRET_SIGNING_KEY } from "../config";
const { generateToken, doubleCsrfProtection } = doubleCsrf({ const { generateToken, doubleCsrfProtection } = doubleCsrf({
getSecret: () => COOKIE_SECRET, getSecret: () => SECRET_SIGNING_KEY,
cookieName: "csrf", cookieName: "csrf",
cookieOptions: { cookieOptions: {
sameSite: "strict", sameSite: "strict",

View File

@ -1,14 +1,14 @@
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import expressSession from "express-session"; import expressSession from "express-session";
import MemoryStore from "memorystore"; import MemoryStore from "memorystore";
import { config, COOKIE_SECRET } from "../config"; import { config, SECRET_SIGNING_KEY } from "../config";
const ONE_WEEK = 1000 * 60 * 60 * 24 * 7; const ONE_WEEK = 1000 * 60 * 60 * 24 * 7;
const cookieParserMiddleware = cookieParser(COOKIE_SECRET); const cookieParserMiddleware = cookieParser(SECRET_SIGNING_KEY);
const sessionMiddleware = expressSession({ const sessionMiddleware = expressSession({
secret: COOKIE_SECRET, secret: SECRET_SIGNING_KEY,
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
store: new (MemoryStore(expressSession))({ checkPeriod: ONE_WEEK }), store: new (MemoryStore(expressSession))({ checkPeriod: ONE_WEEK }),

View File

@ -2,6 +2,7 @@ import crypto from "crypto";
import express from "express"; import express from "express";
import argon2 from "@node-rs/argon2"; import argon2 from "@node-rs/argon2";
import { z } from "zod"; import { z } from "zod";
import { signMessage } from "../../shared/hmac-signing";
import { import {
authenticate, authenticate,
createUser, createUser,
@ -13,15 +14,13 @@ import { config } from "../../config";
/** Lockout time after verification in milliseconds */ /** Lockout time after verification in milliseconds */
const LOCKOUT_TIME = 1000 * 60; // 60 seconds const LOCKOUT_TIME = 1000 * 60; // 60 seconds
/** HMAC key for signing challenges; regenerated on startup */ let powKeySalt = crypto.randomBytes(32).toString("hex");
let hmacSecret = crypto.randomBytes(32).toString("hex");
/** /**
* Regenerate the HMAC key used for signing challenges. Calling this function * Invalidates any outstanding unsolved challenges.
* will invalidate all existing challenges.
*/ */
export function invalidatePowHmacKey() { export function invalidatePowChallenges() {
hmacSecret = crypto.randomBytes(32).toString("hex"); powKeySalt = crypto.randomBytes(32).toString("hex");
} }
const argon2Params = { const argon2Params = {
@ -141,16 +140,6 @@ function generateChallenge(clientIp?: string, token?: string): Challenge {
}; };
} }
function signMessage(msg: any): string {
const hmac = crypto.createHmac("sha256", hmacSecret);
if (typeof msg === "object") {
hmac.update(JSON.stringify(msg));
} else {
hmac.update(msg);
}
return hmac.digest("hex");
}
async function verifySolution( async function verifySolution(
challenge: Challenge, challenge: Challenge,
solution: string, solution: string,
@ -225,11 +214,11 @@ router.post("/challenge", (req, res) => {
return; return;
} }
const challenge = generateChallenge(req.ip, refreshToken); const challenge = generateChallenge(req.ip, refreshToken);
const signature = signMessage(challenge); const signature = signMessage(challenge, powKeySalt);
res.json({ challenge, signature }); res.json({ challenge, signature });
} else { } else {
const challenge = generateChallenge(req.ip); const challenge = generateChallenge(req.ip);
const signature = signMessage(challenge); const signature = signMessage(challenge, powKeySalt);
res.json({ challenge, signature }); res.json({ challenge, signature });
} }
}); });
@ -253,7 +242,7 @@ router.post("/verify", async (req, res) => {
} }
const { challenge, signature, solution } = result.data; const { challenge, signature, solution } = result.data;
if (signMessage(challenge) !== signature) { if (signMessage(challenge, powKeySalt) !== signature) {
res.status(400).json({ res.status(400).json({
error: error:
"Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.", "Invalid signature; server may have restarted since challenge was issued. Please request a new challenge.",