diff --git a/package.json b/package.json index c43489f73..0c773d048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hono", - "version": "4.11.9", + "version": "4.11.10", "description": "Web framework built on Web Standards", "main": "dist/cjs/index.js", "type": "module", diff --git a/src/utils/buffer.test.ts b/src/utils/buffer.test.ts index 62f805815..56db598df 100644 --- a/src/utils/buffer.test.ts +++ b/src/utils/buffer.test.ts @@ -40,13 +40,6 @@ describe('buffer', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect(await timingSafeEqual(undefined, undefined)).toBe(true) - expect(await timingSafeEqual(true, true)).toBe(true) - expect(await timingSafeEqual(false, false)).toBe(true) - expect( - await timingSafeEqual(true, true, (d: boolean) => - createHash('sha256').update(d.toString()).digest('hex') - ) - ) }) it('negative', async () => { @@ -58,10 +51,30 @@ describe('buffer', () => { await timingSafeEqual('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 'a') ).toBe(false) expect(await timingSafeEqual('alpha', 'beta')).toBe(false) - expect(await timingSafeEqual(false, true)).toBe(false) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore expect(await timingSafeEqual(false, undefined)).toBe(false) + + expect( + await timingSafeEqual( + // well known md5 hash collision + // https://marc-stevens.nl/research/md5-1block-collision/ + 'TEXTCOLLBYfGiJUETHQ4hAcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak', + 'TEXTCOLLBYfGiJUETHQ4hEcKSMd5zYpgqf1YRDhkmxHkhPWptrkoyz28wnI9V0aHeAuaKnak', + (input) => createHash('md5').update(input).digest('hex') + ) + ).toBe(false) + }) + + it.skip('comparing variables except string are deprecated', async () => { + expect(await timingSafeEqual(true, true)).toBe(true) + expect(await timingSafeEqual(false, false)).toBe(true) + expect( + await timingSafeEqual(true, true, (d: boolean) => + createHash('sha256').update(d.toString()).digest('hex') + ) + ) + expect(await timingSafeEqual(false, true)).toBe(false) expect( await timingSafeEqual( () => {}, diff --git a/src/utils/buffer.ts b/src/utils/buffer.ts index b8ead4eed..663405095 100644 --- a/src/utils/buffer.ts +++ b/src/utils/buffer.ts @@ -26,22 +26,73 @@ export const equal = (a: ArrayBuffer, b: ArrayBuffer): boolean => { return true } -export const timingSafeEqual = async ( - a: string | object | boolean, - b: string | object | boolean, +const constantTimeEqualString = (a: string, b: string): boolean => { + const aLen = a.length + const bLen = b.length + const maxLen = Math.max(aLen, bLen) + let out = aLen ^ bLen + for (let i = 0; i < maxLen; i++) { + const aChar = i < aLen ? a.charCodeAt(i) : 0 + const bChar = i < bLen ? b.charCodeAt(i) : 0 + out |= aChar ^ bChar + } + return out === 0 +} + +type StringHashFunction = (input: string) => string | null | Promise + +const timingSafeEqualString = async ( + a: string, + b: string, + hashFunction?: StringHashFunction +): Promise => { + if (!hashFunction) { + hashFunction = sha256 + } + + const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]) + + if (sa == null || sb == null || typeof sa !== 'string' || typeof sb !== 'string') { + return false + } + + const hashEqual = constantTimeEqualString(sa, sb) + const originalEqual = constantTimeEqualString(a, b) + + return hashEqual && originalEqual +} + +type TimingSafeEqual = { + (a: string, b: string, hashFunction?: StringHashFunction): Promise + /** + * @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 + */ + ( + a: string | object | boolean, + b: string | object | boolean, + hashFunction?: Function + ): Promise +} +export const timingSafeEqual: TimingSafeEqual = async ( + a, + b, hashFunction?: Function ): Promise => { + if (typeof a === 'string' && typeof b === 'string') { + return timingSafeEqualString(a, b, hashFunction as StringHashFunction) + } + if (!hashFunction) { hashFunction = sha256 } const [sa, sb] = await Promise.all([hashFunction(a), hashFunction(b)]) - if (!sa || !sb) { + if (!sa || !sb || typeof sa !== 'string' || typeof sb !== 'string') { return false } - return sa === sb && a === b + return timingSafeEqualString(sa, sb) } export const bufferToString = (buffer: ArrayBuffer): string => {