Skip to content

HOTP & TOTP Implementation

A complete implementation of HOTP and TOTP algorithms

Updated: at 04:24 PM

In this article, I will provide a rough overview of the main process from implementing HOTP to TOTP. I won’t delve into the detailed explanations of each line of code; instead, the primary focus will be on extracting the implementation steps and highlighting some points I encountered during the development process that require attention.

If you wish to refer to detailed processes, principles, and information about HOTP and TOTP, please visit the following website:

To implement RFC 6238 (TOTP), it has been stated in the standard's Abstract section that TOTP is an extension of HOTP (RFC 4226), thus requiring the implementation of RFC 4226 (HOTP).

This document describes an extension of the One-Time Password (OTP) algorithm, namely the HMAC-based One-Time Password (HOTP) algorithm, as defined in RFC 4226, to support the time-based moving factor.

RFC 4226 (HOTP) Implementation

Implementing HOTP requires three essential parameters that we will be using today:

  • Key
  • Counter
  • Digit
Symbol  Represents
   -------------------------------------------------------------------
   C       8-byte counter value, the moving factor.  This counter
           MUST be synchronized between the HOTP generator (client)
           and the HOTP validator (server).

   K       shared secret between client and server; each HOTP
           generator has a different and unique secret K.

   T       throttling parameter: the server will refuse connections
           from a user after T unsuccessful authentication attempts.

   s       resynchronization parameter: the server will attempt to
           verify a received authenticator across s consecutive
           counter values.

   Digit   number of digits in an HOTP value; system parameter.

Step 1

Using the HMAC-SHA1 algorithm, generate the hash value hs. The key is the aforementioned K (Key), and the message is C (Counter in 8 bytes).

Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C)  // HS is a 20-byte string
var hmac = new HMACSHA1(Secret);
var hs = hmac.ComputeHash(Counter);

The code above is written in C#, keeping only the main parts as an example.

Step 2

This step requires using a method called Dynamic Truncation to convert the 20-byte HMAC hash result into a shorter fixed code, which serves as a one-time password.

   Step 2: Generate a 4-byte string (Dynamic Truncation)
   Let Sbits = DT(HS)   //  DT, defined below,
                        //  returns a 31-bit string

Since the hmac.ComputeHash method in .NET will return a byte[] type, I will be directly working with data of byte[] type.

private int DynamicTruncation(byte[] p)
{
    var offset = p.Last() & 0x0F; // Offset value

    return ((p[offset] & 0x7F) << 24) |
        ((p[offset + 1] & 0xFF) << 16) |
        ((p[offset + 2] & 0xFF) << 8) |
        (p[offset + 3] & 0xFF); // Return the Last 31 bits of p
}

This step has completed the entire second step, and the return type is an integer, as we will use it for numerical operations in the third step.

Step 3

   Step 3: Compute an HOTP value
   Let Snum  = StToNum(Sbits)   // Convert S to a number in
                                    0...2^{31}-1
   Return D = Snum mod 10^Digit //  D is a number in the range
                                    0...10^{Digit}-1

In this step, we need to perform a modulo operation on the number we obtained. The algorithm is num % 10^Digit.

var code = (DynamicTruncation(hs) % (int)Math.Pow(10, Digit)) // hs is defined in Step 1.

At this point, the one-time password generation for HOTP is complete, and we will convert it to a string type.

var code = (DynamicTruncation(hs) % (int)Math.Pow(10, Digit)).ToString().PadLeft(Digit, '0');

Since we are working with numerical data when calculating Dynamic Truncation and performing modulo operations on numbers, if a one-time password includes a 0 in the highest order of magnitude, the mathematical 0 will be discarded. Therefore, when we convert this to a string (or any data type in any language that represents arbitrary text), we need to apply the corresponding PadLeft operation. In this case, you can use the string.PadLeft() method directly in C#.

To validate its algorithm correctness, test cases are provided for developers in the standard Appendix D.

