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計數器進行演算法計算
認證器安裝
手機需要安裝認證器:
效果圖
容
案例
控制台
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 = "[email protected]"; 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 }View Code
認證器類
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 = "[email protected]") 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 }View Code
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 }View Code
總結
需要註意的坑
移動端下載的認證器的秘鑰key是通過base32轉碼得到的,而程式端是直接輸入源碼。
如原秘鑰為[email protected]生成的base32碼PBSW63RZHE3UAZTPPBWWC2LMFZRW63I=才是移動端需要輸入的秘鑰。
在網上找了很多資料沒有發現關於C#的案例,所以在此記錄一下自己遇到的坑,讓更多的人能夠跳過這個坑。
案例地址:
git:https://github.com/CN-Yi/GoogleAuthenticator
gitee:https://gitee.com/hsyi/GoogleAuthenticator
在此感謝提供學習資料的大神們,如果有錯誤的地方歡迎留言。