Remove argon2id, use bcrypt-only password hashing
This commit is contained in:
1 parent
7f58e428ed
commit
7cdb785218
8 files changed
+14
-124
No files matched your search
+2
-3
@@ -28,12 +28,11 @@ AUTH_URL=http://localhost:3002
|
|||||||
IMAGING_UPSTREAM_URL=http://127.0.0.1:3030/imaging
|
IMAGING_UPSTREAM_URL=http://127.0.0.1:3030/imaging
|
||||||
NEXT_PUBLIC_IMAGER_URL=http://localhost:3002/imaging
|
NEXT_PUBLIC_IMAGER_URL=http://localhost:3002/imaging
|
||||||
|
|
||||||
# --- SECURITY & HASHING (High Performance) ---
|
# --- SECURITY & HASHING ---
|
||||||
AUTH_SECRET=your-super-secret-auth-key-change-this-min-32-chars
|
AUTH_SECRET=your-super-secret-auth-key-change-this-min-32-chars
|
||||||
APP_KEY=base64:your-app-key-here=
|
APP_KEY=base64:your-app-key-here=
|
||||||
CONVERT_PASSWORDS=true
|
CONVERT_PASSWORDS=true
|
||||||
PASSWORD_HASH=argon2id
|
BCRYPT_ROUNDS=12
|
||||||
BCRYPT_ROUNDS=10
|
|
||||||
|
|
||||||
# --- PATHS ---
|
# --- PATHS ---
|
||||||
BADGE_UPLOAD_DIR=./public/assets/images/badges
|
BADGE_UPLOAD_DIR=./public/assets/images/badges
|
||||||
|
|||||||
@@ -34,8 +34,6 @@ jobs:
|
|||||||
export DATABASE_URL="mysql://test:test@localhost:3306/test?charset=utf8mb4"
|
export DATABASE_URL="mysql://test:test@localhost:3306/test?charset=utf8mb4"
|
||||||
export AUTH_SECRET="ci-test-secret-key-that-is-long-enough"
|
export AUTH_SECRET="ci-test-secret-key-that-is-long-enough"
|
||||||
export REDIS_URL="redis://127.0.0.1:6379?connect_timeout=1"
|
export REDIS_URL="redis://127.0.0.1:6379?connect_timeout=1"
|
||||||
export ARGON2_MEMORY_SIZE=1024
|
|
||||||
export ARGON2_ITERATIONS=1
|
|
||||||
export BCRYPT_ROUNDS=4
|
export BCRYPT_ROUNDS=4
|
||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
pnpm prisma:generate
|
pnpm prisma:generate
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
echo '<a id="what-is-epicnext-cms"></a>'
|
echo '<a id="what-is-epicnext-cms"></a>'
|
||||||
echo "## What is EpicNext-CMS?"
|
echo "## What is EpicNext-CMS?"
|
||||||
echo ""
|
echo ""
|
||||||
echo "EpicNext-CMS is a full public-facing hotel website plus an administrative panel. It features NextAuth authentication (argon2id/bcrypt with MD5 upgrade), real-time RCON communication with the emulator, Server-Sent Events for live radio, smooth page transitions and extensive extensibility. Full documentation: https://gitlab.epicnabbo.nl/remco/EpicNext-Cms/src/branch/main/README.md"
|
echo "EpicNext-CMS is a full public-facing hotel website plus an administrative panel. It features NextAuth authentication (bcrypt with MD5 upgrade), real-time RCON communication with the emulator, Server-Sent Events for live radio, smooth page transitions and extensive extensibility. Full documentation: https://gitlab.epicnabbo.nl/remco/EpicNext-Cms/src/branch/main/README.md"
|
||||||
echo ""
|
echo ""
|
||||||
echo '<a id="system-requirements"></a>'
|
echo '<a id="system-requirements"></a>'
|
||||||
echo "## System Requirements"
|
echo "## System Requirements"
|
||||||
@@ -314,7 +314,7 @@ jobs:
|
|||||||
pnpm install --frozen-lockfile
|
pnpm install --frozen-lockfile
|
||||||
# prisma generate does not need a live DB connection.
|
# prisma generate does not need a live DB connection.
|
||||||
pnpm prisma:generate
|
pnpm prisma:generate
|
||||||
export ARGON2_MEMORY_SIZE=1024 ARGON2_ITERATIONS=1 BCRYPT_ROUNDS=4
|
export BCRYPT_ROUNDS=4
|
||||||
pnpm typecheck
|
pnpm typecheck
|
||||||
pnpm test
|
pnpm test
|
||||||
# Validate production env (AUTH_SECRET, DATABASE_URL, …) during build.
|
# Validate production env (AUTH_SECRET, DATABASE_URL, …) during build.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Prisma 7** and **React 19**. Designed to integrate seamlessly with Polaris / Arcturus Morningstar MySQL/MariaDB databases.
|
A modern, high-performance content management system for Habbo hotel emulators, built on **Next.js 16** (App Router) with **Prisma 7** and **React 19**. Designed to integrate seamlessly with Polaris / Arcturus Morningstar MySQL/MariaDB databases.
|
||||||
|
|
||||||
Features a premium animated homepage (typewriter hero, floating orbs, scroll counters), a full admin panel, NextAuth authentication (argon2id/bcrypt with MD5-to-argon2id upgrade), real-time RCON communication, Server-Sent Events for live radio data, smooth page transitions, and PM2 production deployment.
|
Features a premium animated homepage (typewriter hero, floating orbs, scroll counters), a full admin panel, NextAuth authentication (bcrypt with MD5-to-bcrypt upgrade), real-time RCON communication, Server-Sent Events for live radio data, smooth page transitions, and PM2 production deployment.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+1
-7
@@ -50,17 +50,11 @@ const schema = z
|
|||||||
// Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets.
|
// Laravel APP_KEY (base64:...) — needed to read existing 2FA secrets.
|
||||||
APP_KEY: z.string().optional(),
|
APP_KEY: z.string().optional(),
|
||||||
|
|
||||||
// Mirrors Laravel config('habbo.site.convert_passwords') — enables md5->argon2id.
|
// Mirrors Laravel config('habbo.site.convert_passwords') — enables md5->bcrypt upgrade.
|
||||||
CONVERT_PASSWORDS: z
|
CONVERT_PASSWORDS: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((v) => v === "true" || v === "1"),
|
.transform((v) => v === "true" || v === "1"),
|
||||||
// Hashing driver for NEW passwords: bcrypt (default, fits varchar(64)) | argon2id.
|
|
||||||
PASSWORD_HASH: z.enum(["bcrypt", "argon2id"]).optional(),
|
|
||||||
// Argon2id parameters (mirrors config/hashing.php).
|
|
||||||
ARGON2_PARALLELISM: z.coerce.number().int().positive().default(1),
|
|
||||||
ARGON2_ITERATIONS: z.coerce.number().int().positive().default(4),
|
|
||||||
ARGON2_MEMORY_SIZE: z.coerce.number().int().positive().default(65536),
|
|
||||||
// Bcrypt cost factor (rounds).
|
// Bcrypt cost factor (rounds).
|
||||||
BCRYPT_ROUNDS: z.coerce.number().int().positive().default(12),
|
BCRYPT_ROUNDS: z.coerce.number().int().positive().default(12),
|
||||||
// Filesystem dir the badge uploader writes <code>.gif into (the emulator's
|
// Filesystem dir the badge uploader writes <code>.gif into (the emulator's
|
||||||
|
|||||||
+1
-1
@@ -96,7 +96,7 @@ export const { handlers, signOut, auth } = NextAuth({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Byte-compatible AtomCMS check (argon2id/bcrypt + md5->argon2id upgrade).
|
// Byte-compatible AtomCMS check (bcrypt + md5->bcrypt upgrade).
|
||||||
const res = await checkLogin(password, user.password, {
|
const res = await checkLogin(password, user.password, {
|
||||||
convertPasswords: env.CONVERT_PASSWORDS,
|
convertPasswords: env.CONVERT_PASSWORDS,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { randomBytes } from "node:crypto";
|
|
||||||
import { argon2id } from "hash-wasm";
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const mockEnv = vi.hoisted(() => ({
|
const mockEnv = vi.hoisted(() => ({
|
||||||
PASSWORD_HASH: undefined as string | undefined,
|
|
||||||
ARGON2_PARALLELISM: 1,
|
|
||||||
ARGON2_ITERATIONS: 4,
|
|
||||||
ARGON2_MEMORY_SIZE: 65536,
|
|
||||||
BCRYPT_ROUNDS: 12,
|
BCRYPT_ROUNDS: 12,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -29,9 +23,8 @@ describe("md5Hex", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hashPassword (default driver: bcrypt)", () => {
|
describe("hashPassword", () => {
|
||||||
it("emits a bcrypt hash and round-trips", async () => {
|
it("emits a bcrypt hash and round-trips", async () => {
|
||||||
mockEnv.PASSWORD_HASH = undefined;
|
|
||||||
const h = await hashPassword("s3cret!");
|
const h = await hashPassword("s3cret!");
|
||||||
expect(h).toMatch(/^\$2y\$\d{2}\$/);
|
expect(h).toMatch(/^\$2y\$\d{2}\$/);
|
||||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||||
@@ -39,35 +32,8 @@ describe("hashPassword (default driver: bcrypt)", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("hashPassword (PASSWORD_HASH=argon2id)", () => {
|
|
||||||
it("hashes with the AtomCMS params and round-trips", async () => {
|
|
||||||
mockEnv.PASSWORD_HASH = "argon2id";
|
|
||||||
mockEnv.ARGON2_MEMORY_SIZE = 1024;
|
|
||||||
mockEnv.ARGON2_ITERATIONS = 1;
|
|
||||||
const h = await hashPassword("s3cret!");
|
|
||||||
expect(h).toMatch(/^\$argon2id\$v=19\$m=1024,t=1,p=1\$/);
|
|
||||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
|
||||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("verifyPassword", () => {
|
describe("verifyPassword", () => {
|
||||||
it("verifies argon2id hashes", async () => {
|
|
||||||
const h = await argon2id({
|
|
||||||
password: "hunter2",
|
|
||||||
salt: randomBytes(16),
|
|
||||||
outputType: "encoded",
|
|
||||||
parallelism: 1,
|
|
||||||
iterations: 4,
|
|
||||||
memorySize: 1024,
|
|
||||||
hashLength: 32,
|
|
||||||
});
|
|
||||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
|
||||||
expect(await verifyPassword("nope", h)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("verifies legacy bcrypt hashes ($2y$)", async () => {
|
it("verifies legacy bcrypt hashes ($2y$)", async () => {
|
||||||
mockEnv.PASSWORD_HASH = undefined;
|
|
||||||
const h = await hashPassword("hunter2");
|
const h = await hashPassword("hunter2");
|
||||||
expect(h).toMatch(/^\$2y\$/);
|
expect(h).toMatch(/^\$2y\$/);
|
||||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
expect(await verifyPassword("hunter2", h)).toBe(true);
|
||||||
@@ -85,7 +51,6 @@ describe("isMd5Of", () => {
|
|||||||
|
|
||||||
describe("checkLogin", () => {
|
describe("checkLogin", () => {
|
||||||
it("upgrades a legacy md5 hash to bcrypt when conversion is enabled", async () => {
|
it("upgrades a legacy md5 hash to bcrypt when conversion is enabled", async () => {
|
||||||
mockEnv.PASSWORD_HASH = undefined;
|
|
||||||
const stored = await md5Hex("oldpass");
|
const stored = await md5Hex("oldpass");
|
||||||
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
||||||
expect(res.valid).toBe(true);
|
expect(res.valid).toBe(true);
|
||||||
@@ -105,10 +70,9 @@ describe("checkLogin", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("validates an existing modern hash with no upgrade", async () => {
|
it("validates an existing modern hash with no upgrade", async () => {
|
||||||
mockEnv.PASSWORD_HASH = undefined;
|
|
||||||
const stored = await hashPassword("modern");
|
const stored = await hashPassword("modern");
|
||||||
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
||||||
expect(res.valid).toBe(true);
|
expect(res.valid).toBe(true);
|
||||||
expect(res.upgradedHash).toBeUndefined();
|
expect(res.upgradedHash).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user