C#实现RFC 6238(TOTP)持续生成错误验证码求助
TOTP实现与第三方验证工具不匹配的问题排查与修复
问题背景
参考RFC 4226(HOTP)和RFC 6238(TOTP)标准实现相关功能,使用测试密钥12345678901234567890时能通过标准测试用例,但使用在线测试工具及Google Authenticator验证时,生成的TOTP验证码不符合预期。以下是原实现代码:
using System.Security.Cryptography; using System.Text; namespace ConsoleTotp; 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 hsString = BytesConvertToString(hs); var code = (Dt(hsString) % (int)Math.Pow(10, Digit)).ToString().PadLeft(Digit, '0'); return code; } private int Dt(string 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); } private string BytesConvertToString(byte[] data) { var charaters = data.Select(x => (char)x).ToArray(); return new string(charaters); } } 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 testTime = new DateTime(2603, 10, 11, 11, 33, 20); var t = (int) (Math.Floor(DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds / Timestep)); var hotp = new HOTP(Secret, t, Digit); var code = hotp.Code(); return code; } } class Program { public static void Main() { var secret = Encoding.ASCII.GetBytes("I65VU7K5ZQL7WB4E"); var totp = new TOTP(secret, 30, 6); var code = totp.Code(); Console.WriteLine(code); // Wrong result } }
核心问题与修复方案
1. 密钥编码错误(最关键)
Google Authenticator等工具要求密钥为Base32编码格式,原代码直接将字符串通过Encoding.ASCII.GetBytes()转换为字节数组,完全不符合Base32解码规则,必须实现Base32解码逻辑将密钥转换为正确的字节数组。
2. HOTP中错误的字节处理逻辑
原代码将HMAC计算后的字节数组转换为字符串再操作,会导致不可打印字节的丢失或错误转换,正确做法是直接操作字节数组,无需转换为字符串。
3. 计数器类型溢出风险
原代码用int存储时间计数器,int最大值对应2038年1月19日的时间戳,未来会出现溢出问题,应改用long类型。
4. 资源未正确释放
原代码中HMACSHA1实例未用using语句包裹,会导致资源泄漏,需修正。
修正后的完整代码
using System.Security.Cryptography; using System.Text; namespace ConsoleTotp; class HOTP { private readonly byte[] _secret; private readonly byte[] _counter; private readonly int _digit; public HOTP(byte[] secret, long counter, int digit) { _secret = secret; _digit = digit; _counter = new byte[8]; // 将long类型计数器转换为大端字节数组 for (var i = 7; i >= 0; i--) { _counter[i] = (byte)(counter & 0xFF); counter >>= 8; } } public string GenerateCode() { using var hmac = new HMACSHA1(_secret); var hs = hmac.ComputeHash(_counter); var code = (Truncate(hs) % (int)Math.Pow(10, _digit)).ToString().PadLeft(_digit, '0'); return code; } private int Truncate(byte[] hs) { var offset = hs[hs.Length - 1] & 0x0F; return ((hs[offset] & 0x7F) << 24) | ((hs[offset + 1] & 0xFF) << 16) | ((hs[offset + 2] & 0xFF) << 8) | (hs[offset + 3] & 0xFF); } } public class TOTP { private readonly byte[] _secret; private readonly int _timestep = 30; private readonly 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 GenerateCode() { // 用UTC时间计算Unix时间戳,避免时区差异 var unixTime = (long)Math.Floor(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds); var counter = unixTime / _timestep; var hotp = new HOTP(_secret, counter, _digit); return hotp.GenerateCode(); } // Base32解码方法,适配Google Authenticator的密钥格式 public static byte[] Base32Decode(string base32String) { const string base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; base32String = base32String.TrimEnd('=').ToUpperInvariant(); var bits = new StringBuilder(); foreach (var c in base32String) { var index = base32Chars.IndexOf(c); if (index == -1) throw new ArgumentException("无效的Base32字符", nameof(base32String)); bits.Append(Convert.ToString(index, 2).PadLeft(5, '0')); } var byteCount = bits.Length / 8; var result = new byte[byteCount]; for (var i = 0; i < byteCount; i++) { var byteString = bits.ToString(i * 8, 8); result[i] = Convert.ToByte(byteString, 2); } return result; } } class Program { public static void Main() { // 使用Base32解码密钥,而非ASCII转换 var secretBase32 = "I65VU7K5ZQL7WB4E"; var secretBytes = TOTP.Base32Decode(secretBase32); var totp = new TOTP(secretBytes, 30, 6); var code = totp.GenerateCode(); Console.WriteLine(code); // 现在应与第三方工具结果一致 } }
关键修改说明
- Base32解码:新增
Base32Decode方法,将Google Authenticator使用的Base32密钥转换为正确的字节数组。 - 计数器类型:将HOTP构造函数的计数器参数改为
long,避免2038年溢出问题。 - 字节直接操作:移除
BytesConvertToString方法,Truncate方法直接处理字节数组,避免字符转换错误。 - 资源释放:用
using语句包裹HMACSHA1实例,确保资源正确释放。 - UTC时间:改用
DateTime.UtcNow计算Unix时间戳,避免本地时区差异导致的时间步数错误。
内容的提问来源于stack exchange,提问作者Jimmy
相关产品推荐
相关产品推荐

