This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
@@ -1,89 +1,99 @@
|
||||
import { hash as bcryptHash } from "bcryptjs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { checkLogin, hashPassword, isMd5Of, md5Hex, verifyPassword } from "./password";
|
||||
import {
|
||||
checkLogin,
|
||||
hashPassword,
|
||||
isMd5Of,
|
||||
md5Hex,
|
||||
verifyPassword,
|
||||
} from "./password";
|
||||
|
||||
describe("md5Hex", () => {
|
||||
it("matches PHP md5() on canonical vectors", async () => {
|
||||
expect(await md5Hex("")).toBe("d41d8cd98f00b204e9800998ecf8427e");
|
||||
expect(await md5Hex("abc")).toBe("900150983cd24fb0d6963f7d28e17f72");
|
||||
});
|
||||
it("matches PHP md5() on canonical vectors", async () => {
|
||||
expect(await md5Hex("")).toBe("d41d8cd98f00b204e9800998ecf8427e");
|
||||
expect(await md5Hex("abc")).toBe("900150983cd24fb0d6963f7d28e17f72");
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashPassword (default driver: bcrypt)", () => {
|
||||
it("emits a PHP-style $2y$ bcrypt hash that fits varchar(64) and round-trips", async () => {
|
||||
const prev = process.env.PASSWORD_HASH;
|
||||
delete process.env.PASSWORD_HASH; // exercise the default
|
||||
try {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$2y\$/);
|
||||
expect(h.length).toBeLessThanOrEqual(60); // fits varchar(64)
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PASSWORD_HASH;
|
||||
else process.env.PASSWORD_HASH = prev;
|
||||
}
|
||||
});
|
||||
it("emits a PHP-style $2y$ bcrypt hash that fits varchar(64) and round-trips", async () => {
|
||||
const prev = process.env.PASSWORD_HASH;
|
||||
delete process.env.PASSWORD_HASH; // exercise the default
|
||||
try {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$2y\$/);
|
||||
expect(h.length).toBeLessThanOrEqual(60); // fits varchar(64)
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PASSWORD_HASH;
|
||||
else process.env.PASSWORD_HASH = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("hashPassword (PASSWORD_HASH=argon2id)", () => {
|
||||
it("hashes with the AtomCMS params (m=65536,t=4,p=1) and round-trips", async () => {
|
||||
const prev = process.env.PASSWORD_HASH;
|
||||
process.env.PASSWORD_HASH = "argon2id";
|
||||
try {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$argon2id\$v=19\$m=65536,t=4,p=1\$/);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PASSWORD_HASH;
|
||||
else process.env.PASSWORD_HASH = prev;
|
||||
}
|
||||
});
|
||||
it("hashes with the AtomCMS params (m=65536,t=4,p=1) and round-trips", async () => {
|
||||
const prev = process.env.PASSWORD_HASH;
|
||||
process.env.PASSWORD_HASH = "argon2id";
|
||||
try {
|
||||
const h = await hashPassword("s3cret!");
|
||||
expect(h).toMatch(/^\$argon2id\$v=19\$m=65536,t=4,p=1\$/);
|
||||
expect(await verifyPassword("s3cret!", h)).toBe(true);
|
||||
expect(await verifyPassword("wrong", h)).toBe(false);
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PASSWORD_HASH;
|
||||
else process.env.PASSWORD_HASH = prev;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("bcrypt", () => {
|
||||
it("verifies a bcrypt hash and accepts the PHP $2y$ prefix", async () => {
|
||||
const h = await bcryptHash("hunter2", 10); // bcryptjs emits $2a$
|
||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
||||
// PHP stores $2y$ — bcryptjs must accept it as equivalent.
|
||||
const phpStyle = h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
expect(await verifyPassword("hunter2", phpStyle)).toBe(true);
|
||||
expect(await verifyPassword("nope", h)).toBe(false);
|
||||
});
|
||||
it("verifies a bcrypt hash and accepts the PHP $2y$ prefix", async () => {
|
||||
const h = await bcryptHash("hunter2", 10); // bcryptjs emits $2a$
|
||||
expect(await verifyPassword("hunter2", h)).toBe(true);
|
||||
// PHP stores $2y$ — bcryptjs must accept it as equivalent.
|
||||
const phpStyle = h.replace(/^\$2[ab]\$/, "$2y$");
|
||||
expect(await verifyPassword("hunter2", phpStyle)).toBe(true);
|
||||
expect(await verifyPassword("nope", h)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isMd5Of", () => {
|
||||
it("detects a legacy md5 password", async () => {
|
||||
expect(await isMd5Of("habbo", await md5Hex("habbo"))).toBe(true);
|
||||
expect(await isMd5Of("habbo", await md5Hex("other"))).toBe(false);
|
||||
expect(await isMd5Of("habbo", "not-a-hash")).toBe(false);
|
||||
});
|
||||
it("detects a legacy md5 password", async () => {
|
||||
expect(await isMd5Of("habbo", await md5Hex("habbo"))).toBe(true);
|
||||
expect(await isMd5Of("habbo", await md5Hex("other"))).toBe(false);
|
||||
expect(await isMd5Of("habbo", "not-a-hash")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkLogin", () => {
|
||||
it("upgrades a legacy md5 hash to the configured hash when conversion is enabled", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
// Default driver is bcrypt — the upgraded hash must fit varchar(64).
|
||||
expect(res.upgradedHash).toMatch(/^\$2y\$/);
|
||||
expect((res.upgradedHash as string).length).toBeLessThanOrEqual(60);
|
||||
// The upgraded hash verifies the same password.
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(true);
|
||||
});
|
||||
it("upgrades a legacy md5 hash to the configured hash when conversion is enabled", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
// Default driver is bcrypt — the upgraded hash must fit varchar(64).
|
||||
expect(res.upgradedHash).toMatch(/^\$2y\$/);
|
||||
expect((res.upgradedHash as string).length).toBeLessThanOrEqual(60);
|
||||
// The upgraded hash verifies the same password.
|
||||
expect(await verifyPassword("oldpass", res.upgradedHash as string)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("does NOT upgrade md5 when conversion is disabled", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored, { convertPasswords: false });
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
});
|
||||
it("does NOT upgrade md5 when conversion is disabled", async () => {
|
||||
const stored = await md5Hex("oldpass");
|
||||
const res = await checkLogin("oldpass", stored, {
|
||||
convertPasswords: false,
|
||||
});
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
});
|
||||
|
||||
it("validates an existing modern hash with no upgrade", async () => {
|
||||
const stored = await hashPassword("modern");
|
||||
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
});
|
||||
it("validates an existing modern hash with no upgrade", async () => {
|
||||
const stored = await hashPassword("modern");
|
||||
const res = await checkLogin("modern", stored, { convertPasswords: true });
|
||||
expect(res.valid).toBe(true);
|
||||
expect(res.upgradedHash).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user