HOTP & TOTP Implementation [En & Fr] / HOTP & TOTP 实现

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.

Dans cet article, je vais donner un aperçu général du processus principal de la mise en œuvre de HOTP à 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.

在这篇文章中,我会粗略的介绍从实现 HOTP 到 TOTP 的主要重点过程。我不会细致的阐述每行代码的作用,相反,这篇文章主要提炼出实现步骤和我在开发过程中遇到的一些需要注意的地方。

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

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

如果你想参考详细的 HOTP 和 TOTP 的过程,原理和资料,请查看下列的网站:

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

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

为了实现 RFC 6238 (TOTP),根据标准的 Abstract 部分已经说明 TOTP 是 HOTP 的一个拓展,因此需要对 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 / RFC 4226 (HOTP) 实现

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

  • Key
  • Counter
  • Digit

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

  • Clé
  • Compteur
  • Chiffre

实现 HOTP 有三个重要的,今天我们会用到的参数

  • Key
  • Counter
  • Digit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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).

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

利用 HMAC-SHA1 算法生成出散列值 hs。密钥是上文提到的 K (Key) ,消息为 C (Counter in 8-byte)

1
Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C)  // HS is a 20-byte string
1
2
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.

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

上面的代码使用 C# 编写,仅保留主要部分作为示例

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.

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.

这一步需要使用一个叫做 Dynamic Truncdation 的方法,将 20 字节的 HMAC 哈希结果转成一个较短的固定代码作为一次性密码

1
2
3
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.

É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[].

由于 .NET 中的 hmac.ComputeHash 方法将会返回 byte[] 类型,我将直接操作 byte[] 类型的数据

1
2
3
4
5
6
7
8
9
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.

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

1
2
3
4
5
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.

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.

这一步需要将我们得到的数字进行取模运算,算法为 num % 10^Digit

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

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

到这一步就完成了 HOTP 的一次性密码生成,我们将它转为字符串类型

1
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#.

É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#.

由于在计算 Dynamic Truncation 和对数字进行取模运算时,我们的数据类型为数字。因此,如果一次性密码中包含有 0 出现在数量级最高位,在数学中的 0 将会被舍弃。所以在我们转换成字符串 (或者在任何语言中可以代表任意文本的数据类型) 需要进行相应的 PadLeft 操作,在此,C# 中可以直接使用 string.PadLeft() 方法

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

Pour vérifier la justesse de son algorithme, des cas de test sont fournis aux développeurs dans le standard Annexe D.

为了验证其算法正确性,在标准的附录 D 中携带了测试用例供开发者测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
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());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 / RFC 6238 (TOTP) 实现

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.

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.

完成了对于 HOTP 的实现,我们就已经做到了整个项目的一大半了。因为 TOTP 是对 HOTP 的一个拓展,因此我们的工作量会大大减少。

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

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

前往标准的 Section 4 部分是对整个 TOTP 算法的核心描述

Step 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.

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.

根据上面的描述,我们需要获得时间步长 X 和时间戳参数 T (一般为 UTC 时间戳,单位为秒)。一般情况下,参数 T0 为 0,因此可以不执行减法计算。

1
2
3
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.

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.

最后,将参数 T 作为 HOTP 的 Counter 参数传入,就可以得到 TOTP 的一次性密码

1
2
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.

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

这一步我们将探讨 TOTP 算法实现完毕后的正确性…首先,这是整个项目完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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.

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.

在标准的 Appendix B 中同样具有跟 HOTP 一样的测试用例。但请注意,测试用例中的 Digit 参数为 8 位,而不是 6 位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
 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:

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 :

为了测试该标准实现的正确性,需要根据测试用例中提供的时间戳和密钥计算出同样的 TOTP 结果。因此我们需要稍作修改,将 TOTP 的 T 参数相关的逻辑代码修改成如下:

1
2
3
4
5
+-------------+--------------+------------------+----------+--------+
| Time (sec) | UTC Time | Value of T (hex) | TOTP | Mode |
+-------------+--------------+------------------+----------+--------+
| 1234567890 | 2009-02-13 | 000000000273EF07 | 89005924 | SHA1 |
+-------------+--------------+------------------+----------+--------+
1
2
3
4
5
6
7
8
9
10
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;
}
1
2
3
4
5
6
7
8
9
class Program
{
public static void Main()
{
var totp = new TOTP(Encoding.ASCII.GetBytes("12345678901234567890"), 30, 8);

Console.WriteLine(totp.Code());
}
}
1
2
3
4
5
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.

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

看,我们能计算出标准中的测试用例给的正确结果了

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.

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.

