diff --git a/backend/src/auth/password.ts b/backend/src/auth/password.ts index afc71c11..d46fb050 100644 --- a/backend/src/auth/password.ts +++ b/backend/src/auth/password.ts @@ -59,8 +59,9 @@ function passwordBytes(password: string): Buffer | undefined { } else if (/[\uD800-\uDFFF]/.test(password)) { return undefined; } - const bytes = Buffer.from(password, "utf8"); - return bytes.length >= MINIMUM_PASSWORD_BYTES && bytes.length <= MAXIMUM_PASSWORD_BYTES ? bytes : undefined; + const byteLength = Buffer.byteLength(password, "utf8"); + if (byteLength < MINIMUM_PASSWORD_BYTES || byteLength > MAXIMUM_PASSWORD_BYTES) return undefined; + return Buffer.from(password, "utf8"); } export function isValidPasswordHash(encoded: string): boolean { diff --git a/backend/test/auth-password.test.ts b/backend/test/auth-password.test.ts index 22318cb7..ab6e27e1 100644 --- a/backend/test/auth-password.test.ts +++ b/backend/test/auth-password.test.ts @@ -44,6 +44,28 @@ describe("local Argon2id password verification", () => { expect(verifyPassword(loneSurrogatePassword, replacementPasswordHash)).toBe(false); }); + test("accepts a multibyte password at exactly the 1024-byte boundary", () => { + const password = "é".repeat(512); + const passwordHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$LfO3M3JBKeXf1knenCjQ6m9z4B7nedb0As2uc522I1I"; + + expect(Buffer.byteLength(password, "utf8")).toBe(1024); + expect(verifyPassword(password, passwordHash)).toBe(true); + expect(verifyPassword(`${password}é`, passwordHash)).toBe(false); + }); + + test("rejects an over-limit password before converting it with Buffer.from", () => { + const password = "é".repeat(513); + const fromSpy = vi.spyOn(Buffer, "from"); + + try { + expect(Buffer.byteLength(password, "utf8")).toBe(1026); + expect(verifyPassword(password, vectors[0].phc)).toBe(false); + expect(fromSpy.mock.calls.some(([value, encoding]) => value === password && encoding === "utf8")).toBe(false); + } finally { + fromSpy.mockRestore(); + } + }); + test("rejects malformed and oversized PHC parameters before Argon2 allocation", () => { const password = vectors[0].password; const digest = vectors[0].phc.split("$")[5];