diff --git a/src/argon2/argon2-key-generator.ts b/src/argon2/argon2-key-generator.ts index 24dd13e..31698df 100644 --- a/src/argon2/argon2-key-generator.ts +++ b/src/argon2/argon2-key-generator.ts @@ -13,11 +13,13 @@ export function createArgon2KeyGenerator({ passes, parallelism, tagLength, + pepper, }: { memory: number; passes: number; parallelism: number; tagLength: number; + pepper?: BinaryLike | undefined; }): KeyGenerator { return { generateKey(password, salt) { @@ -28,6 +30,7 @@ export function createArgon2KeyGenerator({ passes, parallelism, tagLength, + secret: pepper, }); }, }; diff --git a/src/argon2/argon2.ts b/src/argon2/argon2.ts index 3b70189..ace2336 100644 --- a/src/argon2/argon2.ts +++ b/src/argon2/argon2.ts @@ -16,6 +16,7 @@ const optionsSchema = z parallelism: z.number().min(1).max(MAX_UINT24), tagLength: z.number().min(4).max(MAX_UINT32), saltLength: z.number().min(16).max(1024), + pepper: z.string().min(1).max(1024).optional(), }) .refine( (params) => { @@ -82,6 +83,7 @@ export function createArgon2Hashing( passes: phcNode.params.passes, parallelism: phcNode.params.parallelism, tagLength: phcNode.hash.byteLength, + pepper: defaultOptions.pepper, }).generateKey(password, phcNode.salt); return timingSafeEqual(targetKey, phcNode.hash); diff --git a/src/argon2/pepper.test.ts b/src/argon2/pepper.test.ts new file mode 100644 index 0000000..9241769 --- /dev/null +++ b/src/argon2/pepper.test.ts @@ -0,0 +1,66 @@ +import { createArgon2Hashing } from "./argon2.ts"; +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +void describe("Argon2 Pepper Support", () => { + const password = "my-secret-password"; + const firstPepper = "secret-pepper-one"; + const secondPepper = "secret-pepper-two"; + + void it("should successfully verify a password when the correct pepper is provided", async () => { + const hashing = createArgon2Hashing({ pepper: firstPepper }); + const hash = await hashing.hash(password); + + const isValid = await hashing.verify(password, hash); + assert.strictEqual( + isValid, + true, + "Verification with the correct pepper should succeed", + ); + }); + + void it("should fail verification if a different pepper is used", async () => { + const hashingA = createArgon2Hashing({ pepper: firstPepper }); + const hashingB = createArgon2Hashing({ pepper: secondPepper }); + const hash = await hashingA.hash(password); + const isValid = await hashingB.verify(password, hash); + + assert.strictEqual( + isValid, + false, + "Verification with a mismatched pepper should fail", + ); + }); +}); + +void describe("Argon2 Pepper Validation", () => { + const minIncludedPepper = "a"; + const minExcludedPepper = ""; + + const maxIncludedPepper = "a".repeat(1024); + const maxExcludedPepper = "a".repeat(1025); + + for (const pepper of [minIncludedPepper, maxIncludedPepper]) { + const password = "my-secret-password"; + + void it(`should verify a password when the pepper is ${pepper.length} characters long`, async () => { + const hashing = createArgon2Hashing({ pepper }); + const hash = await hashing.hash(password); + + const isValid = await hashing.verify(password, hash); + assert.strictEqual( + isValid, + true, + "Verification with the correct pepper should succeed", + ); + }); + } + + for (const pepper of [minExcludedPepper, maxExcludedPepper]) { + void it(`should throw an error if the pepper is ${pepper.length} characters long`, () => { + assert.throws(() => { + createArgon2Hashing({ pepper }); + }, "Expected error to be thrown"); + }); + } +});