This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+57
-46
@@ -6,10 +6,10 @@ import { argon2id, argon2Verify, md5 } from "hash-wasm";
|
||||
// threads=1; bcrypt rounds=12 as the legacy fallback. The game emulator
|
||||
// validates the SAME users.password hash, so these must match.
|
||||
const ARGON2_PARAMS = {
|
||||
parallelism: 1,
|
||||
iterations: 4,
|
||||
memorySize: 65536, // KiB
|
||||
hashLength: 32,
|
||||
parallelism: 1,
|
||||
iterations: 4,
|
||||
memorySize: 65536, // KiB
|
||||
hashLength: 32,
|
||||
} as const;
|
||||
const BCRYPT_ROUNDS = 12;
|
||||
|
||||
@@ -18,7 +18,9 @@ const BCRYPT_ROUNDS = 12;
|
||||
// - "argon2id": ~97-char PHC hash. Opt in with PASSWORD_HASH=argon2id.
|
||||
// verifyPassword() always accepts BOTH, so logins keep working either way.
|
||||
function hashDriver(): "bcrypt" | "argon2id" {
|
||||
return process.env.PASSWORD_HASH?.toLowerCase() === "argon2id" ? "argon2id" : "bcrypt";
|
||||
return process.env.PASSWORD_HASH?.toLowerCase() === "argon2id"
|
||||
? "argon2id"
|
||||
: "bcrypt";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,7 +32,7 @@ function hashDriver(): "bcrypt" | "argon2id" {
|
||||
* and does NOT affect credential security.
|
||||
*/
|
||||
export async function md5Hex(input: string): Promise<string> {
|
||||
return await md5(input);
|
||||
return await md5(input);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,23 +41,29 @@ export async function md5Hex(input: string): Promise<string> {
|
||||
* for argon2id (requires a wider column). Both are verifiable by verifyPassword.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
if (hashDriver() === "argon2id") {
|
||||
return argon2id({
|
||||
password,
|
||||
salt: randomBytes(16),
|
||||
outputType: "encoded",
|
||||
...ARGON2_PARAMS,
|
||||
});
|
||||
}
|
||||
// bcryptjs emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the emulator
|
||||
// and existing AtomCMS rows use.
|
||||
const h = await bcryptHash(password, BCRYPT_ROUNDS);
|
||||
return h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
if (hashDriver() === "argon2id") {
|
||||
return argon2id({
|
||||
password,
|
||||
salt: randomBytes(16),
|
||||
outputType: "encoded",
|
||||
...ARGON2_PARAMS,
|
||||
});
|
||||
}
|
||||
// bcryptjs emits $2a$/$2b$; normalise to the PHP-canonical $2y$ the emulator
|
||||
// and existing AtomCMS rows use.
|
||||
const h = await bcryptHash(password, BCRYPT_ROUNDS);
|
||||
return h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
}
|
||||
|
||||
/** True when `stored` is exactly the md5 of `password` (legacy AtomCMS accounts). */
|
||||
export async function isMd5Of(password: string, stored: string): Promise<boolean> {
|
||||
return /^[a-f0-9]{32}$/i.test(stored) && (await md5Hex(password)) === stored.toLowerCase();
|
||||
export async function isMd5Of(
|
||||
password: string,
|
||||
stored: string,
|
||||
): Promise<boolean> {
|
||||
return (
|
||||
/^[a-f0-9]{32}$/i.test(stored) &&
|
||||
(await md5Hex(password)) === stored.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,28 +71,31 @@ export async function isMd5Of(password: string, stored: string): Promise<boolean
|
||||
* Laravel's Hash::check does. Returns false for unknown/legacy formats (md5 is
|
||||
* handled by the conversion path in checkLogin, not here).
|
||||
*/
|
||||
export async function verifyPassword(password: string, stored: string): Promise<boolean> {
|
||||
if (stored.startsWith("$argon2")) {
|
||||
try {
|
||||
return await argon2Verify({ password, hash: stored });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (/^\$2[aby]\$/.test(stored)) {
|
||||
try {
|
||||
return await bcryptCompare(password, stored);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
stored: string,
|
||||
): Promise<boolean> {
|
||||
if (stored.startsWith("$argon2")) {
|
||||
try {
|
||||
return await argon2Verify({ password, hash: stored });
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (/^\$2[aby]\$/.test(stored)) {
|
||||
try {
|
||||
return await bcryptCompare(password, stored);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface LoginCheck {
|
||||
valid: boolean;
|
||||
/** Set when a legacy md5 hash was upgraded — persist it to users.password. */
|
||||
upgradedHash?: string;
|
||||
valid: boolean;
|
||||
/** Set when a legacy md5 hash was upgraded — persist it to users.password. */
|
||||
upgradedHash?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,12 +104,12 @@ export interface LoginCheck {
|
||||
* Mirrors RedirectIfTwoFactorAuthenticatable::convertUserPassword + validate.
|
||||
*/
|
||||
export async function checkLogin(
|
||||
password: string,
|
||||
stored: string,
|
||||
opts: { convertPasswords: boolean },
|
||||
password: string,
|
||||
stored: string,
|
||||
opts: { convertPasswords: boolean },
|
||||
): Promise<LoginCheck> {
|
||||
if (opts.convertPasswords && (await isMd5Of(password, stored))) {
|
||||
return { valid: true, upgradedHash: await hashPassword(password) };
|
||||
}
|
||||
return { valid: await verifyPassword(password, stored) };
|
||||
if (opts.convertPasswords && (await isMd5Of(password, stored))) {
|
||||
return { valid: true, upgradedHash: await hashPassword(password) };
|
||||
}
|
||||
return { valid: await verifyPassword(password, stored) };
|
||||
}
|
||||
Reference in new issue
Block a user