Appendix D - HOTP Algorithm: Test Values

   The following test data uses the ASCII string
   "12345678901234567890" for the secret:

   Secret = 0x3132333435363738393031323334353637383930

   Table 1 details for each count, the intermediate HMAC value.

   Count    Hexadecimal HMAC-SHA-1(secret, count)
   0        cc93cf18508d94934c64b65d8ba7667fb7cde4b0
   1        75a48a19d4cbe100644e8ac1397eea747a2d33ab
   2        0bacb7fa082fef30782211938bc1c5e70416ff44
   3        66c28227d03a2d5529262ff016a1e6ef76557ece
   4        a904c900a64b35909874b33e61c5938a8e15ed1c
   5        a37e783d7b7233c083d4f62926c7a25f238d0316
   6        bc9cd28561042c83f219324d3c607256c03272ae
   7        a4fb960c0bc06e1eabb804e5b397cdc4b45596fa
   8        1b3c89f65e6c9e883012052823443f048b4332db
   9        1637409809a679dc698207310c8c7fc07290d9e5

   Table 2 details for each count the truncated values (both in
   hexadecimal and decimal) and then the HOTP value.

                     Truncated
   Count    Hexadecimal    Decimal        HOTP
   0        4c93cf18       1284755224     755224
   1        41397eea       1094287082     287082
   2         82fef30        137359152     359152
   3        66ef7655       1726969429     969429
   4        61c5938a       1640338314     338314
   5        33c083d4        868254676     254676
   6        7256c032       1918287922     287922
   7         4e5b397         82162583     162583
   8        2823443f        673399871     399871
   9        2679dc69        645520489     520489
class HOTP
{
    private byte[] Secret { get; set; }
    private byte[] Counter { get; set; }
    private int Digit { get; set; }

    public HOTP(byte[] secret, int counter, int digit)
    {
        Secret = secret;
        Digit = digit;
        var bytes = new byte[8];

        for (var i = 7; i >= 0; i--)
        {
            bytes[i] = (byte)(counter & 0xFF);

            counter >>= 8;
        }

        Counter = bytes;
    }

    public string Code()
    {
        var hmac = new HMACSHA1(Secret);
        var hs = hmac.ComputeHash(Counter);
        var code = (DynamicTruncation(hs) % (int)Math.Pow(10, Digit)).ToString().PadLeft(Digit, '0');

        return code;
    }

    private int DynamicTruncation(byte[] p)
    {
        var offset = p.Last() & 0x0F;

        return ((p[offset] & 0x7F) << 24) |
            ((p[offset + 1] & 0xFF) << 16) |
            ((p[offset + 2] & 0xFF) << 8) |
            (p[offset + 3] & 0xFF);
    }
}
class Program
{
    public static void Main()
    {
        for (int i = 0; i != 10; i++)
        {
            var hotp = new HOTP(Encoding.ASCII.GetBytes("12345678901234567890"), i, 6);

            Console.WriteLine(hotp.Code());
        }
    }
}
755224
287082
359152
969429
338314
254676
287922
162583
399871
520489

C:\Users\xyfbs\source\repos\ConsoleTotp\ConsoleTotp\bin\Debug\net7.0\ConsoleTotp.exe (process 26588) exited with code 0.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

RFC 6238 (TOTP) Implementation

With the completion of the implementation of HOTP, we have already accomplished more than half of the entire project. Since TOTP is an extension of HOTP, our workload will be significantly reduced.

Navigating to the standard's Section 4 is the core description of the entire TOTP algorithm.

Step 1

   o  X represents the time step in seconds (default value X =
      30 seconds) and is a system parameter.

   o  T0 is the Unix time to start counting time steps (default value is
      0, i.e., the Unix epoch) and is also a system parameter.

   Basically, we define TOTP as TOTP = HOTP(K, T), where T is an integer
   and represents the number of time steps between the initial counter
   time T0 and the current Unix time.

   More specifically, T = (Current Unix time - T0) / X, where the
   default floor function is used in the computation.

   For example, with T0 = 0 and Time Step X = 30, T = 1 if the current
   Unix time is 59 seconds, and T = 2 if the current Unix time is
   60 seconds.

According to the description above, we need to obtain the time step X and the timestamp parameter T (usually in UTC timestamp format, measured in seconds). In general, the parameter T0 is 0, so subtraction calculation can be omitted.

var x = 30; // Time step in seconds
var timeestamp = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; // Current UTC timestamp
var t = (int)Math.Floor(unixTimestamp / x);

Step 2

Finally, by using the parameter T as the Counter parameter for HOTP, you can obtain the one-time password (OTP) for TOTP.

var totp = new HOTP(SECRET, t, DIGIT); // This is pseudocode, please refer to the complete code example provided at the end of the article.
var code = totp.Code(); // Also pseudocode...

Step 3

