摘要:Google Authenticator(谷歌身份验证器),是谷歌公司推出的一款动态令牌工具,解决账户使用时遭到的一些不安全的操作进行的“二次验证”,认证器基于RFC文档中的HOTP/TOTP算法实现 ,是一种从共享秘钥和时间或次数一次性令牌的算法。在工作中可以通过认证器方式对账户有更好的保护,但是在查阅一些资料发现适合我这样的小白文章真的很少,针对于C#的文章就更加少了,本文主要是对C#如何使用Google Authenticator(谷歌身份验证器)进行探讨,有不足之处还请见谅。
Google Authenticator(谷歌身份验证器)
什么是认证器?怎么对接?
Google Authenticator(谷歌身份验证器)是微软推出的一个动态密令工具,它有两种密令模式。分别是“TOTP 基于时间”、“HOTP 基于计数器”,通过手机上 简单的设置就可以设定自己独一的动态密令, 那么我们怎么将我们的程序和认证器进行对接呢?其实谷歌认证器并不是需要我们对接这个工具的API而是通过算法来决定,谷歌使用使用HMAC算法生成密令,通过基于次数或者基于时间两个模板进行计算,因此在程序中只需要使用相同的算法即可与之匹配。
TOTP 基于时间
- HMAC算法使用固定为HmacSHA1
- 更新时长固定为30秒
- APP端输入数据维度只有两个:账户名称(自己随意填写方便自己查看)和base32格式的key
HOTP 基于计数器
基于计数器模式是根据一个共享秘钥K和一个C计数器进行算法计算
认证器安装
手机需要安装认证器:
- Android版:安卓版下载
- IOS版:苹果版下载
效果图
控制台
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
namespace GoogleAuthenticator | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
long duration = 30; | |
string key = "xeon997@foxmail.com"; | |
GoogleAuthenticator authenticator = new GoogleAuthenticator(duration, key); | |
var mobileKey = authenticator.GetMobilePhoneKey(); | |
while (true) | |
{ | |
Console.WriteLine("手机端秘钥为:" + mobileKey); | |
var code = authenticator.GenerateCode(); | |
Console.WriteLine("动态验证码为:" + code); | |
Console.WriteLine("刷新倒计时:" + | |
authenticator.EXPIRE_SECONDS); | |
System.Threading.Thread.Sleep(1000); | |
Console.Clear(); | |
} | |
} | |
} | |
} |
认证器类:
using GoogleAuthorization; | |
using System; | |
using System.Security.Cryptography; | |
using System.Text; | |
namespace GoogleAuthenticator | |
{ | |
public class GoogleAuthenticator | |
{ | |
/// <summary> | |
/// 初始化验证码生成规则 | |
/// </summary> | |
/// <param name="key">秘钥(手机使用Base32码)</param> | |
/// <param name="duration">验证码间隔多久刷新一次(默认30秒和google同步)</param> | |
public GoogleAuthenticator(long duration = 30, string key = "xeon997@foxmail.com") | |
{ | |
this.SERECT_KEY = key; | |
this.SERECT_KEY_MOBILE = 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; | |
} | |
} | |
/// <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)); | |
} | |
} | |
} |
Base32转换类:
using System; | |
namespace GoogleAuthorization | |
{ | |
public static class Base32 | |
{ | |
public static byte[] ToBytes(string input) | |
{ | |
if (string.IsNullOrEmpty(input)) | |
{ | |
throw new ArgumentNullException("input"); | |
} | |
input = input.TrimEnd('='); | |
int byteCount = input.Length * 5 / 8; | |
byte[] returnArray = new byte[byteCount]; | |
byte curByte = 0, bitsRemaining = 8; | |
int mask = 0, arrayIndex = 0; | |
foreach (char c in input) | |
{ | |
int cValue = CharToValue(c); | |
if (bitsRemaining > 5) | |
{ | |
mask = cValue << (bitsRemaining - 5); | |
curByte = (byte)(curByte | mask); | |
bitsRemaining -= 5; | |
} | |
else | |
{ | |
mask = cValue >> (5 - bitsRemaining); | |
curByte = (byte)(curByte | mask); | |
returnArray[arrayIndex++] = curByte; | |
curByte = (byte)(cValue << (3 + bitsRemaining)); | |
bitsRemaining += 3; | |
} | |
} | |
if (arrayIndex != byteCount) | |
{ | |
returnArray[arrayIndex] = curByte; | |
} | |
return returnArray; | |
} | |
public static string ToString(byte[] input) | |
{ | |
if (input == null || input.Length == 0) | |
{ | |
throw new ArgumentNullException("input"); | |
} | |
int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; | |
char[] returnArray = new char[charCount]; | |
byte nextChar = 0, bitsRemaining = 5; | |
int arrayIndex = 0; | |
foreach (byte b in input) | |
{ | |
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); | |
returnArray[arrayIndex++] = ValueToChar(nextChar); | |
if (bitsRemaining < 4) | |
{ | |
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); | |
returnArray[arrayIndex++] = ValueToChar(nextChar); | |
bitsRemaining += 5; | |
} | |
bitsRemaining -= 3; | |
nextChar = (byte)((b << bitsRemaining) & 31); | |
} | |
if (arrayIndex != charCount) | |
{ | |
returnArray[arrayIndex++] = ValueToChar(nextChar); | |
while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; | |
} | |
return new string(returnArray); | |
} | |
private static int CharToValue(char c) | |
{ | |
var value = (int)c; | |
if (value < 91 && value > 64) | |
{ | |
return value - 65; | |
} | |
if (value < 56 && value > 49) | |
{ | |
return value - 24; | |
} | |
if (value < 123 && value > 96) | |
{ | |
return value - 97; | |
} | |
throw new ArgumentException("Character is not a Base32 character.", "c"); | |
} | |
private static char ValueToChar(byte b) | |
{ | |
if (b < 26) | |
{ | |
return (char)(b + 65); | |
} | |
if (b < 32) | |
{ | |
return (char)(b + 24); | |
} | |
throw new ArgumentException("Byte is not a value Base32 value.", "b"); | |
} | |
} | |
} |
总结
需要注意的坑
移动端下载的认证器的秘钥key是通过base32转码得到的,而程序端是直接输入源码。
如原秘钥为xeon997@foxmail.com生成的base32码
PBSW63RZHE3UAZTPPBWWC2LMFZRW63I=才是移动端需要输入的秘钥。
在网上找了很多资料没有发现关于C#的案例,所以在此记录一下自己遇到的坑,让更多的人能够跳过这个坑
原文地址:
https://www.cnblogs.com/easyauthor/p/11054869.html