java和c#里的TOTP統一算法
基礎說明
本文根據 RFC4226 和 RFC6238 文檔,詳細的介紹 HOTP 和 TOTP 算法的原理和實現。
兩步驗證已經被廣泛應用于各種互聯網應用當中,用來提供安全性。對于如何使用兩步驗證,大家并不陌生,無非是開啟兩步驗證,然后出現一個二維碼,使用支持兩步驗證的移動應用比如 Google Authenticator 或者 LassPass Authenticator 掃一下二維碼。這時候應用會出現一個6位數的一次性密碼,首次需要輸入驗證從而完成開啟過程。以后在登陸的時候,除了輸入用戶名和密碼外,還需要把當前的移動應用上顯示的6位數編碼輸入才能完成登陸。
這個(ge)過程(cheng)的(de)(de)(de)背后主要由兩個(ge)算法(fa)來支撐:HOTP 和(he) TOTP。也分別對應著兩份(fen) RFC 協議(yi)(yi) RFC4266 和(he) RFC6238。前者(zhe)是 HOTP 的(de)(de)(de)標準(zhun),后者(zhe)是 TOTP 的(de)(de)(de)標準(zhun)。本文(wen)將使用圖(tu)文(wen)并(bing)茂的(de)(de)(de)方式詳細介紹 HOTP 和(he) TOTP 的(de)(de)(de)算法(fa)原理,并(bing)在最后分析其安全性。當然所有內(nei)容都是基(ji)于(yu)協議(yi)(yi)的(de)(de)(de),通過自己的(de)(de)(de)理解更加直觀的(de)(de)(de)表達出(chu)來。
為(wei)了確(que)保(bao)在不同語言下生成相同的(de) TOTP 結果(guo),你(ni)需(xu)要確(que)保(bao)使用(yong)相同的(de)密鑰和相同的(de)時間戳步長。同時,你(ni)還需(xu)要確(que)保(bao)使用(yong)相同的(de)哈希算法(fa)(通常(chang)是 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)是一種基于哈希(xi)(xi)函數(shu)(shu)的消息(xi)認證碼算法,用于保護(hu)數(shu)(shu)據(ju)完整性和身份驗證。它結合了兩(liang)個主要的技術:哈希(xi)(xi)函數(shu)(shu)(SHA-1)和密鑰(Key)。
下面是 HMAC-SHA1 算法的工作(zuo)原理和說明:
-
輸入數據:HMAC-SHA1 接受兩個(ge)輸入:消息數(shu)據(Message)和(he)密鑰(Key)。消息數(shu)據可(ke)以是(shi)任意長度的(de)二進(jin)制(zhi)數(shu)據。
-
密鑰填充:如(ru)果密鑰的長(chang)度小(xiao)(xiao)于哈希函(han)數(shu)的塊大小(xiao)(xiao),HMAC-SHA1 會將密鑰填充到相應的塊大小(xiao)(xiao),通常使用 0x00 字節(jie)。
-
內部填充:將密鑰與常數
0x36做異或(huo)操作,然后將結果(guo)與消息數據(ju)連接起(qi)來。 -
哈希計算:對(dui)連接(jie)后的數據進行 SHA-1 哈希計算(suan)。SHA-1 生成一個(ge)固定長(chang)度(160位或20字節)的哈希值。
-
外部填充:將密鑰與常數
0x5C做(zuo)異或操作,然后將結(jie)果與內部哈(ha)希值連接起來。 -
二次哈希計算:對連接(jie)后(hou)的(de)數據進行 SHA-1 哈(ha)希計算。這一(yi)次的(de)哈(ha)希計算包括了內部(bu)哈(ha)希值(zhi)和密鑰(yao)。
-
結果:HMAC-SHA1 的(de)最終結果是SHA-1 哈希的(de)輸出。
HMAC-SHA1 的主要(yao)目的是確(que)保數據的完整性(xing)和(he)(he)身(shen)份(fen)驗證(zheng)。由于它需要(yao)密(mi)鑰(yao),因此只(zhi)有知道密(mi)鑰(yao)的實體才(cai)能生成正確(que)的 HMAC 值。這使得 HMAC-SHA1 在加密(mi)通(tong)信和(he)(he)身(shen)份(fen)驗證(zheng)中非(fei)常有用(yong),例如在數字簽(qian)名、認證(zheng)協議(如OAuth)、以及一次性(xing)密(mi)碼(ma)算(suan)法(fa)(如TOTP和(he)(he)HOTP)中廣泛使用(yong)。
需要注(zhu)意的(de)(de)是,SHA-1 已經不再(zai)被視(shi)為安(an)全的(de)(de)哈(ha)希算(suan)法,因為它存在碰(peng)撞漏洞(dong)。因此,安(an)全敏感的(de)(de)應(ying)用程序應(ying)該使用更強(qiang)大的(de)(de)哈(ha)希算(suan)法,如(ru)SHA-256或(huo)SHA-3,來代替 SHA-1。如(ru)果可能,也(ye)應(ying)該使用更安(an)全的(de)(de) HMAC 變種,如(ru)HMAC-SHA-256。
TOTP的生成過程
- 服務器和客戶端都知道共享的密鑰。
- 客戶端獲取當前時間戳,通常是以秒為單位。
- 客戶端將當前時間戳除以時間步長并取整,以獲得一個時間窗口(Time Window)的序號。
- 客戶端使用哈希函數(如HMAC-SHA1)將密鑰和時間窗口的序號作為輸入來生成哈希值。
- 從哈希值中提取指定的位數作為一次性密碼,通常是6位數字。
- 客戶端將生成的一次性密碼顯示給用戶,用戶輸入該密碼進行身份驗證。
- 服務器使用相同的密鑰和時間戳計算一次性密碼,以驗證用戶輸入的密碼是否匹配。
TOTP的(de)(de)關(guan)鍵之處在(zai)于,只(zhi)有在(zai)相同的(de)(de)時(shi)間窗口內才能生成相同的(de)(de)密碼(ma)(ma),因(yin)此(ci)(ci)它能夠提供一定的(de)(de)安全性,即使密鑰泄露,攻擊者(zhe)也只(zhi)有在(zai)短時(shi)間內才能使用密碼(ma)(ma)。另外(wai),TOTP密碼(ma)(ma)的(de)(de)生成依賴于共享密鑰和時(shi)間,因(yin)此(ci)(ci)需要客戶端(duan)和服務器之間的(de)(de)時(shi)間同步。
核心代碼
以下是一個 Java 和(he) C# 中可(ke)以生成相同 TOTP 結果(guo)的示(shi)例代(dai)碼,使(shi)用的是 HMAC-SHA1 哈希算法和(he) Joda-Time 庫來處(chu)理時間(jian):
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),并使用相(xiang)(xiang)同的(de) HMAC-SHA1 哈希算法來生成 TOTP。確保在實際應(ying)用中提(ti)供(gong)相(xiang)(xiang)同的(de)參數值,你將(jiang)能夠(gou)生成相(xiang)(xiang)同的(de) 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
上面的兩種語言的測試代(dai)碼,在30秒之(zhi)內(一般使用UTC時間(jian)計算), 產生的totp碼是相同的。