In this step, we will discuss the correctness after the implementation of the TOTP algorithm is completed... First of all, here is the complete code for the entire project.

class HOTP
{
    private byte[] Secret { get; set; }
    private byte[] Counter { get; set; }
    private int Digit { get; set; }

    public HOTP(byte[] secret, int counter, int digit)
    {
        Secret = secret;
        Digit = digit;
        var bytes = new byte[8];

        for (var i = 7; i >= 0; i--)
        {
            bytes[i] = (byte)(counter & 0xFF);

            counter >>= 8;
        }

        Counter = bytes;
    }

    public string Code()
    {
        var hmac = new HMACSHA1(Secret);
        var hs = hmac.ComputeHash(Counter);
        var code = (DynamicTruncation(hs) % (int)Math.Pow(10, Digit)).ToString().PadLeft(Digit, '0');

        return code;
    }

    private int DynamicTruncation(byte[] p)
    {
        var offset = p.Last() & 0x0F;

        return ((p[offset] & 0x7F) << 24) |
            ((p[offset + 1] & 0xFF) << 16) |
            ((p[offset + 2] & 0xFF) << 8) |
            (p[offset + 3] & 0xFF);
    }
}

public class TOTP
{
    private byte[] Secret;
    private int Timestep = 30;
    private int Digit = 6;

    public TOTP(byte[] secret)
    {
        Secret = secret;
    }

    public TOTP(byte[] secret, int timestep)
    {
        Secret = secret;
        Timestep = timestep;
    }

    public TOTP(byte[] secret, int timestep, int digit)
    {
        Secret = secret;
        Timestep = timestep;
        Digit = digit;
    }

    public string Code()
    {
        var unixTimestamp = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
        var t = (int)Math.Floor(unixTimestamp / Timestep);
        var hotp = new HOTP(Secret, t, Digit);
        var code = hotp.Code();

        return code;
    }
}

In the standard, Appendix B, there are also test cases similar to HOTP. However, please note that the Digit parameter in the test cases consists of 8 digits, not 6 digits.

   The test token shared secret uses the ASCII string value
   "12345678901234567890".  With Time Step X = 30, and the Unix epoch as
   the initial value to count time steps, where T0 = 0, the TOTP
   algorithm will display the following values for specified modes and
   timestamps.

  +-------------+--------------+------------------+----------+--------+
  |  Time (sec) |   UTC Time   | Value of T (hex) |   TOTP   |  Mode  |
  +-------------+--------------+------------------+----------+--------+
  |      59     |  1970-01-01  | 0000000000000001 | 94287082 |  SHA1  |
  |             |   00:00:59   |                  |          |        |
  |      59     |  1970-01-01  | 0000000000000001 | 46119246 | SHA256 |
  |             |   00:00:59   |                  |          |        |
  |      59     |  1970-01-01  | 0000000000000001 | 90693936 | SHA512 |
  |             |   00:00:59   |                  |          |        |
  |  1111111109 |  2005-03-18  | 00000000023523EC | 07081804 |  SHA1  |
  |             |   01:58:29   |                  |          |        |
  |  1111111109 |  2005-03-18  | 00000000023523EC | 68084774 | SHA256 |
  |             |   01:58:29   |                  |          |        |
  |  1111111109 |  2005-03-18  | 00000000023523EC | 25091201 | SHA512 |
  |             |   01:58:29   |                  |          |        |
  |  1111111111 |  2005-03-18  | 00000000023523ED | 14050471 |  SHA1  |
  |             |   01:58:31   |                  |          |        |
  |  1111111111 |  2005-03-18  | 00000000023523ED | 67062674 | SHA256 |
  |             |   01:58:31   |                  |          |        |
  |  1111111111 |  2005-03-18  | 00000000023523ED | 99943326 | SHA512 |
  |             |   01:58:31   |                  |          |        |
  |  1234567890 |  2009-02-13  | 000000000273EF07 | 89005924 |  SHA1  |
  |             |   23:31:30   |                  |          |        |
  |  1234567890 |  2009-02-13  | 000000000273EF07 | 91819424 | SHA256 |
  |             |   23:31:30   |                  |          |        |
  |  1234567890 |  2009-02-13  | 000000000273EF07 | 93441116 | SHA512 |
  |             |   23:31:30   |                  |          |        |
  |  2000000000 |  2033-05-18  | 0000000003F940AA | 69279037 |  SHA1  |
  |             |   03:33:20   |                  |          |        |
  |  2000000000 |  2033-05-18  | 0000000003F940AA | 90698825 | SHA256 |
  |             |   03:33:20   |                  |          |        |
  |  2000000000 |  2033-05-18  | 0000000003F940AA | 38618901 | SHA512 |
  |             |   03:33:20   |                  |          |        |
  | 20000000000 |  2603-10-11  | 0000000027BC86AA | 65353130 |  SHA1  |
  |             |   11:33:20   |                  |          |        |
  | 20000000000 |  2603-10-11  | 0000000027BC86AA | 77737706 | SHA256 |
  |             |   11:33:20   |                  |          |        |
  | 20000000000 |  2603-10-11  | 0000000027BC86AA | 47863826 | SHA512 |
  |             |   11:33:20   |                  |          |        |
  +-------------+--------------+------------------+----------+--------+

