Generate your own one-time password like authenticator apps

  • Date: September 26, 2023

Time-based one time password

Authenticator apps like FreeOTP, Authy, and Google Authenticator support something called Time-based One Time Password, or TOTP for short. TOTP is one of the most popular 2-factor-authentication methods, used by websites such as Discord, Github, Facebook, Instagram, Outlook, Docker, USCIS.gov, etc.

Below is a live Time-based one time password generator. The password changes every 30 seconds.

000 000
-

Other potential OTP values due to clock skew: -

Why TOTP?

The clever thing about TOTP is that the authenticator app can just generate short lived one-time passwords without contacting any server, requiring no connection. In contrast, other means of 2-factor authentication such as push notification, sending over SMS or email require a working connection, working email address, or phone number.

In this article, we'll examine how TOTPs are generated, how the server side validates the authenticity of the TOTP, and implement our own generator and validator. Both of which are relatively straight forward.

High level

At a high level, the protocol to generate a one-time password is relatively simple:

  1. The server and client agree upon a shared secret
  2. Get the current timestamp
  3. Hash this timestamp with the secret
  4. Derive the password from the hash result
  5. Profit!

The shared secret

To start, we need a shared secret that the validator and the generator agreed upon. This secret is per-user, so the validator (the server side) usually generates and stores this secret. The secret is then exposed as a QR code or as a base32 string to the generator (the authenticator app).

According to RFC 4226, a secret should be at least 128 bits, but recommended to be 160 bits.

If you're following along and implementing your own, you can use the following secret to cross check the result of your output against the demo above.

secret = byte[] {
    77, 250, 238, 242, 124, 49, 117, 228, 180, 187,
    159, 76, 67, 37, 210, 40, 57, 28, 18, 116,
}
secretHex = "4dfaeef27c3175e4b4bb9f4c4325d228391c1274"
secretBase32 = "JX5O54T4GF26JNF3T5GEGJOSFA4RYETU"

One-time password generation

As the name suggested, TOTP generates one-time passwords using the calendar time. That is pretty much the only variable in generating TOTP (see more details in RFC 6238). The overall algorithm involves the following steps:

  1. Get the current Unix timestamp (number of seconds since 00:00:00 UTC on 1 January 1970).
  2. Divide the timestamp by 30. We'll call this the input. This effectively means that we can generate a single one-time password every 30 seconds, for each given secret.
  3. Calculate a SHA1-HMAC using the secret and the timestamp.
    • The HMAC is essentially just SHA1(secret + SHA1(secret + input)), with some block size checking, etc. Most programming languages should have libraries readily available.
    • The input (timestamp / 30) is converted to binary using Big Endian encoding.
    • The output of this step is a 20-byte array.
  4. From the HMAC, we'll derive the final output as followed:
    1. Take the last 4 bits of the HMAC. This will give us a value between 0 - 15 to use as the byte starting offset into the HMAC array.
    2. Starting from this offset in the array, take 4 bytes.
    3. Use these 4 bytes to make a 32-bit integer, again using the Big Endian encoding.
    4. Remove the first bit of the integer, effectively making it a 31-bit integer.
    5. Mod this integer by 1,000,00, which will give us a number that's at most 6 digit. This will be our OTP.

In this sample implementation, the main implementation is in the generateOTPWithOffset function. We'll use this offset for the validator. For the generator, or the authenticator app, this offset is zero:

var secret = []byte{ ... }

func calcHmac(input int64) []byte {
    inputBin := make([]byte, 8)
    binary.BigEndian.PutUint64(inputBin, uint64(input))

    hmac := hmac.New(sha1.New, secret)
    hmac.Write(inputBin)
    return hmac.Sum(nil)
}

// Generate a one-time password with an offset to the input variable
func generateOTPWithOffset(offset int) uint32 {
    // The input is the current timestamp / 30, meaning we have a unique input every 30 seconds
    input := (time.Now().Unix() / 30) + int64(offset)
    hmac := t.hmac(input)

    // Once the HMAC is calculated, we can derive the one-time-password from it:
    // 1. Use the last 4 bits of the HMAC as the offset into the array
    start := hmac[len(hmac)-1] & 0x0F
    // 2. Take 4 bytes from the array (32 bits), and remove the first bit per RFC 4226
    passwordBin := hmac[start : start+4]
    passwordBin[0] &= 0x7F
    // 3. Once we have a 31 bit integer, cap it to 6 digits by modding 1,000,000
    password := binary.BigEndian.Uint32(passwordBin) % 1_000_000

    return password
}

// Generate a one-time password
func generateOTP() uint32 {
    return generateOTPWithOffset(0)
}

Validating the one-time password

The validator side simply follows the same process above to generate a one-time password and then compares it to the input from the user.

However, the validator needs to take the following into account:

  • The clock on the validator might not match the clock on the generator. This is known as clock skew.
  • Since each OTP is only valid for 30 seconds, by the time it gets to the validator, it is no longer valid, even if the clock matches between the validator and the generator.

Due to these issues, the validator often accepts OTPs generated within a time range. For example, validating the OTP against the following range will allow the clock skew 30 seconds behind or ahead:

  • generateOTP((now / 30) - 1)
  • generateOTP(now / 30)
  • generateOTP((now / 30) + 1)

How large his range should be, depends on the security model of the validator.

Below is a simplistic implementation of the validator function. The server side or the validator side should keep track of code that was successfully used recently in order to prevent replay attacks.

// Validate the input
func validate(input uint32) bool {
    // Before checking for the validity of this input,
    // check if the input had recently been used.
    // If the input is already used within a short time frame,
    // we should reject it to prevent replay attacks.
    // Recency should coordinate with the offset used below.
    if codeRecentlyUsed(input) {
        return false
    }

    // Allow the input to be one OTP behind or ahead
    for offset := -1; offset <= 1; offset++ {
        if generateOTPWithOffset(offset) == input {
            storeRecentlyUsedCode(time.Now().Unix(), input);
                
            return true
        }
    }

    return false
}

Summary

The TOTP protocol is relatively simple in nature, yet effective. The sample implementation in the article follows the default parameters in RFC 4226. However, some services and authenticator apps allow customizing the number of digits, duration of the password, and the hashing function. Those parameters can be adjusted to your security needs.