我尝试根据 RFC 6238 (https://datatracker.ietf.org/doc/html/rfc6238) 使用 VB.NET 构建自己的 2FA 应用程序。
我设法重现了论文附录的结果,但是当我尝试使用真正的 2FA 移动应用程序对其进行测试时,我得到了不同的结果。
我向 ChatGPT 寻求帮助(“生成一个像 google 身份验证器一样计算密钥的代码”),但返回的代码段也没有帮助。我在网上找到的很少的提示表明文本的初始编码可能存在问题。 “Base32”似乎是所需的编码,但我无法理解这到底意味着什么。
适用于 IOS 和 Android 的 FreeOTP 应用程序 (https://github.com/freeotp) 需要不同的输入密码。两个应用程序的文本框中都显示“Base32”,但虽然在 IOS 上无法输入 0、1、8 或 9,但在 Android 上绝对可以。
那么 Base32 在这种情况下意味着什么?密钥必须仅包含 [A-Z2-7],还是算法会对任何字母数字输入执行某些操作?
这是我的代码:
Public Function GenerateTOTP(secretKey As String, stepwidth As Long, stepdeviation As Integer, dt As DateTime, digits As UInteger) As String
Dim unixEpoch As Long = Convert.ToInt64((dt - New DateTime(1970, 1, 1)).TotalSeconds)
Dim steps As Long = Math.Floor(unixEpoch / stepwidth) + stepdeviation
Dim secretKeyBytes As Byte() = Encoding.UTF8.GetBytes(secretKey.ToUpper)
If (BitConverter.IsLittleEndian) Then Array.Reverse(secretKeyBytes) 'For RFC 6238 appendix, this line needs to be disabled
Dim hmac As New HMACSHA1(secretKeyBytes)
Dim stepbytes As Byte() = BitConverter.GetBytes(steps)
If (BitConverter.IsLittleEndian) Then Array.Reverse(stepbytes)
Dim hash As Byte() = hmac.ComputeHash(stepbytes)
Dim offset As Integer = hash(hash.Length - 1) And 15
'Additional check
Dim binaryArray(3) As Byte
Array.Copy(hash, offset, binaryArray, 0, 4)
If (BitConverter.IsLittleEndian) Then Array.Reverse(binaryArray)
Dim binaryBA As UInteger = BitConverter.ToUInt32(binaryArray, 0)
Dim binary As UInteger = ((hash(offset) And 127) << 24) Or ((hash(offset + 1) And 255) << 16) Or ((hash(offset + 2) And 255) << 8) Or (hash(offset + 3) And 255)
Dim totp As UInteger = binary Mod (10 ^ digits)
Dim totpBA As UInteger = binaryBA Mod (10 ^ digits)
Console.WriteLine("-------------------------------------------------------")
Console.WriteLine("Key: " & secretKey)
Console.WriteLine("Date: " & dt.ToString("dd.MM.yyyy HH:mm:ss"))
Console.WriteLine("unixEpoch: " & unixEpoch)
Console.WriteLine("steps: " & steps)
Console.WriteLine("Binary Norm UInt: " & totp.ToString().PadLeft(digits, "0"c))
Console.WriteLine("Binary Norm UInt check:" & totpBA.ToString().PadLeft(digits, "0"c))
Return totp.ToString().PadLeft(digits, "0"c) & " " & totpBA.ToString().PadLeft(digits, "0"c)
End Function
我猜主要问题在于 UTF8 编码,但我不知道应该如何将字符串转换为 Base32 兼容的内容。我尝试了不同的编码,使用大端和小端,更改了位,但没有任何效果。
RFC 6238 包含一个我不理解的类。 SecretKeySpec 的作用是什么?关键代码段没有显示任何使用 Base32 的提示。甚至测试键也包含0、1、8和9。
这是 RFC 6238 的摘要:
private static byte[] hexStr2Bytes(String hex){
// Adding one byte to get the right conversion
// Values starting with "0" can be converted
byte[] bArray = new BigInteger("10" + hex,16).toByteArray(); ' <--------- standard hex from ASCII or UTF8
// Copy all the REAL bytes, not the "first"
byte[] ret = new byte[bArray.length - 1];
for (int i = 0; i < ret.length; i++)
ret[i] = bArray[i+1];
return ret;
}
private static byte[] hmac_sha(String crypto, byte[] keyBytes,
byte[] text){
try {
Mac hmac;
hmac = Mac.getInstance(crypto);
SecretKeySpec macKey = ' <--------- no idea
new SecretKeySpec(keyBytes, "RAW");
hmac.init(macKey);
return hmac.doFinal(text);
} catch (GeneralSecurityException gse) {
throw new UndeclaredThrowableException(gse);
}
}
public static String generateTOTP(String key,
String time,
String returnDigits,
String crypto){
int codeDigits = Integer.decode(returnDigits).intValue();
String result = null;
// Using the counter
// First 8 bytes are for the movingFactor
// Compliant with base RFC 4226 (HOTP)
while (time.length() < 16 )
time = "0" + time;
// Get the HEX in a Byte[]
byte[] msg = hexStr2Bytes(time);
byte[] k = hexStr2Bytes(key);
byte[] hash = hmac_sha(crypto, k, msg); ' <--------- just "normal" bytes, no Base32
// put selected bytes into result int
int offset = hash[hash.length - 1] & 0xf;
int binary =
((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otp = binary % DIGITS_POWER[codeDigits];
result = Integer.toString(otp);
while (result.length() < codeDigits) {
result = "0" + result;
}
return result;
}
奇怪的是,当我使用测试键“12345678901234567890”时,我得到了论文的结果,但是FreeOTP应用程序返回了不同的东西(我改变了手机时间)。在 IOS 上我什至无法输入密钥(如上所述)。我检查的每个 2FA 应用程序都会返回相同的结果并引用 RFC 6238,但该算法对我不起作用。
那么我做错了什么? 我是否必须在代码中包含 Base32-ish 的内容?如果是这样,编码到底应该做什么? SecretKeySpec 的作用是什么? 为什么IOS和Android算法有差异?
与ChatGPT的对话终于给了我答案。这是几个错误的混合体:
我终于能够创建一个工人阶级了:
Public Class TOTP
Private Const Base32Alphabet As String = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
Public Function GenerateSecretKey(length As Byte) As String
Dim rnd As New Random
Dim output As String = ""
For i = 0 To length - 1
output &= Base32Alphabet(rnd.Next(32)).ToString
Next
Return output
End Function
Public Function GenerateTOTP(secretKey As String, stepwidth As Long, stepdeviation As Integer, dt As DateTime, digits As UInteger) As String
Dim unixEpoch As Long = Convert.ToInt64((dt - New DateTime(1970, 1, 1)).TotalSeconds)
Dim steps As Long = Math.Floor(unixEpoch / stepwidth) + stepdeviation
Dim secretKeyBytes As Byte() = Base32StringToByteArray(secretKey.ToUpper)
Dim hmac As New HMACSHA1(secretKeyBytes)
Dim stepbytes As Byte() = BitConverter.GetBytes(steps)
If (BitConverter.IsLittleEndian) Then Array.Reverse(stepbytes)
Dim hash As Byte() = hmac.ComputeHash(stepbytes)
Dim offset As Integer = hash(hash.Length - 1) And 15
Dim binary As Integer = ((hash(offset) And 127) << 24) Or ((hash(offset + 1) And 255) << 16) Or ((hash(offset + 2) And 255) << 8) Or (hash(offset + 3) And 255)
Dim totp As Integer = binary Mod (10 ^ digits)
Return totp.ToString().PadLeft(digits, "0"c)
End Function
Private Function Base32StringToByteArray(base32String As String) As Byte()
Dim bitString As New StringBuilder()
For Each character In base32String
Dim index As Integer = Base32Alphabet.IndexOf(Char.ToUpper(character))
If index < 0 Then
Throw New ArgumentException("Invalid character")
End If
bitString.Append(Convert.ToString(index, 2).PadLeft(5, "0"c))
Next
Dim byteList As New List(Of Byte)()
For i As Integer = 0 To bitString.Length - 1 Step 8
If i + 8 <= bitString.Length Then
byteList.Add(Convert.ToByte(bitString.ToString().Substring(i, 8), 2))
End If
Next
Return byteList.ToArray()
End Function
End Class