Implementing RFC 4226: Creating a HOTP Generator in Node.js

This post assumes you are familiar with Node.js and the crypto module in Node.js, if you are not please see "What is Node.js" and/or "What Is the Crypto Module in Node.js?".


For those unaware of what RFC 4226 is, RFC 4226 is the HMAC--Based One-Time Password (HOTP) algorithm, which, stands as a fundamental method for generating one-time passwords used in two-factor authentication systems. Implementing this algorithm in Node.js can be achieved easily by levearging the built-in crypto module. To quote RFC 4226:

...the HOTP algorithm is based on the
   HMAC-SHA-1 algorithm (as specified in [RFC2104]) and applied to an
   increasing counter value representing the message in the HMAC
   computation.

   Basically, the output of the HMAC-SHA-1 calculation is truncated to
   obtain user-friendly values:

      HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

   where Truncate represents the function that can convert an HMAC-SHA-1
   value into an HOTP value.  K and C represent the shared secret and
   counter value; see [RFC4226] for detailed definitions.

To learn more about the HMAC-SHA-1 algorithm described in RFC 2104 please see "RFC 2104: HMAC: Keyed-Hashing for Message Authentication"

Understanding the HOTP Algorithm

The HOTP algorithm primarily utilizes HMAC-SHA-1, employing a secret key and a counter to produce a one-time password. The algorithm involves multiple steps, including:

  1. Generating an HMAC-SHA-1 hash by combining the secret key and counter.
  2. Extracting a dynamic binary value from the hash.
  3. Calculating a truncated numeric value from the binary, which constitutes the one-time password.

Implementing HOTP in Node.js

Step 1: The crypto Module

Since the crypto module provides the necessary tools to create HMAC-SHA-1 hashes and perform bitwise operations, implementing RFC 4226 is fairly trivial.

Firstly, we require the crypto module.

const crypto = require('crypto');

Step 2: Implement the HMAC-SHA-1 Generator

Since we already imported crypto via require we can utilize the built-in functions in the crypto module to create a new HMAC-SHA-1 hash based on a secret and a counter.

function generateHmac(secret, counter = 0) {
  const hmac = crypto.createHmac('sha1', secret);
  hmac.update(Buffer.alloc(8, 0)); // Initializing the counter with zeros
  hmac.update(Buffer.alloc(8).writeBigInt64BE(BigInt(counter), 0)); // Writing the counter value
  return hmac.digest('hex');
}
Initialize and implement the ability to create HMAC-SHA-1 hashes

The second line in the code snippet above initializes the HMAC-SHA-1 hash based on the secret, while the next two lines update the value of the hash based o the value of the counter. Finally, the last line of the function processes the updates and returns the value of the updated hash.

Converting the HMAC-SHA-1 Hash to an OTP Code

Now that we can generate an HMAC-SAH-1 based hash, we can use that hash to create our HOTP code. This is the Truncate functionality as described by the snippet taken from the RFC earlier.

function generateHOTP(secret, counter = 0, digits = 6) {
  const hmacResult = generateHmac(secret, counter);
  const offset = parseInt(hmacResult.slice(-1), 16) * 2;
  const binary = (parseInt(hmacResult.slice(offset, offset + 8), 16) & 0x7fffffff).toString(10);
  const otp = ('000000' + (parseInt(binary) % (10 ** digits))).slice(-digits);
  return otp;
}
Implement the function to convert HMAC-SHA-1 hashes to OTP codes

In the function above, the second line of code calls our previously defined generateHmac function and returns an HMAC-SHA-1 hash; however, now that we have the value of the hash, on the third line, we extract the offset for the starting point based on the last character of the hash. After extracting the offset we retrieve a dynamic binary value from a specific portion of the hash and apply a bitwise operation (& 0x7fffffff) to ensure it is a positive integer, then after ensuring the value is positive, we return the numeric one-time password by taking the modulo (%) of the binary value with 10 ** digits and then padding it with leading zeros to reach the desired number of digits (which defaults to 6).

How to use

const secret = 'YourSecretKeyHere'; // Replace with your secret key
const counter = 0; // Replace with the current counter value
const generatedHOTP = generateHOTP(secret, counter);
console.log('Generated HOTP:', generatedHOTP);

The above code shows how you would utilize the above functions to generate an HOTP code, when implementing the above code make sure to replace 'YourSecretKeyHere' with your actual secret key and adjust the counter value based on your current counter.

Implementing RFC 4226 to generate HOTP in Node.js relies on the core principles of cryptographic algorithms. This approach offers a deeper understanding of the algorithm's inner workings and fosters a foundation for implementing other cryptographic functionalities.


The full code for this post can be found below, and hopefully this article helped you learn about how to generate an HOTP in Node.js!

const crypto = require('crypto');

function generateHmac(secret, counter) {
  const hmac = crypto.createHmac('sha1', secret);
  hmac.update(Buffer.alloc(8, 0)); // Initializing the counter with zeros
  hmac.update(Buffer.alloc(8).writeBigInt64BE(BigInt(counter), 0)); // Writing the counter value
  return hmac.digest('hex');
}

function generateHOTP(secret, counter = 0, digits = 6) {
  const hmacResult = generateHmac(secret, counter);
  const offset = parseInt(hmacResult.slice(-1), 16) * 2;
  const binary = (parseInt(hmacResult.slice(offset, offset + 8), 16) & 0x7fffffff).toString(10);
  const otp = ('000000' + (parseInt(binary) % (10 ** digits))).slice(-digits);
  return otp;
}

// Example usage
const secret = 'YourSecretKeyHere'; // Replace with your secret key
const counter = 0; // Replace with the current counter value
const generatedHOTP = generateHOTP(secret, counter);
console.log('Generated HOTP:', generatedHOTP);