背景:
需要实现类似于QQ令牌这种动态数据验证;
TOTP 介绍:
TOTP 是基于时间的一次性密码生成算法,它由 RFC 6238 定义。和基于事件的一次性密码生成算法不同 HOTP,TOTP 是基于时间的,并且TOTP 算法是基于 HOTP 的,对于 HOTP 算法来说,HOTP 的输入一致时始终输出相同的值,而 TOTP 是基于时间来算出来的一个值,可以在一段时间内(官方推荐是30s)保证这个值是固定以实现,在一段时间内始终是同一个值,以此来达到基于时间的一次性密码生成算法。
实现:
实现是使用 谷歌身份验证GoogleAuthenticator来实现的,Google Authenticator(谷歌身份验证器),是谷歌公司推出的一款动态令牌工具,解决账户使用时遭到的一些不安全的操作进行的“二次验证”,认证器基于RFC文档中的HOTP/TOTP算法实现 ,是一种从共享秘钥和时间或次数一次性令牌的算法。
示例代码:
1.调用
GoogleAuthenticator ga = new GoogleAuthenticator();
string mm = ga.GenerateCode();
2.创建一个类 GoogleAuthenticator.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace WpfApp2
{
public class GoogleAuthenticator
{
/// <summary>
/// 初始化验证码生成规则
/// </summary>
/// <param name="key">秘钥(手机使用Base32码,程序端不需要Base32转换)</param>
/// <param name="duration">验证码间隔多久刷新一次(默认30秒和google同步)</param>
public GoogleAuthenticator(long duration = 60, string key = "123456")
{
this.SERECT_KEY = key;
this.SERECT_KEY_MOBILE = key; //Base32.ToString(Encoding.UTF8.GetBytes(key));
this.DURATION_TIME = duration;
}
/// <summary>
/// 间隔时间
/// </summary>
private long DURATION_TIME { get; set; }
/// <summary>
/// 迭代次数
/// </summary>
private long COUNTER
{
get
{
//return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds / DURATION_TIME;
DateTime nowdt = DateTime.Now; //Convert.ToDateTime("2023/4/23 12:12:12");
return (long)(nowdt - new DateTime(1970, 1, 1, 8, 0, 0, 0)).TotalSeconds / DURATION_TIME;
}
}
/// <summary>
/// 秘钥
/// </summary>
private string SERECT_KEY { get; set; }
/// <summary>
/// 手机端输入的秘钥
/// </summary>
private string SERECT_KEY_MOBILE { get; set; }
/// <summary>
/// 到期秒数
/// </summary>
public long EXPIRE_SECONDS
{
get
{
return (DURATION_TIME - (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds % DURATION_TIME);
}
}
/// <summary>
/// 获取手机端秘钥
/// </summary>
/// <returns></returns>
public string GetMobilePhoneKey()
{
if (SERECT_KEY_MOBILE == null)
throw new ArgumentNullException("SERECT_KEY_MOBILE");
return SERECT_KEY_MOBILE;
}
/// <summary>
/// 生成认证码
/// </summary>
/// <returns>返回验证码</returns>
public string GenerateCode()
{
return GenerateHashedCode(SERECT_KEY, COUNTER);
}
/// <summary>
/// 按照次数生成哈希编码
/// </summary>
/// <param name="secret">秘钥</param>
/// <param name="iterationNumber">迭代次数</param>
/// <param name="digits">生成位数</param>
/// <returns>返回验证码</returns>
private string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
{
byte[] counter = BitConverter.GetBytes(iterationNumber);
if (BitConverter.IsLittleEndian)
Array.Reverse(counter);
byte[] key = Encoding.ASCII.GetBytes(secret);
HMACSHA1 hmac = new HMACSHA1(key, true);
byte[] hash = hmac.ComputeHash(counter);
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 password = binary % (int)Math.Pow(10, digits); // 6 digits
return password.ToString(new string('0', digits));
}
}
}