From 54e9c61b850bc405412b198badbcc895258cc93f Mon Sep 17 00:00:00 2001 From: Danilo Alonso Date: Tue, 9 Dec 2025 11:48:11 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20Add=20timing-safe=20compa?= =?UTF-8?q?rison=20tests=20and=20update=20API=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 21 +++++++++++++++++---- lib/index.js | 15 +++++++++------ test/index.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 10 deletions(-) diff --git a/API.md b/API.md index 3c29ebb..9ae6434 100755 --- a/API.md +++ b/API.md @@ -1,11 +1,24 @@ + ## Methods -### `randomString( size)` + +### `randomString(size: number): string` + Returns a cryptographically strong pseudo-random data string. Takes a size argument for the length of the string. -### `randomAlphanumString( size)` +### `randomAlphanumString(size: number): string` + Returns a cryptographically strong pseudo-random alphanumeric data string. Takes a size argument for the length of the string. -### `randomDigits( size)` -Returns a cryptographically strong pseudo-random data string consisting of only numerical digits (0-9). Takes a size argument for the length of the string. \ No newline at end of file +### `randomDigits(size: number): string` + +Returns a cryptographically strong pseudo-random data string consisting of only numerical digits (0-9). Takes a size argument for the length of the string. + +### `randomBits(bits: number): Buffer` + +Returns a Buffer of cryptographically strong pseudo-random bits. Takes a bits argument for the number of bits to generate. + +### `fixedTimeComparison(a: string, b: string): boolean` + +Performs a constant-time comparison of two strings to prevent timing attacks. Returns `true` if the strings are equal, `false` otherwise. Safe to use with strings of different lengths. diff --git a/lib/index.js b/lib/index.js index a05f31b..564c012 100755 --- a/lib/index.js +++ b/lib/index.js @@ -73,17 +73,20 @@ exports.randomBits = function (bits) { return internals.random(bytes); }; - exports.fixedTimeComparison = function (a, b) { - try { - return Crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); - } - catch (err) { + const bufA = Buffer.from(a); + const bufB = Buffer.from(b); + + if (bufA.length !== bufB.length) { + + Crypto.timingSafeEqual(bufA, bufA); + return false; } -}; + return Crypto.timingSafeEqual(bufA, bufB); +}; internals.random = function (bytes) { diff --git a/test/index.js b/test/index.js index eb77329..cc1edb2 100755 --- a/test/index.js +++ b/test/index.js @@ -90,4 +90,52 @@ describe('fixedTimeComparison()', () => { expect(Cryptiles.fixedTimeComparison('', '')).to.be.true(); expect(Cryptiles.fixedTimeComparison('asdas', 'asdasd')).to.be.false(); }); + + it('should not throw if buffer size differs', () => { + + expect(() => Cryptiles.fixedTimeComparison('a', 'ab')).to.not.throw(); + expect(() => Cryptiles.fixedTimeComparison('abc', 'a')).to.not.throw(); + expect(() => Cryptiles.fixedTimeComparison('', 'a')).to.not.throw(); + expect(() => Cryptiles.fixedTimeComparison('a', '')).to.not.throw(); + }); + + it('should provide constant time regardless of the size of the right-most argument', { timeout: 10000 }, () => { + + // Test that comparison time is based on left argument, not right + // When lengths differ, we compare left to itself (constant time based on left) + const largeLeft = 'a'.repeat(100000); + const smallLeft = 'b'.repeat(10); + const smallRight = 'x'.repeat(10); + const largeRight = 'y'.repeat(100000); + + const iterations = 10000; + + // Warm up + for (let i = 0; i < 1000; ++i) { + Cryptiles.fixedTimeComparison(largeLeft, smallRight); + Cryptiles.fixedTimeComparison(smallLeft, largeRight); + } + + // Measure large left + small right (timing should be based on large left) + const startLargeLeft = process.hrtime.bigint(); + for (let i = 0; i < iterations; ++i) { + Cryptiles.fixedTimeComparison(largeLeft, smallRight); + } + + const endLargeLeft = process.hrtime.bigint(); + const largeLeftTime = Number(endLargeLeft - startLargeLeft); + + // Measure small left + large right (timing should be based on small left) + const startSmallLeft = process.hrtime.bigint(); + for (let i = 0; i < iterations; ++i) { + Cryptiles.fixedTimeComparison(smallLeft, largeRight); + } + + const endSmallLeft = process.hrtime.bigint(); + const smallLeftTime = Number(endSmallLeft - startSmallLeft); + + // Large left should take longer than small left, proving timing is based on left + // Even though small left has a much larger right argument + expect(largeLeftTime).to.be.above(smallLeftTime); + }); });