java和c#里的TOTP統一算法
基礎說明
本文根據 RFC4226 和 RFC6238 文檔,詳細的介紹 HOTP 和 TOTP 算法的原理和實現。
兩步驗證已經被廣泛應用于各種互聯網應用當中,用來提供安全性。對于如何使用兩步驗證,大家并不陌生,無非是開啟兩步驗證,然后出現一個二維碼,使用支持兩步驗證的移動應用比如 Google Authenticator 或者 LassPass Authenticator 掃一下二維碼。這時候應用會出現一個6位數的一次性密碼,首次需要輸入驗證從而完成開啟過程。以后在登陸的時候,除了輸入用戶名和密碼外,還需要把當前的移動應用上顯示的6位數編碼輸入才能完成登陸。
這個過程的背后主要由兩個算法來支撐:HOTP 和 TOTP。也分別對應著兩份 RFC 協議 RFC4266 和 RFC6238。前者是 HOTP 的標準,后者是 TOTP 的標準。本文將使用圖文并茂的方式詳細介紹 HOTP 和 TOTP 的算法原理,并在最后分析其安全性。當然所有內容都是基于協議的,通過自己的理解更加直觀的表達出來。
為了確保在不同語言下生成相同的 TOTP 結果,你需要確保使用相同的密鑰和相同的時間戳步長。同時,你還需要確保使用相同的哈希算法(通常是 HMAC-SHA1 或 HMAC-SHA256)。
參數解釋
- base32Key: 這是生成totp數字時的共享密鑰
- timeStep: 這是時間步長,作用是每隔多長時間(秒),你的totp數字變化一次,即一個totp數字的失效時間
- digits: 這是生成多少位的totp數字
- HMAC-SHA1: 是一種基于哈希函數的消息認證碼算法,用于保護數據完整性和身份驗證
HMAC-SHA1
HMAC-SHA1(Hash-based Message Authentication Code with SHA-1)是一種基于哈希函數的消息認證碼算法,用于保護數據完整性和身份驗證。它結合了兩個主要的技術:哈希函數(SHA-1)和密鑰(Key)。
下面是 HMAC-SHA1 算法的工作原理和說明:
-
輸入數據:HMAC-SHA1 接受兩個輸入:消息數據(Message)和密鑰(Key)。消息數據可以是任意長度的二進制數據。
-
密鑰填充:如果密鑰的長度小于哈希函數的塊大小,HMAC-SHA1 會將密鑰填充到相應的塊大小,通常使用 0x00 字節。
-
內部填充:將密鑰與常數
0x36做異或操作,然后將結果與消息數據連接起來。 -
哈希計算:對連接后的數據進行 SHA-1 哈希計算。SHA-1 生成一個固定長度(160位或20字節)的哈希值。
-
外部填充:將密鑰與常數
0x5C做異或操作,然后將結果與內部哈希值連接起來。 -
二次哈希計算:對連接后的數據進行 SHA-1 哈希計算。這一次的哈希計算包括了內部哈希值和密鑰。
-
結果:HMAC-SHA1 的最終結果是SHA-1 哈希的輸出。
HMAC-SHA1 的主要目的是確保數據的完整性和身份驗證。由于它需要密鑰,因此只有知道密鑰的實體才能生成正確的 HMAC 值。這使得 HMAC-SHA1 在加密通信和身份驗證中非常有用,例如在數字簽名、認證協議(如OAuth)、以及一次性密碼算法(如TOTP和HOTP)中廣泛使用。
需要注意的是,SHA-1 已經不再被視為安全的哈希算法,因為它存在碰撞漏洞。因此,安全敏感的應用程序應該使用更強大的哈希算法,如SHA-256或SHA-3,來代替 SHA-1。如果可能,也應該使用更安全的 HMAC 變種,如HMAC-SHA-256。
TOTP的生成過程
- 服務器和客戶端都知道共享的密鑰。
- 客戶端獲取當前時間戳,通常是以秒為單位。
- 客戶端將當前時間戳除以時間步長并取整,以獲得一個時間窗口(Time Window)的序號。
- 客戶端使用哈希函數(如HMAC-SHA1)將密鑰和時間窗口的序號作為輸入來生成哈希值。
- 從哈希值中提取指定的位數作為一次性密碼,通常是6位數字。
- 客戶端將生成的一次性密碼顯示給用戶,用戶輸入該密碼進行身份驗證。
- 服務器使用相同的密鑰和時間戳計算一次性密碼,以驗證用戶輸入的密碼是否匹配。
TOTP的關鍵之處在于,只有在相同的時間窗口內才能生成相同的密碼,因此它能夠提供一定的安全性,即使密鑰泄露,攻擊者也只有在短時間內才能使用密碼。另外,TOTP密碼的生成依賴于共享密鑰和時間,因此需要客戶端和服務器之間的時間同步。
核心代碼
以下是一個 Java 和 C# 中可以生成相同 TOTP 結果的示例代碼,使用的是 HMAC-SHA1 哈希算法和 Joda-Time 庫來處理時間:
Java 示例:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
public class TOTPGenerator {
public static String generateTOTP(String base32Key, int timeStep, int digits) throws Exception {
long counter = (System.currentTimeMillis() / 1000) / timeStep;
byte[] key = new Base32().decode(base32Key);
SecretKeySpec secretKey = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(secretKey);
byte[] counterBytes = new byte[8];
for (int i = 0; i < 8; i++) {
counterBytes[7 - i] = (byte) (counter >> (8 * i));
}
byte[] hash = mac.doFinal(counterBytes);
int offset = hash[hash.length - 1] & 0x0F;
int binary = ((hash[offset] & 0x7F) << 24 | (hash[offset + 1] & 0xFF) << 16 | (hash[offset + 2] & 0xFF) << 8 | (hash[offset + 3] & 0xFF));
int otp = binary % (int) Math.pow(10, digits);
return String.format("%0" + digits + "d", otp);
}
}
C# 示例:
using System;
using System.Security.Cryptography;
using System.Text;
public class TOTPGenerator
{
public static string GenerateTOTP(string base32Key, int timeStep, int digits)
{
long counter = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds / timeStep;
byte[] key = Base32Decode(base32Key);
using (HMACSHA1 hmac = new HMACSHA1(key))
{
byte[] counterBytes = BitConverter.GetBytes(counter);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(counterBytes);
}
byte[] hash = hmac.ComputeHash(counterBytes);
int offset = hash[hash.Length - 1] & 0x0F;
int binary = (hash[offset] & 0x7F) << 24 | (hash[offset + 1] & 0xFF) << 16 | (hash[offset + 2] & 0xFF) << 8 | (hash[offset + 3] & 0xFF);
int otp = binary % (int)Math.Pow(10, digits);
return otp.ToString($"D{digits}");
}
}
private static byte[] Base32Decode(string base32)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var bits = base32.ToUpper().ToCharArray().Select(c => Convert.ToString(chars.IndexOf(c), 2).PadLeft(5, '0')).Aggregate((a, b) => a + b);
return Enumerable.Range(0, bits.Length / 8).Select(i => Convert.ToByte(bits.Substring(i * 8, 8), 2)).ToArray();
}
}
這兩個示例使用了相同的密鑰(base32Key)、時間戳步長(timeStep)和位數(digits),并使用相同的 HMAC-SHA1 哈希算法來生成 TOTP。確保在實際應用中提供相同的參數值,你將能夠生成相同的 TOTP 結果。
測試代碼
// C#
string totp = GenerateTOTP("pkulaw", 30, 8);
Console.WriteLine("Current TOTP:" + totp);
// 結果:30396996
// java
String totp=generateTOTP("pkulaw", 30, 8);
System.out.println(totp);
// 結果:30396996
上面的兩種語言的測試代碼,在30秒之內(一般使用UTC時間計算), 產生的totp碼是相同的。
浙公網安備 33010602011771號