• 计算机网络安全 —— C# 使用谷歌身份验证器(Google Authenticator)(五)


    一、Google Authenticator 基本概念

    Google Authenticator是谷歌推出的一款动态口令工具,旨在解决大家Google账户遭到恶意攻击的问题,在手机端生成动态口令后,在Google相关的服务登陆中除了用正常用户名和密码外,需要输入一次动态口令才能验证成功,此举是为了保护用户的信息安全。

    ​ 谷歌验证(Google Authenticator)通过两个验证步骤,在登录时为用户的谷歌帐号提供一层额外的安全保护。使用谷歌验证可以直接在用户的设备上生成动态密码,无需网络连接。其基本步骤如下:

    1. 使用google authenticator PAM插件为登录账号生成动态验证码。
    2. 手机安装Google身份验证器,通过此工具扫描上一步生成的二维码图形,获取动态验证码。

    当用户在Google帐号中启用“两步验证”功能后,就可以使用Google Authenticator来防止陌生人通过盗取的密码访问用户的帐户。通过两步验证流程登录时,用户需要同时使用密码和通过手机产生的动态密码来验证用户的身份。也就是说,即使可能的入侵者窃取或猜出了用户的密码,也会因不能使用用户的手机而无法登录帐户。

    ​ 更多原理可以查看阅读“详解Google Authenticator工作原理”。

    二、.NET 使用 Google Authenticator

    ​ 第一步,通过 Nuget 下载 Google Authenticator 安装包

    img

    第二步,例如我们要实现这样的功能:手机扫描 PC 生成的二维码,绑定用户信息后,后续使用手机生成的验证码输入到 PC 端进行校验。我们通过编程生成一个二维码如下图所示:

    img

    第三步:安装 Google Authenticator APP,安卓版下载IOS下载(注意:安卓版本下载需翻墙)。安装成功后,扫描上图的二维码添加如下:

    img

    第四步:输入生成的验证码,在 PC 端输入口令后,展示校验通过(注意:口令有效时间为30秒)。

    img

    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         }
    
  • 相关阅读:
    关于音视频同步
    redis JedisConnectionException: Could not get a resource from the pool
    ping指定IP的指定端口号
    如何查看端口号是否被占用
    mongodb增删改查基础语法
    mongodb重置密码
    本地MongoDB服务开启与连接本地以及远程服务器MongoDB服务
    MongoDB服务的安装与删除
    可视化
    Elasticsearch下载安装
  • 原文地址:https://www.cnblogs.com/dongweian/p/14339757.html
Copyright © 2020-2023  润新知