Skip to content

Mise en œuvre de HOTP & TOTP

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

Updated: at 04:24 PM

Dans cet article, je fournirai un aperçu sommaire des principaux processus de mise en œuvre de HOTP et TOTP. Je n'entrerai pas dans les explications détaillées de chaque ligne de code ; à la place, l'accent principal sera mis sur l'extraction des étapes de mise en œuvre et la mise en évidence de certains points que j'ai rencontrés lors du processus de développement qui nécessitent une attention particulière.

Si vous souhaitez consulter des processus détaillés, des principes et des informations sur HOTP et TOTP, veuillez visiter le site Web suivant :

Pour mettre en œuvre RFC 6238 (TOTP), il a été indiqué dans la section Résumé de la norme que TOTP est une extension de HOTP (RFC 4226), ce qui nécessite donc la mise en œuvre de 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

La mise en œuvre de HOTP nécessite trois paramètres essentiels que nous utiliserons aujourd'hui :

  • Clé
  • Compteur
  • Chiffre
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

Utilisez l'algorithme HMAC-SHA1 pour générer la valeur de hachage hs. La clé est la K (Clé) mentionnée précédemment, et le message est C (Compteur sur 8 octets).

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);

Le code ci-dessus est écrit en C#, ne conservant que les parties principales à titre d'exemple.

Step 2

Cette étape nécessite l'utilisation d'une méthode appelée Dynamic Truncation pour convertir le résultat de hachage HMAC de 20 octets en un code fixe plus court, qui sert de mot de passe à usage unique.

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

Étant donné que la méthode hmac.ComputeHash dans .NET renverra un type byte[], je vais directement travailler avec des données de type byte[].

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
}

Cette étape a achevé l'ensemble de la deuxième étape, et le type de retour est un entier, car nous l'utiliserons pour des opérations numériques dans la troisième étape.

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

Dans cette étape, nous devons effectuer une opération de modulo sur le nombre que nous avons obtenu. L'algorithme est le suivant : num % 10^Digit.

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

À ce stade, la génération du mot de passe à usage unique pour HOTP est terminée, et nous allons le convertir en type chaîne de caractères.

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

Étant donné que nous travaillons avec des données numériques lors du calcul de la Troncature Dynamique et lors de l'exécution d'opérations de modulo sur des nombres, si un mot de passe à usage unique inclut un 0 dans la position la plus élevée, le 0 mathématique sera ignoré. Par conséquent, lorsque nous le convertissons en une chaîne de caractères (ou tout autre type de données dans n'importe quel langage qui représente du texte arbitraire), nous devons effectuer l'opération PadLeft correspondante. Dans ce cas, vous pouvez utiliser directement la méthode string.PadLeft() en C#.

Pour vérifier la justesse de son algorithme, des cas de test sont fournis aux développeurs dans le standard Annexe 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

Avec l'achèvement de la mise en œuvre de HOTP, nous avons déjà accompli plus de la moitié de l'ensemble du projet. Étant donné que TOTP est une extension de HOTP, notre charge de travail sera considérablement réduite.

Se rendre à la Section 4 de la norme constitue la description centrale de l'algorithme TOTP dans son ensemble.

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.

Selon la description ci-dessus, nous devons obtenir le pas de temps X et le paramètre de l'horodatage T (généralement au format horodatage UTC, mesuré en secondes). En général, le paramètre T0 est de 0, donc le calcul de soustraction peut être omis.

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

Enfin, en utilisant le paramètre T comme paramètre de « Counter » pour HOTP, vous pouvez obtenir le mot de passe à usage unique (OTP) pour 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

À cette étape, nous allons discuter de la justesse après que l'implémentation de l'algorithme TOTP est achevée... Tout d'abord, voici le code complet pour l'ensemble du projet.

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;
    }
}

Dans la norme, Annexe B, il existe également des cas de test similaires à HOTP. Cependant, veuillez noter que le paramètre Digit dans les cas de test est constitué de 8 chiffres, et non pas de 6 chiffres.

   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   |                  |          |        |
  +-------------+--------------+------------------+----------+--------+

Pour tester la justesse de cette mise en œuvre standard, il est nécessaire de calculer le même résultat TOTP en se basant sur les horodatages et les clés fournis dans les cas de test. Par conséquent, nous devons apporter de légères modifications et mettre à jour la logique liée au paramètre T de TOTP comme suit :

  +-------------+--------------+------------------+----------+--------+
  |  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 . . .

Regardez, nous sommes capables de calculer les résultats corrects donnés par les cas de test dans la norme.

Appendix

Base32

Dans des cas d'utilisation réels courants, les clés sont souvent encodées en utilisant le format Base32. Pour obtenir les résultats corrects, les développeurs doivent généralement décoder la clé en utilisant le décodage Base32 avant de continuer. Par conséquent, je vais fournir dans cet article l'implémentation que j'ai utilisée pour le décodage en Base32.

/*
 * 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();
    }
}

Pour tester l'efficacité du code TOTP avec des clés réelles, je recommande d'utiliser la page de test TOTP proposée par Authentication Test comme cas de test. La clé est généralement I65VU7K5ZQL7WB4E (mais veuillez vous référer à la clé fournie sur le site web). Le paramètre d'intervalle de temps X et le paramètre Digit sont généralement définis à des valeurs courantes de 30 et 6, respectivement.

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.
    }
}

Après avoir obtenu le résultat, saisissez le mot de passe TOTP, cliquez sur "Se connecter" et observez si vous pouvez vous connecter avec succès.

Written by:
Jimmy
Keywords:
C#, Tech, 2FA, TOTP, RFC Implementation

Other Languages

  • HOTP & TOTP Implementation

    Updated: at 04:24 PM

    A complete implementation of HOTP and TOTP algorithms

  • HOTP & TOTP 实现

    Updated: at 04:24 PM

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