/web-timing-safe-equal

A timing-safe comparison function utilizing the 'Double HMAC validation pattern' with webcrypto, designed to operate in Node.JS, Edge, and Browser environments.

Primary LanguageJavaScriptMIT LicenseMIT

Web Timing Safe Equal

A JavaScript library for timing-safe comparison of strings using the "Double HMAC verification" pattern. The library works in Node.js, Edge and Browser environments and uses the Subtle WebCrypto API under the hood.

Node.js
If you need a timing safe comparison function for Node.js only, then consider using the native crypto.timingSafeEqual implementation.

Installation

npm install @advena/web-timing-safe-equal

Usage

const { webTimingSafeEqual } = require("@advena/web-timing-safe-equal");

const left = "my secret value";
const right = "another secret value";

(async () => {
  const isEqual = await webTimingSafeEqual(left, right);
  console.log("Are the values equal?", isEqual); // false
})();

About web-timing-safe-equal

Inspiration

Node.JS has a native crypto.timingSafeEqual function for time constant comparison, which has been available since version 6 (2016).

However, there is no similar function for time safe comparison of values in web or edge environments. Cloudflare Workers runtime, upon which the Next.js Edge runtime is built, recently added the Node.js timingSafeEqual to their runtime as a non-standard extension. You can find more information in the Cloudflare Workers documentation. This means timingSafeEqual might be available in the latest Edge runtime.

There is a feature request with the Web3 Consortium to add a similar native function to the WebCrypto API. However, even if it is implemented, it may take years for browsers to support it.

Several alternative libraries, such as scmp and buffer-equal-constant-time, use the "Bitwise comparison with XOR" pattern. While this should work in theory, it may not be effective in practice due to potential time leaks caused by the way bits are implemented in JavaScript. Time-safe comparison with "Bitwise comparison with XOR" in JavaScript is difficult to implement and test, and it cannot be guaranteed to be secure. The ideal solution would be to implement a safe function at a lower level, outside of JavaScript.

In the meantime, we can use the "Double HMAC Verification" pattern as a common alternative for time-constant comparison. This pattern compares two HMACs: hmac(key, left) === hmac(key, right). In this case, it doesn't matter if === leaks length information, because the HMAC hashes constantly change. Therefore, even if the first byte of hmac(key, left) and hmac(key, right) matches, an attacker cannot rely on the assumption that they have correctly guessed it. Moving to the next byte will change the hash, effectively turning the attack into a brute-force attempt instead of a time-based one.

Performance

The webTimingSafeEqual function runs about 110 times slower than timingSafeEqual method. If you pre-generate a key or provide your own, then it's only 65 times slower than timingSafeEqual method.

crypto.timingSafeEqual('a','b') // fastest
(async()=>{
  await webTimingSafeEqual('a', 'b'); // 110 times slower

  const secretKey = await generateSecretKey();
  await webTimingSafeEqual('a', 'b', {secretKey}); // 65 times slower
})()

API

webTimingSafeEqual(left, right, [options])

Asynchronous method

  • left: (string | Uint8Array) - First value to compare
  • right: (string | Uint8Array) - Second value to compare
  • options: (object) - Optional configuration object
    • secretKey: (string | CryptoKey) - Optional HMAC key for comparison (default: auto-generated key)
    • keyLength: (integer) - Optional length in bytes of the auto-generated key (default: 64)
    • keyAlgorithm: ("SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") - Optional hash algorithm for the key (default: "SHA-256")
    • hmacAlgorithm: ("SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") - Optional hash algorithm for the HMAC (default: "SHA-256")

Returns a Promise that resolves to a boolean indicating whether the two values are equal.

generateSecretKey([options])

Asynchronous method

  • options: (object) - Optional configuration object
    • keyLength: (integer) - Length of key in bytes (default: 64)
    • algorithm: ("SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") - Hash algorithm (default: "SHA-256")

Returns a Promise that resolves to a randomly generated HMAC secret key (CryptoKey).

hmac(secretKey, message, [algorithm])

Asynchronous method

  • secretKey: (string | Uint8Array | CryptoKey) - Secret key for HMAC computation
  • message: (string | Uint8Array) - Message to compute the HMAC for
  • algorithm: ("SHA-1" | "SHA-256" | "SHA-384" | "SHA-512") - Optional hash algorithm for HMAC (default: "SHA-256")

Returns a Promise that resolves to a hexadecimal digest of the HMAC computation.

Security

This library implements the "Double HMAC verification" pattern to provide a timing-safe comparison of strings. However, it is crucial to understand that the security of your application depends on various factors, and using this library alone does not guarantee your application is secure from timing attacks. Please consult a security expert if you have concerns about the security of your application.

Todo

I've built this package for personal use. However, If it should ever gain traction then I would consider adding:

  • tests
  • CJS/ESM dual build
  • rewrite in typescript and provide types
  • ...?

Disclaimer

Please note that I am not responsible for any errors or issues that may arise from using this package. By using this package, you acknowledge that you are using it at your own risk and that I cannot be held accountable for any problems or damages that may occur as a result of using this v. Please ensure that you have adequate backups and precautions in place before implementing or using this package in any production or critical environments.

I myself have used it on one large Next.js client project and have not seen any issues yet.