Skip to content

Commit 91def7c

Browse files
yusukebeusualoma
andauthored
Merge commit from fork
* fix: fixed to be more properly timing safe Co-authored-by: Taku Amano <taku@taaas.jp> * format * Remove comparing `a` and `b` I think this is necessary. * Remove `console.warn` It is annoying for users. I think only adding the `@deprecated` comment is okay. * update the test * test: add a test for well-known md5 hash collision Co-authored-by: Taku Amano <taku@taaas.jp> * Revert "Remove comparing `a` and `b`" This reverts commit 77b86fd6f7b9f8746dde0933b69eda922e96d0de. * remove the unused file --------- Co-authored-by: Taku Amano <taku@taaas.jp>
1 parent 8b17935 commit 91def7c

File tree

2 files changed

+77
-13
lines changed

2 files changed

+77
-13
lines changed

‎src/utils/buffer.test.ts‎

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,6 @@ describe('buffer', () => {
4040
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
4141
// @ts-ignore
4242
expect(await timingSafeEqual(undefined, undefined)).toBe(true)
43-
expect(await timingSafeEqual(true, true)).toBe(true)
44-
expect(await timingSafeEqual(false, false)).toBe(true)
45-
expect(
46-
await timingSafeEqual(true, true, (d: boolean) =>
47-
createHash('sha256').update(d.toString()).digest('hex')
48-
)
49-
)
5043
})
5144

5245
it('negative', async () => {
@@ -58,10 +51,30 @@ describe('buffer', () => {
5851
await timingSafeEqual('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'a')
5952
).toBe(false)
6053
expect(await timingSafeEqual('alpha', 'beta')).toBe(false)
61-
expect(await timingSafeEqual(false, true)).toBe(false)
6254
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6355
// @ts-ignore
6456
expect(await timingSafeEqual(false, undefined)).toBe(false)
57+
58+
expect(
59+
await timingSafeEqual(
60+
// well known md5 hash collision
61+
// https://marc-stevens.nl/research/md5-1block-collision/
62+
'TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak',
63+
'TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak',
64+
(input) => createHash('md5').update(input).digest('hex')
65+
)
66+
).toBe(false)
67+
})
68+
69+
it.skip('comparing variables except string are deprecated', async () => {
70+
expect(await timingSafeEqual(true, true)).toBe(true)
71+
expect(await timingSafeEqual(false, false)).toBe(true)
72+
expect(
73+
await timingSafeEqual(true, true, (d: boolean) =>
74+
createHash('sha256').update(d.toString()).digest('hex')
75+
)
76+
)
77+
expect(await timingSafeEqual(false, true)).toBe(false)
6578
expect(
6679
await timingSafeEqual(
6780
() => {},

‎src/utils/buffer.ts‎

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,73 @@ export const equal = (a: ArrayBuffer, b: ArrayBuffer): boolean => {
2626
return true
2727
}
2828

29-
export const timingSafeEqual = async (
30-
a: string | object | boolean,
31-
b: string | object | boolean,
29+
const constantTimeEqualString = (a: string, b: string): boolean => {
30+
const aLen = a.length
31+
const bLen = b.length
32+
const maxLen = Math.max(aLen, bLen)
33+
let out = aLen ^ bLen
34+
for (let i = 0; i < maxLen; i++) {
35+
const aChar = i < aLen ? a.charCodeAt(i) : 0
36+
const bChar = i < bLen ? b.charCodeAt(i) : 0
37+
out |= aChar ^ bChar
38+
}
39+
return out === 0
40+
}
41+
42+
type StringHashFunction = (input: string) => string | null | Promise<string | null>
43+
44+
const timingSafeEqualString = async (
45+
a: string,
46+
b: string,
47+
hashFunction?: StringHashFunction
48+
): Promise<boolean> => {
49+
if (!hashFunction) {
50+
hashFunction = sha256
51+
}
52+
53+
const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)])
54+
55+
if (sa == null || sb == null || typeof sa !== 'string' || typeof sb !== 'string') {
56+
return false
57+
}
58+
59+
const hashEqual = constantTimeEqualString(sa, sb)
60+
const originalEqual = constantTimeEqualString(a, b)
61+
62+
return hashEqual && originalEqual
63+
}
64+
65+
type TimingSafeEqual = {
66+
(a: string, b: string, hashFunction?: StringHashFunction): Promise<boolean>
67+
/**
68+
* @deprecated object and boolean signatures that take boolean as first and second arguments, and functions with signatures that take non-string arguments have been deprecated
69+
*/
70+
(
71+
a: string | object | boolean,
72+
b: string | object | boolean,
73+
hashFunction?: Function
74+
): Promise<boolean>
75+
}
76+
export const timingSafeEqual: TimingSafeEqual = async (
77+
a,
78+
b,
3279
hashFunction?: Function
3380
): Promise<boolean> => {
81+
if (typeof a === 'string' && typeof b === 'string') {
82+
return timingSafeEqualString(a, b, hashFunction as StringHashFunction)
83+
}
84+
3485
if (!hashFunction) {
3586
hashFunction = sha256
3687
}
3788

3889
const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)])
3990

40-
if (!sa || !sb) {
91+
if (!sa || !sb || typeof sa !== 'string' || typeof sb !== 'string') {
4192
return false
4293
}
4394

44-
return sa === sb && a === b
95+
return timingSafeEqualString(sa, sb)
4596
}
4697

4798
export const bufferToString = (buffer: ArrayBuffer): string => {

0 commit comments

Comments
 (0)