計算機網路安全 —— C# 使用谷歌身份驗證器(Google Authenticator)(五)
一、Google Authenticator 基本概念
Google Authenticator是谷歌推出的一款動態口令工具,旨在解決大家Google賬戶遭到惡意攻擊的問題,在手機端生成動態口令後,在Google相關的服務登陸中除了用正常使用者名稱和密碼外,需要輸入一次動態口令才能驗證成功,此舉是為了保護使用者的資訊保安。
谷歌驗證(Google Authenticator)通過兩個驗證步驟,在登入時為使用者的谷歌帳號提供一層額外的安全保護。使用谷歌驗證可以直接在使用者的裝置上生成動態密碼,無需網路連線。其基本步驟如下:
- 使用google authenticator PAM外掛為登入賬號生成動態驗證碼。
- 手機安裝Google身份驗證器,通過此工具掃描上一步生成的二維碼圖形,獲取動態驗證碼。
當用戶在Google帳號中啟用“兩步驗證”功能後,就可以使用Google Authenticator來防止陌生人通過盜取的密碼訪問使用者的帳戶。通過兩步驗證流程登入時,使用者需要同時使用密碼和通過手機產生的動態密碼來驗證使用者的身份。也就是說,即使可能的入侵者竊取或猜出了使用者的密碼,也會因不能使用使用者的手機而無法登入帳戶。
更多原理可以檢視閱讀“詳解Google Authenticator工作原理”。
二、.NET 使用 Google Authenticator
第一步,通過 Nuget 下載 Google Authenticator 安裝包
第二步,例如我們要實現這樣的功能:手機掃描 PC 生成的二維碼,繫結使用者資訊後,後續使用手機生成的驗證碼輸入到 PC 端進行校驗。我們通過程式設計生成一個二維碼如下圖所示:
第三步:安裝 Google Authenticator APP,安卓版下載、IOS下載(注意:安卓版本下載需FQ)。安裝成功後,掃描上圖的二維碼新增如下:
第四步:輸入生成的驗證碼,在 PC 端輸入口令後,展示校驗通過(注意:口令有效時間為30秒)。
Google Authenticator 在 PC 端生成二維碼、手機上生成驗證碼、 PC 端校驗驗證碼,這些過程無需網路,只需要保證 PC 時間和手機時間正確一致即可。
Google Authenticator 工具類程式碼如下(引用自 https://www.cnblogs.com/denuk/p/11608510.html):
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 161 /// <summary> 162 /// Writes a string into a bitmap 163 /// </summary> 164 /// <param name="qrCodeSetupImageUrl"></param> 165 /// <returns></returns> 166 public static Image GetQRCodeImage(string qrCodeSetupImageUrl) 167 { 168 // data:image/png;base64, 169 qrCodeSetupImageUrl = qrCodeSetupImageUrl.Replace("data:image/png;base64,", ""); 170 Image img = null; 171 byte[] buffer = Convert.FromBase64String(qrCodeSetupImageUrl); 172 using (MemoryStream ms = new MemoryStream(buffer)) 173 { 174 img = Image.FromStream(ms); 175 } 176 return img; 177 } 178 } 179 180 public class Base32Encoding 181 { 182 /// <summary> 183 /// Base32 encoded string to byte[] 184 /// </summary> 185 /// <param name="input">Base32 encoded string</param> 186 /// <returns>byte[]</returns> 187 public static byte[] ToBytes(string input) 188 { 189 if (string.IsNullOrEmpty(input)) 190 { 191 throw new ArgumentNullException("input"); 192 } 193 194 input = input.TrimEnd('='); //remove padding characters 195 int byteCount = input.Length * 5 / 8; //this must be TRUNCATED 196 byte[] returnArray = new byte[byteCount]; 197 198 byte curByte = 0, bitsRemaining = 8; 199 int mask = 0, arrayIndex = 0; 200 201 foreach (char c in input) 202 { 203 int cValue = CharToValue(c); 204 205 if (bitsRemaining > 5) 206 { 207 mask = cValue << (bitsRemaining - 5); 208 curByte = (byte)(curByte | mask); 209 bitsRemaining -= 5; 210 } 211 else 212 { 213 mask = cValue >> (5 - bitsRemaining); 214 curByte = (byte)(curByte | mask); 215 returnArray[arrayIndex++] = curByte; 216 curByte = (byte)(cValue << (3 + bitsRemaining)); 217 bitsRemaining += 3; 218 } 219 } 220 221 //if we didn't end with a full byte 222 if (arrayIndex != byteCount) 223 { 224 returnArray[arrayIndex] = curByte; 225 } 226 227 return returnArray; 228 } 229 230 /// <summary> 231 /// byte[] to Base32 string, if starting from an ordinary string use Encoding.UTF8.GetBytes() to convert it to a byte[] 232 /// </summary> 233 /// <param name="input">byte[] of data to be Base32 encoded</param> 234 /// <returns>Base32 String</returns> 235 public static string ToString(byte[] input) 236 { 237 if (input == null || input.Length == 0) 238 { 239 throw new ArgumentNullException("input"); 240 } 241 242 int charCount = (int)Math.Ceiling(input.Length / 5d) * 8; 243 char[] returnArray = new char[charCount]; 244 245 byte nextChar = 0, bitsRemaining = 5; 246 int arrayIndex = 0; 247 248 foreach (byte b in input) 249 { 250 nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining))); 251 returnArray[arrayIndex++] = ValueToChar(nextChar); 252 253 if (bitsRemaining < 4) 254 { 255 nextChar = (byte)((b >> (3 - bitsRemaining)) & 31); 256 returnArray[arrayIndex++] = ValueToChar(nextChar); 257 bitsRemaining += 5; 258 } 259 260 bitsRemaining -= 3; 261 nextChar = (byte)((b << bitsRemaining) & 31); 262 } 263 264 //if we didn't end with a full char 265 if (arrayIndex != charCount) 266 { 267 returnArray[arrayIndex++] = ValueToChar(nextChar); 268 while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding 269 } 270 271 return new string(returnArray); 272 } 273 274 private static int CharToValue(char c) 275 { 276 int value = (int)c; 277 278 //65-90 == uppercase letters 279 if (value < 91 && value > 64) 280 { 281 return value - 65; 282 } 283 //50-55 == numbers 2-7 284 if (value < 56 && value > 49) 285 { 286 return value - 24; 287 } 288 //97-122 == lowercase letters 289 if (value < 123 && value > 96) 290 { 291 return value - 97; 292 } 293 294 throw new ArgumentException("Character is not a Base32 character.", "c"); 295 } 296 297 private static char ValueToChar(byte b) 298 { 299 if (b < 26) 300 { 301 return (char)(b + 65); 302 } 303 304 if (b < 32) 305 { 306 return (char)(b + 24); 307 } 308 309 throw new ArgumentException("Byte is not a value Base32 value.", "b"); 310 } 311 }
測試程式碼如下:
1 // 金鑰 2 private string key = "123456"; 3 4 // 生成新的二維碼 5 private void ButtonBase_OnClick1(object sender, RoutedEventArgs e) 6 { 7 // 發行人 8 string issuer = TextBoxIssuer.Text; 9 10 //登陸賬號名稱 11 string user = TextBoxUser.Text; 12 13 // 生成 SetupCode 14 var code = new GoogleAuthenticator().GenerateSetupCode(issuer, user, key, 5); 15 16 // 轉換成點陣圖 17 var img = GoogleAuthenticator.GetQRCodeImage(code.QrCodeSetupImageUrl); 18 19 // 展示點陣圖 20 { 21 Bitmap bitmap = img as Bitmap; 22 IntPtr myImagePtr = bitmap.GetHbitmap(); 23 ImageSource imgsource = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(myImagePtr, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); //建立imgSource 24 ImageQRCode.Source = imgsource; 25 } 26 } 27 28 // 驗證校驗 29 private void ButtonBase_OnClick(object sender, RoutedEventArgs e) 30 { 31 string token = TextBoxToken.Text; 32 if (string.IsNullOrEmpty(token) == false) 33 { 34 GoogleAuthenticator gat = new GoogleAuthenticator(); 35 var result = gat.ValidateTwoFactorPIN(key, token.ToString()); 36 if (result) 37 { 38 MessageBox.Show("動態碼校驗通過!", "提示資訊", MessageBoxButton.OK, MessageBoxImage.Question); 39 } 40 else 41 { 42 MessageBox.Show("動態碼校驗未通過!", "提示資訊", MessageBoxButton.OK, MessageBoxImage.Warning); 43 } 44 } 45 }
&n