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.
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).
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.
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.
privateintDynamicTruncation(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#.
publicHOTP(byte[] secret, int counter, int digit) { Secret = secret; Digit = digit; var bytes = newbyte[8];
for (var i = 7; i >= 0; i--) { bytes[i] = (byte)(counter & 0xFF);
counter >>= 8; }
Counter = bytes; }
publicstringCode() { 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; }
privateintDynamicTruncation(byte[] p) { var offset = p.Last() & 0x0F;
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 . . .
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.
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.
publicHOTP(byte[] secret, int counter, int digit) { Secret = secret; Digit = digit; var bytes = newbyte[8];
for (var i = 7; i >= 0; i--) { bytes[i] = (byte)(counter & 0xFF);
counter >>= 8; }
Counter = bytes; }
publicstringCode() { 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; }
privateintDynamicTruncation(byte[] p) { var offset = p.Last() & 0x0F;
publicTOTP(byte[] secret, int timestep, int digit) { Secret = secret; Timestep = timestep; Digit = digit; }
publicstringCode() { 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.
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.
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
publicstringCode() { 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
classProgram { publicstaticvoidMain() { 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.
/* * 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. */
publicstaticbyte[] FromBase32String(string encoded) { if (encoded == null) thrownew 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) returnnewbyte[0];
var outLength = encoded.Length * _shift / 8; var result = newbyte[outLength]; var buffer = 0; var next = 0; var bitsLeft = 0; var charValue = 0; foreach (var c in encoded) { charValue = CharToInt(c); if (charValue < 0) thrownew FormatException("Illegal character: `" + c + "`");
publicstaticstringToBase32String(byte[] data, int offset, int length, bool padOutput = false) { if (data == null) thrownew ArgumentNullException(nameof(data));
if (offset < 0) thrownew ArgumentOutOfRangeException(nameof(offset));
if (length < 0) thrownew ArgumentOutOfRangeException(nameof(length));
if ((offset + length) > data.Length) thrownew 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)) thrownew 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
classProgram { publicstaticvoidMain() { 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.
/* 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);
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();