Google Authenticator(谷歌身份验证器)C#版
程序员文章站
2022-12-30 07:53:58
Google Authenticator(谷歌身份验证器)在C#中的封装使用 ......
摘要: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版:
效果图
容
案例
控制台
1 using system; 2 using system.collections.generic; 3 using system.linq; 4 using system.text; 5 6 namespace googleauthenticator 7 { 8 class program 9 { 10 static void main(string[] args) 11 { 12 long duration = 30; 13 string key = "xeon997@foxmail.com"; 14 googleauthenticator authenticator = new googleauthenticator(duration, key); 15 var mobilekey = authenticator.getmobilephonekey(); 16 17 while (true) 18 { 19 20 console.writeline("手机端秘钥为:" + mobilekey); 21 22 var code = authenticator.generatecode(); 23 console.writeline("动态验证码为:" + code); 24 25 console.writeline("刷新倒计时:" + authenticator.expire_seconds); 26 27 system.threading.thread.sleep(1000); 28 console.clear(); 29 } 30 } 31 } 32 }
认证器类
1 using googleauthorization; 2 using system; 3 using system.security.cryptography; 4 using system.text; 5 namespace googleauthenticator 6 { 7 public class googleauthenticator 8 { 9 /// <summary> 10 /// 初始化验证码生成规则 11 /// </summary> 12 /// <param name="key">秘钥(手机使用base32码)</param> 13 /// <param name="duration">验证码间隔多久刷新一次(默认30秒和google同步)</param> 14 public googleauthenticator(long duration = 30, string key = "xeon997@foxmail.com") 15 { 16 this.serect_key = key; 17 this.serect_key_mobile = base32.tostring(encoding.utf8.getbytes(key)); 18 this.duration_time = duration; 19 } 20 21 /// <summary> 22 /// 间隔时间 23 /// </summary> 24 private long duration_time { get; set; } 25 26 /// <summary> 27 /// 迭代次数 28 /// </summary> 29 private long counter 30 { 31 get 32 { 33 return (long)(datetime.utcnow - new datetime(1970, 1, 1, 0, 0, 0, datetimekind.utc)).totalseconds / duration_time; 34 } 35 } 36 37 /// <summary> 38 /// 秘钥 39 /// </summary> 40 private string serect_key { get; set; } 41 42 /// <summary> 43 /// 手机端输入的秘钥 44 /// </summary> 45 private string serect_key_mobile { get; set; } 46 47 /// <summary> 48 /// 到期秒数 49 /// </summary> 50 public long expire_seconds 51 { 52 get 53 { 54 return (duration_time - (long)(datetime.utcnow - new datetime(1970, 1, 1, 0, 0, 0, datetimekind.utc)).totalseconds % duration_time); 55 } 56 } 57 58 /// <summary> 59 /// 获取手机端秘钥 60 /// </summary> 61 /// <returns></returns> 62 public string getmobilephonekey() 63 { 64 if (serect_key_mobile == null) 65 throw new argumentnullexception("serect_key_mobile"); 66 return serect_key_mobile; 67 } 68 69 /// <summary> 70 /// 生成认证码 71 /// </summary> 72 /// <returns>返回验证码</returns> 73 public string generatecode() 74 { 75 return generatehashedcode(serect_key, counter); 76 } 77 78 /// <summary> 79 /// 按照次数生成哈希编码 80 /// </summary> 81 /// <param name="secret">秘钥</param> 82 /// <param name="iterationnumber">迭代次数</param> 83 /// <param name="digits">生成位数</param> 84 /// <returns>返回验证码</returns> 85 private string generatehashedcode(string secret, long iterationnumber, int digits = 6) 86 { 87 byte[] counter = bitconverter.getbytes(iterationnumber); 88 89 if (bitconverter.islittleendian) 90 array.reverse(counter); 91 92 byte[] key = encoding.ascii.getbytes(secret); 93 94 hmacsha1 hmac = new hmacsha1(key, true); 95 96 byte[] hash = hmac.computehash(counter); 97 98 int offset = hash[hash.length - 1] & 0xf; 99 100 int binary = 101 ((hash[offset] & 0x7f) << 24) 102 | ((hash[offset + 1] & 0xff) << 16) 103 | ((hash[offset + 2] & 0xff) << 8) 104 | (hash[offset + 3] & 0xff); 105 106 int password = binary % (int)math.pow(10, digits); // 6 digits 107 108 return password.tostring(new string('0', digits)); 109 } 110 } 111 }
base32转码类
1 using system; 2 namespace googleauthorization 3 { 4 public static class base32 5 { 6 public static byte[] tobytes(string input) 7 { 8 if (string.isnullorempty(input)) 9 { 10 throw new argumentnullexception("input"); 11 } 12 13 input = input.trimend('='); 14 int bytecount = input.length * 5 / 8; 15 byte[] returnarray = new byte[bytecount]; 16 17 byte curbyte = 0, bitsremaining = 8; 18 int mask = 0, arrayindex = 0; 19 20 foreach (char c in input) 21 { 22 int cvalue = chartovalue(c); 23 24 if (bitsremaining > 5) 25 { 26 mask = cvalue << (bitsremaining - 5); 27 curbyte = (byte)(curbyte | mask); 28 bitsremaining -= 5; 29 } 30 else 31 { 32 mask = cvalue >> (5 - bitsremaining); 33 curbyte = (byte)(curbyte | mask); 34 returnarray[arrayindex++] = curbyte; 35 curbyte = (byte)(cvalue << (3 + bitsremaining)); 36 bitsremaining += 3; 37 } 38 } 39 40 if (arrayindex != bytecount) 41 { 42 returnarray[arrayindex] = curbyte; 43 } 44 45 return returnarray; 46 } 47 48 public static string tostring(byte[] input) 49 { 50 if (input == null || input.length == 0) 51 { 52 throw new argumentnullexception("input"); 53 } 54 55 int charcount = (int)math.ceiling(input.length / 5d) * 8; 56 char[] returnarray = new char[charcount]; 57 58 byte nextchar = 0, bitsremaining = 5; 59 int arrayindex = 0; 60 61 foreach (byte b in input) 62 { 63 nextchar = (byte)(nextchar | (b >> (8 - bitsremaining))); 64 returnarray[arrayindex++] = valuetochar(nextchar); 65 66 if (bitsremaining < 4) 67 { 68 nextchar = (byte)((b >> (3 - bitsremaining)) & 31); 69 returnarray[arrayindex++] = valuetochar(nextchar); 70 bitsremaining += 5; 71 } 72 73 bitsremaining -= 3; 74 nextchar = (byte)((b << bitsremaining) & 31); 75 } 76 77 if (arrayindex != charcount) 78 { 79 returnarray[arrayindex++] = valuetochar(nextchar); 80 while (arrayindex != charcount) returnarray[arrayindex++] = '='; 81 } 82 83 return new string(returnarray); 84 } 85 86 private static int chartovalue(char c) 87 { 88 var value = (int)c; 89 90 if (value < 91 && value > 64) 91 { 92 return value - 65; 93 } 94 if (value < 56 && value > 49) 95 { 96 return value - 24; 97 } 98 if (value < 123 && value > 96) 99 { 100 return value - 97; 101 } 102 103 throw new argumentexception("character is not a base32 character.", "c"); 104 } 105 106 private static char valuetochar(byte b) 107 { 108 if (b < 26) 109 { 110 return (char)(b + 65); 111 } 112 113 if (b < 32) 114 { 115 return (char)(b + 24); 116 } 117 118 throw new argumentexception("byte is not a value base32 value.", "b"); 119 } 120 } 121 }
总结
需要注意的坑
移动端下载的认证器的秘钥key是通过base32转码得到的,而程序端是直接输入源码。
如原秘钥为xeon997@foxmail.com生成的base32码pbsw63rzhe3uaztppbwwc2lmfzrw63i=才是移动端需要输入的秘钥。
在网上找了很多资料没有发现关于c#的案例,所以在此记录一下自己遇到的坑,让更多的人能够跳过这个坑。
案例地址:
git:https://github.com/cn-yi/googleauthenticator
gitee:https://gitee.com/hsyi/googleauthenticator
在此感谢提供学习资料的大神们,如果有错误的地方欢迎留言。
上一篇: 网红营销流量越来越贵,怎么玩转推广?
下一篇: 直播技术简单介绍(非原创)