To test the correctness of this standard implementation, it is necessary to compute the same TOTP result based on the timestamps and keys provided in the test cases. Therefore, we need to make slight modifications and update the logic related to the T parameter of TOTP as follows:

  +-------------+--------------+------------------+----------+--------+
  |  Time (sec) |   UTC Time   | Value of T (hex) |   TOTP   |  Mode  |
  +-------------+--------------+------------------+----------+--------+
  |  1234567890 |  2009-02-13  | 000000000273EF07 | 89005924 |  SHA1  |
  +-------------+--------------+------------------+----------+--------+
public string Code()
{
    var unixTimestamp = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
    var testTimestamp = 1234567890;
    var t = (int)Math.Floor((double)testTimestamp / Timestep);
    var hotp = new HOTP(Secret, t, Digit);
    var code = hotp.Code();

    return code;
}
class Program
{
    public static void Main()
    {
        var totp = new TOTP(Encoding.ASCII.GetBytes("12345678901234567890"), 30, 8);

        Console.WriteLine(totp.Code());
    }
}
89005924

C:\Users\xyfbs\source\repos\ConsoleTotp\ConsoleTotp\bin\Debug\net7.0\ConsoleTotp.exe (process 45364) exited with code 0.
To automatically close the console when debugging stops, enable Tools->Options->Debugging->Automatically close the console when debugging stops.
Press any key to close this window . . .

Look, we are able to calculate the correct results given by the test cases in the standard.

Appendix

Base32

In typical real-world usage, keys are often encoded using Base32. To obtain the correct results, developers usually need to decode the key using Base32 before proceeding. As a result, I will provide the implementation I've used for Base32 decoding in this article.

