Skip to content

Commit e6ac5ca

Browse files
damusixMarsup
authored andcommitted
fix: 🐛 Add timing-safe comparison tests and update API docs
1 parent 9850398 commit e6ac5ca

File tree

3 files changed

+74
-10
lines changed

3 files changed

+74
-10
lines changed

API.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11

2+
23
## Methods
34

4-
### `randomString(<Number> size)`
5+
6+
### `randomString(size: number): string`
7+
58
Returns a cryptographically strong pseudo-random data string. Takes a size argument for the length of the string.
69

7-
### `randomAlphanumString(<Number> size)`
10+
### `randomAlphanumString(size: number): string`
11+
812
Returns a cryptographically strong pseudo-random alphanumeric data string. Takes a size argument for the length of the string.
913

10-
### `randomDigits(<Number> size)`
11-
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.
14+
### `randomDigits(size: number): string`
15+
16+
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.
17+
18+
### `randomBits(bits: number): Buffer`
19+
20+
Returns a Buffer of cryptographically strong pseudo-random bits. Takes a bits argument for the number of bits to generate.
21+
22+
### `fixedTimeComparison(a: string, b: string): boolean`
23+
24+
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.

lib/index.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,20 @@ exports.randomBits = function (bits) {
7373
return internals.random(bytes);
7474
};
7575

76-
7776
exports.fixedTimeComparison = function (a, b) {
7877

79-
try {
80-
return Crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
81-
}
82-
catch (err) {
78+
const bufA = Buffer.from(a);
79+
const bufB = Buffer.from(b);
80+
81+
if (bufA.length !== bufB.length) {
82+
83+
Crypto.timingSafeEqual(bufA, bufA);
84+
8385
return false;
8486
}
85-
};
8687

88+
return Crypto.timingSafeEqual(bufA, bufB);
89+
};
8790

8891
internals.random = function (bytes) {
8992

test/index.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,52 @@ describe('fixedTimeComparison()', () => {
9090
expect(Cryptiles.fixedTimeComparison('', '')).to.be.true();
9191
expect(Cryptiles.fixedTimeComparison('asdas', 'asdasd')).to.be.false();
9292
});
93+
94+
it('should not throw if buffer size differs', () => {
95+
96+
expect(() => Cryptiles.fixedTimeComparison('a', 'ab')).to.not.throw();
97+
expect(() => Cryptiles.fixedTimeComparison('abc', 'a')).to.not.throw();
98+
expect(() => Cryptiles.fixedTimeComparison('', 'a')).to.not.throw();
99+
expect(() => Cryptiles.fixedTimeComparison('a', '')).to.not.throw();
100+
});
101+
102+
it('should provide constant time regardless of the size of the right-most argument', { timeout: 10000 }, () => {
103+
104+
// Test that comparison time is based on left argument, not right
105+
// When lengths differ, we compare left to itself (constant time based on left)
106+
const largeLeft = 'a'.repeat(100000);
107+
const smallLeft = 'b'.repeat(10);
108+
const smallRight = 'x'.repeat(10);
109+
const largeRight = 'y'.repeat(100000);
110+
111+
const iterations = 10000;
112+
113+
// Warm up
114+
for (let i = 0; i < 1000; ++i) {
115+
Cryptiles.fixedTimeComparison(largeLeft, smallRight);
116+
Cryptiles.fixedTimeComparison(smallLeft, largeRight);
117+
}
118+
119+
// Measure large left + small right (timing should be based on large left)
120+
const startLargeLeft = process.hrtime.bigint();
121+
for (let i = 0; i < iterations; ++i) {
122+
Cryptiles.fixedTimeComparison(largeLeft, smallRight);
123+
}
124+
125+
const endLargeLeft = process.hrtime.bigint();
126+
const largeLeftTime = Number(endLargeLeft - startLargeLeft);
127+
128+
// Measure small left + large right (timing should be based on small left)
129+
const startSmallLeft = process.hrtime.bigint();
130+
for (let i = 0; i < iterations; ++i) {
131+
Cryptiles.fixedTimeComparison(smallLeft, largeRight);
132+
}
133+
134+
const endSmallLeft = process.hrtime.bigint();
135+
const smallLeftTime = Number(endSmallLeft - startSmallLeft);
136+
137+
// Large left should take longer than small left, proving timing is based on left
138+
// Even though small left has a much larger right argument
139+
expect(largeLeftTime).to.be.above(smallLeftTime);
140+
});
93141
});

0 commit comments

Comments
 (0)