在一般真实的使用情况下,密钥通常经过了 Base32 编码。为了获得正确的结果,通常开发者需要先对密钥进行 Base32 解码的操作。因此我将在本文章提供我使用的 Base32 解码的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
/*
* 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.

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.

为了测试 TOTP 代码在真实密钥情况下的可用性,我推荐 Authentication Test 的 TOTP 测试页面作为测试用例。密钥一般为 I65VU7K5ZQL7WB4E (但请还是根据网站提供的密钥为准)。时间步长 X 参数和 Digit 参数分别为常用的 30 和 6。

1
2
3
4
5
6
7
8
9
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.

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.

得到结果后,将 TOTP 密码填入,点击登录。并且观察是否能登陆成功即可。

C++ Version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#pragma once

#include <iostream>
#include <string>

#include <openssl/hmac.h>
#include <openssl/sha.h>

namespace std
{
class OtpTools
{
public:
static string generate_hmacsha1(const string& key, const unsigned char* data, size_t data_length);
static string hex_to_string(const string& hex_string);
static string pad_left(const string& input, size_t total_length, char padding_char);
static int get_current_utc_timestamp();
};

class Hotp
{
public:
Hotp(string secret, unsigned int digit, unsigned int counter);
string gen_code();
bool verify_code(int code);
bool verify_code(string code);
private:
string secret;
unsigned int digit;
unsigned char counter[8];

int dynamic_truncation(string p);
};

class Totp
{
public:
Totp(string secret, unsigned int time_step, unsigned int digit);
string gen_code();
bool verify_code(int code);
bool verify_code(string code);
private:
string secret;
unsigned int time_step;
unsigned int digit;
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
// libotp.cpp : Defines the functions for the static library.
//
#include "pch.h"

#include "libotp.h"

namespace std
{
string OtpTools::generate_hmacsha1(const string& key, const unsigned char* data, size_t data_length)
{
unsigned char* result = HMAC(
EVP_sha1(),
key.c_str(), static_cast<int>(key.length()),
data, data_length,
nullptr, nullptr
);

if (result == nullptr)
{
cerr << "HMAC-SHA1 calculation failed." << endl;

return "";
}

string hmac_result;
char buffer[3];

for (int i = 0; i < SHA_DIGEST_LENGTH; ++i)
{
snprintf(buffer, sizeof(buffer), "%02x", result[i]);
hmac_result += buffer;
}

return hmac_result;
}

string OtpTools::hex_to_string(const string& hex_string)
{
string raw_string;

for (size_t i = 0; i < hex_string.length(); i += 2)
{
unsigned char byte = stoi(hex_string.substr(i, 2), nullptr, 16);
raw_string.push_back(byte);
}

return raw_string;
}

string OtpTools::pad_left(const string& input, size_t total_length, char padding_char = '0')
{
if (input.length() >= total_length)
{
return input;
}

return string(total_length - input.length(), padding_char) + input;
}

int OtpTools::get_current_utc_timestamp()
{
time_t now = time(nullptr);
int utc_timestamp = static_cast<int>(now);

return utc_timestamp;
}

// HOTP

Hotp::Hotp(string secret, unsigned int digit, unsigned int counter)
{
// Parsing secret and digit
this->secret = secret;
this->digit = digit;

// Handle counter, this is a 8-bytes counter value
for (int i = 7; i >= 0; --i)
{
this->counter[i] = counter & 0xFF;

counter >>= 8;
}
}

string Hotp::gen_code()
{
// HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

/*
Step 1: Generate an HMAC-SHA-1 value Let HS = HMAC-SHA-1(K,C) // HS
is a 20-byte string

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

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
*/

// Step 1: Generate an HMAC-SHA-1 value
size_t counter_length = sizeof(this->counter);
string hs = OtpTools::generate_hmacsha1(this->secret, this->counter, counter_length);
string hs_raw_string = OtpTools::hex_to_string(hs); // Convert hex into a 20-byte string (int)

// Step 2: Generate a 4-byte string (Dynamic Truncation)
// Step 3: Comput an HOTP value
int dynamic_truncation = this->dynamic_truncation(hs_raw_string);
int code = dynamic_truncation % static_cast<int>(pow(10, this->digit));
string code_str = OtpTools::pad_left(to_string(code), this->digit);

return code_str;
}

int Hotp::dynamic_truncation(string p)
{
int offset = p[p.length() - 1] & 0xF;
int code = ((p[offset] & 0x7F) << 24) |
((p[offset + 1] & 0xFF) << 16) |
((p[offset + 2] & 0xFF) << 8) |
(p[offset + 3] & 0xFF);

return code;
}

bool Hotp::verify_code(int code)
{
string code_str = to_string(code);
string code_result = this->gen_code();

return code_str == code_result;
}

bool Hotp::verify_code(string code)
{
string code_reseult = this->gen_code();

return code == code_reseult;
}

// TOTP
Totp::Totp(string secret, unsigned int time_step, unsigned int digit)
{
this->secret = secret;
this->time_step = time_step;
this->digit = digit;
}

string Totp::gen_code()
{
/*
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)
More specifically, T = (Current Unix time - T0) / X
*/

int utc = OtpTools::get_current_utc_timestamp();
int t = static_cast<int>(floor(utc / this->time_step));
Hotp hotp(this->secret, this->digit, t);
string result = hotp.gen_code();

return result;
}

bool Totp::verify_code(int code)
{
string code_str = to_string(code);
string code_result = this->gen_code();

return code_str == code_result;
}

bool Totp::verify_code(string code)
{
string code_result = this->gen_code();

return code == code_result;
}
}