You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 05:17:07