From d8295127070a05e66e5a44387a94941d0416dc75 Mon Sep 17 00:00:00 2001 From: Absy00 Date: Wed, 21 Jan 2026 22:33:19 +0100 Subject: [PATCH 1/4] feat: add pepper support to argon2 hashing --- CHANGELOG.md | 3 +- src/argon2/argon2-key-generator.ts | 3 ++ src/argon2/argon2.ts | 4 +- src/pepper.test.ts | 65 ++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/pepper.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 057ebd4..d25869a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,8 @@ ## [1.0.2](https://github.com/filipo11021/nodejs-password-hashing/compare/v1.0.1...v1.0.2) (2026-01-21) - ### Bug Fixes -* **#31:** Options config type resolves to any instead of proper type inference ([73c551e](https://github.com/filipo11021/nodejs-password-hashing/commit/73c551e3c3657444940c4b19ce1a4547d511b715)) +- **#31:** Options config type resolves to any instead of proper type inference ([73c551e](https://github.com/filipo11021/nodejs-password-hashing/commit/73c551e3c3657444940c4b19ce1a4547d511b715)) ## [1.0.1](https://github.com/filipo11021/nodejs-password-hashing/compare/v1.0.0...v1.0.1) (2026-01-19) 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..d5f7038 100644 --- a/src/argon2/argon2.ts +++ b/src/argon2/argon2.ts @@ -1,4 +1,4 @@ -import { randomBytes, timingSafeEqual } from "node:crypto"; +import { randomBytes, timingSafeEqual, type BinaryLike } from "node:crypto"; import type { Hashing } from "../hashing.ts"; import { argon2DeserializePHC, @@ -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.custom().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/pepper.test.ts b/src/pepper.test.ts new file mode 100644 index 0000000..34f8aea --- /dev/null +++ b/src/pepper.test.ts @@ -0,0 +1,65 @@ +import { createArgon2Hashing } from "./argon2/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 it("should generate distinct hashes for the same password with different peppers", async () => { + const hashingA = createArgon2Hashing({ pepper: firstPepper }); + const hashingB = createArgon2Hashing({ pepper: secondPepper }); + const hashingNone = createArgon2Hashing(); + + const hashA = await hashingA.hash(password); + const hashB = await hashingB.hash(password); + const hashNone = await hashingNone.hash(password); + + assert.notStrictEqual( + hashA, + hashB, + "Hashes with different peppers must be distinct", + ); + assert.notStrictEqual( + hashA, + hashNone, + "A hashed password with a pepper must be distinct from one without", + ); + }); + + void it("should handle long peppers correctly (BinaryLike input support)", async () => { + const longPepper = Buffer.alloc(32, "a"); + const hashing = createArgon2Hashing({ pepper: longPepper }); + + const hash = await hashing.hash(password); + const isValid = await hashing.verify(password, hash); + + assert.strictEqual(isValid, true, "Should support Buffer-based peppers"); + }); +}); From 633efc79fc2d32731ece9ef894e751a1f9309405 Mon Sep 17 00:00:00 2001 From: Absy00 Date: Wed, 21 Jan 2026 23:05:12 +0100 Subject: [PATCH 2/4] test: move pepper tests to argon2 directory per PR feedback --- src/{ => argon2}/pepper.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{ => argon2}/pepper.test.ts (97%) diff --git a/src/pepper.test.ts b/src/argon2/pepper.test.ts similarity index 97% rename from src/pepper.test.ts rename to src/argon2/pepper.test.ts index 34f8aea..0aba431 100644 --- a/src/pepper.test.ts +++ b/src/argon2/pepper.test.ts @@ -1,4 +1,4 @@ -import { createArgon2Hashing } from "./argon2/argon2.ts"; +import { createArgon2Hashing } from "./argon2.ts"; import assert from "node:assert/strict"; import { describe, it } from "node:test"; From cc3ce2f8c6e1a17569a50efc6388073e91b4e9fd Mon Sep 17 00:00:00 2001 From: Absy00 Date: Fri, 23 Jan 2026 21:23:52 +0100 Subject: [PATCH 3/4] test: remove redundant distinctness test case per Bugbot feedback --- src/argon2/pepper.test.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/argon2/pepper.test.ts b/src/argon2/pepper.test.ts index 0aba431..645f724 100644 --- a/src/argon2/pepper.test.ts +++ b/src/argon2/pepper.test.ts @@ -32,27 +32,6 @@ void describe("Argon2 Pepper Support", () => { ); }); - void it("should generate distinct hashes for the same password with different peppers", async () => { - const hashingA = createArgon2Hashing({ pepper: firstPepper }); - const hashingB = createArgon2Hashing({ pepper: secondPepper }); - const hashingNone = createArgon2Hashing(); - - const hashA = await hashingA.hash(password); - const hashB = await hashingB.hash(password); - const hashNone = await hashingNone.hash(password); - - assert.notStrictEqual( - hashA, - hashB, - "Hashes with different peppers must be distinct", - ); - assert.notStrictEqual( - hashA, - hashNone, - "A hashed password with a pepper must be distinct from one without", - ); - }); - void it("should handle long peppers correctly (BinaryLike input support)", async () => { const longPepper = Buffer.alloc(32, "a"); const hashing = createArgon2Hashing({ pepper: longPepper }); From 6a5daff328d92a892431f6f9676360bd88ff6624 Mon Sep 17 00:00:00 2001 From: Filip <> Date: Fri, 23 Jan 2026 23:46:59 +0100 Subject: [PATCH 4/4] feat(argon2): add pepper length validation and restrict type to string --- src/argon2/argon2.ts | 4 ++-- src/argon2/pepper.test.ts | 36 +++++++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/argon2/argon2.ts b/src/argon2/argon2.ts index d5f7038..ace2336 100644 --- a/src/argon2/argon2.ts +++ b/src/argon2/argon2.ts @@ -1,4 +1,4 @@ -import { randomBytes, timingSafeEqual, type BinaryLike } from "node:crypto"; +import { randomBytes, timingSafeEqual } from "node:crypto"; import type { Hashing } from "../hashing.ts"; import { argon2DeserializePHC, @@ -16,7 +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.custom().optional(), + pepper: z.string().min(1).max(1024).optional(), }) .refine( (params) => { diff --git a/src/argon2/pepper.test.ts b/src/argon2/pepper.test.ts index 645f724..9241769 100644 --- a/src/argon2/pepper.test.ts +++ b/src/argon2/pepper.test.ts @@ -31,14 +31,36 @@ void describe("Argon2 Pepper Support", () => { "Verification with a mismatched pepper should fail", ); }); +}); - void it("should handle long peppers correctly (BinaryLike input support)", async () => { - const longPepper = Buffer.alloc(32, "a"); - const hashing = createArgon2Hashing({ pepper: longPepper }); +void describe("Argon2 Pepper Validation", () => { + const minIncludedPepper = "a"; + const minExcludedPepper = ""; - const hash = await hashing.hash(password); - const isValid = await hashing.verify(password, hash); + const maxIncludedPepper = "a".repeat(1024); + const maxExcludedPepper = "a".repeat(1025); - assert.strictEqual(isValid, true, "Should support Buffer-based peppers"); - }); + 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"); + }); + } });