c#使用谷歌身份验证GoogleAuthenticator
此功能相当于给系统加了个令牌,只有输入对的一组数字才可以验证成功。类似于QQ令牌一样。
一丶创建最核心的一个类GoogleAuthenticator
此类包含了生成密钥,验证,将绑定密钥转为二维码。
1 public class GoogleAuthenticator 2 { 3 private readonly static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 4 private TimeSpan DefaultClockDriftTolerance { get; set; } 5 6 public GoogleAuthenticator() 7 { 8 DefaultClockDriftTolerance = TimeSpan.FromMinutes(5); 9 } 10 11 /// <summary> 12 /// Generate a setup code for a Google Authenticator user to scan 13 /// </summary> 14 /// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param> 15 /// <param name="accountTitleNoSpaces">Account Title (no spaces)</param> 16 /// <param name="accountSecretKey">Account Secret Key</param> 17 /// <param name="QRPixelsPerModule">Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode)</param> 18 /// <returns>SetupCode object</returns> 19 public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, int QRPixelsPerModule) 20 { 21 byte[] key = Encoding.UTF8.GetBytes(accountSecretKey); 22 return GenerateSetupCode(issuer, accountTitleNoSpaces, key, QRPixelsPerModule); 23 } 24 25 /// <summary> 26 /// Generate a setup code for a Google Authenticator user to scan 27 /// </summary> 28 /// <param name="issuer">Issuer ID (the name of the system, i.e. 'MyApp'), can be omitted but not recommended https://github.com/google/google-authenticator/wiki/Key-Uri-Format </param> 29 /// <param name="accountTitleNoSpaces">Account Title (no spaces)</param> 30 /// <param name="accountSecretKey">Account Secret Key as byte[]</param> 31 /// <param name="QRPixelsPerModule">Number of pixels per QR Module (2 = ~120x120px QRCode)</param> 32 /// <returns>SetupCode object</returns> 33 public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, byte[] accountSecretKey, int QRPixelsPerModule) 34 { 35 if (accountTitleNoSpaces == null) { throw new NullReferenceException("Account Title is null"); } 36 accountTitleNoSpaces = RemoveWhitespace(accountTitleNoSpaces); 37 string encodedSecretKey = Base32Encoding.ToString(accountSecretKey); 38 string provisionUrl = null; 39 provisionUrl = String.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", accountTitleNoSpaces, encodedSecretKey.Replace("=",""), UrlEncode(issuer)); 40 41 42 43 using (QRCodeGenerator qrGenerator = new QRCodeGenerator()) 44 using (QRCodeData qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.M)) 45 using (QRCode qrCode = new QRCode(qrCodeData)) 46 using (Bitmap qrCodeImage = qrCode.GetGraphic(QRPixelsPerModule)) 47 using (MemoryStream ms = new MemoryStream()) 48 { 49 qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png); 50 51 return new SetupCode(accountTitleNoSpaces, encodedSecretKey, String.Format("data:image/png;base64,{0}", Convert.ToBase64String(ms.ToArray()))); 52 } 53 54 } 55 56 private static string RemoveWhitespace(string str) 57 { 58 return new string(str.Where(c => !Char.IsWhiteSpace(c)).ToArray()); 59 } 60 61 private string UrlEncode(string value) 62 { 63 StringBuilder result = new StringBuilder(); 64 string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~"; 65 66 foreach (char symbol in value) 67 { 68 if (validChars.IndexOf(symbol) != -1) 69 { 70 result.Append(symbol); 71 } 72 else 73 { 74 result.Append('%' + String.Format("{0:X2}", (int)symbol)); 75 } 76 } 77 78 return result.ToString().Replace(" ", "%20"); 79 } 80 81 public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6) 82 { 83 return GenerateHashedCode(accountSecretKey, counter, digits); 84 } 85 86 internal string GenerateHashedCode(string secret, long iterationNumber, int digits = 6) 87 { 88 byte[] key = Encoding.UTF8.GetBytes(secret); 89 return GenerateHashedCode(key, iterationNumber, digits); 90 } 91 92 internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6) 93 { 94 byte[] counter = BitConverter.GetBytes(iterationNumber); 95 96 if (BitConverter.IsLittleEndian) 97 { 98 Array.Reverse(counter); 99 } 100 101 HMACSHA1 hmac = new HMACSHA1(key); 102 103 byte[] hash = hmac.ComputeHash(counter); 104 105 int offset = hash[hash.Length - 1] & 0xf; 106 107 // Convert the 4 bytes into an integer, ignoring the sign. 108 int binary = 109 ((hash[offset] & 0x7f) << 24) 110 | (hash[offset + 1] << 16) 111 | (hash[offset + 2] << 8) 112 | (hash[offset + 3]); 113 114 int password = binary % (int)Math.Pow(10, digits); 115 return password.ToString(new string('0', digits)); 116 } 117 118 private long GetCurrentCounter() 119 { 120 return GetCurrentCounter(DateTime.UtcNow, _epoch, 30); 121 } 122 123 private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep) 124 { 125 return (long)(now - epoch).TotalSeconds / timeStep; 126 } 127 128 public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient) 129 { 130 return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance); 131 } 132 133 public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance) 134 { 135 var codes = GetCurrentPINs(accountSecretKey, timeTolerance); 136 return codes.Any(c => c == twoFactorCodeFromClient); 137 } 138 139 public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance) 140 { 141 List<string> codes = new List<string>(); 142 long iterationCounter = GetCurrentCounter(); 143 int iterationOffset = 0; 144 145 if (timeTolerance.TotalSeconds > 30) 146 { 147 iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00); 148 } 149 150 long iterationStart = iterationCounter - iterationOffset; 151 long iterationEnd = iterationCounter + iterationOffset; 152 153 for (long counter = iterationStart; counter <= iterationEnd; counter++) 154 { 155 codes.Add(GeneratePINAtInterval(accountSecretKey, counter)); 156 } 157 158 return codes.ToArray(); 159 } 160 }
其中GenerateSetupCode 这个方法是用于把绑定的密钥直接转成二维码图片,然后再转成base64图片 输出再页面上,这样在APP上直接用扫一扫即可绑定。
二丶由于生成的密钥不可以直接使用,需要进行Base32进行编码。下面是Base32Encoding类
1 public class Base32Encoding 2 { 3 /// <summary> 4 /// Base32 encoded string to byte[] 5 /// </summary> 6 /// <param name="input">Base32 encoded string</param> 7 /// <returns>byte[]</returns> 8 public static byte[] ToBytes(string input) 9 { 10 if (string.IsNullOrEmpty(input)) 11 { 12 throw new ArgumentNullException("input"); 13 } 14 15 input = input.TrimEnd('='); //remove padding characters 16 int byteCount = input.Length * 5 / 8; //this must be TRUNCATED 17 byte[] returnArray = new byte[byteCount]; 18 19 byte curByte = 0, bitsRemaining = 8; 20 int mask = 0, arrayIndex = 0; 21 22 foreach (char c in input) 23 { 24 int cValue = CharToValue(c); 25 26 if (bitsRemaining > 5) 27 { 28 mask = cValue << (bitsRemaining - 5); 29 curByte = (byte)(curByte | mask); 30 bitsRemaining -= 5; 31 } 32 else 33 { 34 mask = cValue >> (5 - bitsRemaining); 35 curByte = (byte)(curByte | mask); 36 returnArray[arrayIndex++] = curByte; 37 curByte = (byte)(cValue << (3 + bitsRemaining)); 38 bitsRemaining += 3; 39 } 40 } 41 42 //if we didn't end with a full byte 43 if (arrayIndex != byteCount) 44 { 45 returnArray[arrayIndex] = curByte; 46 } 47 48 return returnArray; 49 } 50 51 /// <summary> 52 /// byte[] to Base32 string, if starting from an ordinary string use Encoding.UTF8.GetBytes() to convert it to a byte[] 53 /// </summary> 54 /// <param name="input">byte[] of data to be Base32 encoded</param> 55 /// <returns>Base32 String</returns> 56 public static string ToString(byte[] input) 57 { 58 if (input == null || input.Length == 0) 59 { 60 throw new ArgumentNullException("input"); 61 } 62 63 int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; 64 char[] returnArray = new char[charCount]; 65 66 byte nextChar = 0, bitsRemaining = 5; 67 int arrayIndex = 0; 68 69 foreach (byte b in input) 70 { 71 nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); 72 returnArray[arrayIndex++] = ValueToChar(nextChar); 73 74 if (bitsRemaining < 4) 75 { 76 nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); 77 returnArray[arrayIndex++] = ValueToChar(nextChar); 78 bitsRemaining += 5; 79 } 80 81 bitsRemaining -= 3; 82 nextChar = (byte)((b << bitsRemaining) & 31); 83 } 84 85 //if we didn't end with a full char 86 if (arrayIndex != charCount) 87 { 88 returnArray[arrayIndex++] = ValueToChar(nextChar); 89 while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding 90 } 91 92 return new string(returnArray); 93 } 94 95 private static int CharToValue(char c) 96 { 97 int value = (int)c; 98 99 //65-90 == uppercase letters 100 if (value < 91 && value > 64) 101 { 102 return value - 65; 103 } 104 //50-55 == numbers 2-7 105 if (value < 56 && value > 49) 106 { 107 return value - 24; 108 } 109 //97-122 == lowercase letters 110 if (value < 123 && value > 96) 111 { 112 return value - 97; 113 } 114 115 throw new ArgumentException("Character is not a Base32 character.", "c"); 116 } 117 118 private static char ValueToChar(byte b) 119 { 120 if (b < 26) 121 { 122 return (char)(b + 65); 123 } 124 125 if (b < 32) 126 { 127 return (char)(b + 24); 128 } 129 130 throw new ArgumentException("Byte is not a value Base32 value.", "b"); 131 } 132 }
三丶主程序里面直接调用方法
1 private SetupCode Google(string key, string Guids) 2 { 3 GoogleAuthenticator gat = new GoogleAuthenticator(); 4 return gat.GenerateSetupCode("Supported Giving", key, Guids, 5); 5 }
//key系统的账号,Guid是进行加密的字符串,要求唯一,不然密钥会重复,所以这里使用Guid. 2为二维码的大小约120x120px。
SetupCode结果类为
public class SetupCode { public string Account { get; internal set; } public string AccountSecretKey { get; internal set; } public string ManualEntryKey { get; internal set; } /// <summary> /// Base64-encoded PNG image /// </summary> public string QrCodeSetupImageUrl { get; internal set; } }
ManualEntryKey 是手机绑定的密钥。如果想手动输入密钥绑定就使用此字符串。
QrCodeSetupImageUrl 是将密钥转成的二维码图片
下载这个APP
进入APP后直接绑定,就会出现一下界面,即为绑定成功,然后我们就可以使用此令牌验证了。
验证方法
//Guids 之前生成密钥的字符,此时当做唯一键来查询,CheckCode为手机上动态的6位验证吗。校验成功会返回true
GoogleAuthenticator gat = new GoogleAuthenticator(); var result = gat.ValidateTwoFactorPIN(parameters["Guids"].ToString(), parameters["CheckCode"].ToString()); if (result) { return "True"; } else { return "False"; }
这样功能就完成了。