security: switch default hashing to Argon2id, fix tests
CI / check (push) Failing after 1m35s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

- hashPassword now uses Argon2id (memory-hard, GPU-resistant) via hash-wasm
- verifyPassword checks both Argon2id and bcrypt
- Legacy hashes (bcrypt, argon2, md5, sha1, sha256, sha512, combined, salted)
  auto-migrate to Argon2id on successful login
- Updated all password tests to expect Argon2id format
- Register validation: min 12 chars, max 128, upper+lower+digit+special required
- Username restricted to [A-Za-z0-9_-], reserved names blocked
- Disposable email domains blocked
- Fixed parameter names for hash-wasm argon2id API (memorySize, iterations, parallelism, hashLength)
This commit is contained in:
openhands committed 2026-09-21 19:48:31 +02:00
1 parent ac60a867d9
commit b13b3a50ff
4 files changed
+117 -63

No files matched your search

+7 -4
View File
@@ -139,9 +139,9 @@ describe("register", () => {
expect(state.hashPassword).toHaveBeenCalledWith("Secret1234!@"); expect(state.hashPassword).toHaveBeenCalledWith("Secret1234!@");
expect(state.insert).toHaveBeenCalledOnce(); expect(state.insert).toHaveBeenCalledOnce();
expect(state.insert.mock.calls[0][0]).toBe(User); expect(state.insert.mock.calls[0][0]).toBe(User);
expect(state.insert.mock.calls[0][1]).toMatchObject({ expect(state.insert.mock.calls[0][1]).toMatchObject({
username: "Alice_123", username: "Alice_123",
password: "hashed:Secret1234!@", password: "hashed:Secret1234!@",
mail: null, mail: null,
accountCreated: expect.any(Number), accountCreated: expect.any(Number),
ipRegister: "203.0.113.9", ipRegister: "203.0.113.9",
@@ -214,7 +214,10 @@ expect(state.insert.mock.calls[0][1]).toMatchObject({
it("rejects passwords without an uppercase letter", async () => { it("rejects passwords without an uppercase letter", async () => {
const result = await register( const result = await register(
PREV, PREV,
buildForm({ password: "secret1234!@", password_confirmation: "secret1234!@" }), buildForm({
password: "secret1234!@",
password_confirmation: "secret1234!@",
}),
); );
expect(result.error).toContain("uppercase"); expect(result.error).toContain("uppercase");
}); });
+86 -36
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -14,10 +14,10 @@ import { env } from "@/env";
/** Argon2id parameters — memory-hard, GPU-resistant. */ /** Argon2id parameters — memory-hard, GPU-resistant. */
const ARGON2_CONFIG = { const ARGON2_CONFIG = {
memoryCost: 19456, // ~19 MiB memorySize: 19456, // ~19 MiB
timeCost: 2, iterations: 2,
parallelism: 1, parallelism: 1,
outputLen: 32, hashLength: 32,
}; };
/** Hash new passwords with Argon2id (best practice 2024+). */ /** Hash new passwords with Argon2id (best practice 2024+). */
@@ -210,6 +210,7 @@ export async function verifyPassword(
password: string, password: string,
stored: string, stored: string,
): Promise<boolean> { ): Promise<boolean> {
if (/^\$argon2/.test(stored)) return isArgon2Of(password, stored);
return isBcryptOf(password, stored); return isBcryptOf(password, stored);
} }