/*
 * Derived from https://github.com/google/google-authenticator-android/blob/master/AuthenticatorApp/src/main/java/com/google/android/apps/authenticator/Base32String.java
 *
 * Copyright (C) 2016 BravoTango86
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Text;

public static class Base32
{
    private static readonly char[] _digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
    private const int _mask = 31;
    private const int _shift = 5;

    private static int CharToInt(char c)
    {
        switch (c)
        {
            case 'A': return 0;
            case 'B': return 1;
            case 'C': return 2;
            case 'D': return 3;
            case 'E': return 4;
            case 'F': return 5;
            case 'G': return 6;
            case 'H': return 7;
            case 'I': return 8;
            case 'J': return 9;
            case 'K': return 10;
            case 'L': return 11;
            case 'M': return 12;
            case 'N': return 13;
            case 'O': return 14;
            case 'P': return 15;
            case 'Q': return 16;
            case 'R': return 17;
            case 'S': return 18;
            case 'T': return 19;
            case 'U': return 20;
            case 'V': return 21;
            case 'W': return 22;
            case 'X': return 23;
            case 'Y': return 24;
            case 'Z': return 25;
            case '2': return 26;
            case '3': return 27;
            case '4': return 28;
            case '5': return 29;
            case '6': return 30;
            case '7': return 31;
        }
        return -1;
    }

    public static byte[] FromBase32String(string encoded)
    {
        if (encoded == null)
            throw new ArgumentNullException(nameof(encoded));

        // Remove whitespace and padding. Note: the padding is used as hint
        // to determine how many bits to decode from the last incomplete chunk
        // Also, canonicalize to all upper case
        encoded = encoded.Trim().TrimEnd('=').ToUpper();
        if (encoded.Length == 0)
            return new byte[0];

        var outLength = encoded.Length * _shift / 8;
        var result = new byte[outLength];
        var buffer = 0;
        var next = 0;
        var bitsLeft = 0;
        var charValue = 0;
        foreach (var c in encoded)
        {
            charValue = CharToInt(c);
            if (charValue < 0)
                throw new FormatException("Illegal character: `" + c + "`");

            buffer <<= _shift;
            buffer |= charValue & _mask;
            bitsLeft += _shift;
            if (bitsLeft >= 8)
            {
                result[next++] = (byte)(buffer >> (bitsLeft - 8));
                bitsLeft -= 8;
            }
        }

        return result;
    }

    public static string ToBase32String(byte[] data, bool padOutput = false)
    {
        return ToBase32String(data, 0, data.Length, padOutput);
    }

    public static string ToBase32String(byte[] data, int offset, int length, bool padOutput = false)
    {
        if (data == null)
            throw new ArgumentNullException(nameof(data));

        if (offset < 0)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (length < 0)
            throw new ArgumentOutOfRangeException(nameof(length));

        if ((offset + length) > data.Length)
            throw new ArgumentOutOfRangeException();

        if (length == 0)
            return "";

        // SHIFT is the number of bits per output character, so the length of the
        // output is the length of the input multiplied by 8/SHIFT, rounded up.
        // The computation below will fail, so don't do it.
        if (length >= (1 << 28))
            throw new ArgumentOutOfRangeException(nameof(data));

        var outputLength = (length * 8 + _shift - 1) / _shift;
        var result = new StringBuilder(outputLength);

        var last = offset + length;
        int buffer = data[offset++];
        var bitsLeft = 8;
        while (bitsLeft > 0 || offset < last)
        {
            if (bitsLeft < _shift)
            {
                if (offset < last)
                {
                    buffer <<= 8;
                    buffer |= (data[offset++] & 0xff);
                    bitsLeft += 8;
                }
                else
                {
                    int pad = _shift - bitsLeft;
                    buffer <<= pad;
                    bitsLeft += pad;
                }
            }
            int index = _mask & (buffer >> (bitsLeft - _shift));
            bitsLeft -= _shift;
            result.Append(_digits[index]);
        }
        if (padOutput)
        {
            int padding = 8 - (result.Length % 8);
            if (padding > 0) result.Append('=', padding == 8 ? 0 : padding);
        }
        return result.ToString();
    }
}

To test the usability of the TOTP code with real keys, I recommend using the TOTP testing page provided by Authentication Test as a test case. The key is typically I65VU7K5ZQL7WB4E (but please refer to the key provided on the website). The time step X parameter and the Digit parameter are usually set to common values of 30 and 6, respectively.

class Program
{
    public static void Main()
    {
        var totp = new TOTP(Base32.FromBase32String("I65VU7K5ZQL7WB4E"), 30, 6);

        Console.WriteLine(totp.Code()); // You will obtain a usable TOTP one-time password result.
    }
}

Once you have the result, enter the TOTP password, click on "Login," and observe whether you can successfully log in.

作者:
Jimmy
关键词:
C#, RFC Implementation, 2FA, TOTP, Tech

参考

Hsieh, V. (2023). Implementing your own time-based OTP generator. In Medium. The Zeals Tech Blog. https://medium.com/zeals-tech-blog/implementing-your-own-time-based-otp-generator-3b971a31330b
MRaihi, D., Bellare, M., Hoornaert, F., Naccache, D., & Ranen, O. (1970). HOTP: An HMAC-based one-time password algorithm. In RFC 4226. https://www.rfc-editor.org/rfc/rfc4226.html
MRaihi, D., Machani, S., Pei, M., & Rydell, J. (1970). TOTP: Time-based one-time password algorithm. In RFC 6238. https://www.rfc-editor.org/rfc/rfc6238.html
(2024a). In Wikipedia - HMAC-based one-time password. Wikimedia Foundation. https://en.wikipedia.org/wiki/HMAC-based_one-time_password
(2024b). In Wikipedia - Time-based one-time password. Wikimedia Foundation. https://en.wikipedia.org/wiki/Time-based_one-time_password

其他语言

  • Mise en œuvre de HOTP & TOTP

    Updated: at 04:24 PM

    Une implémentation complète des algorithmes HOTP et TOTP

  • HOTP & TOTP 实现

    Updated: at 04:24 PM

    一个完整的 HOTP 和 TOTP 算法实现