Google Authenticator(谷歌身份验证器)C#版

.NET
872
0
0
2022-04-10
标签   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版:苹果版下载

效果图


Google Authenticator(谷歌身份验证器)C#版


Google Authenticator(谷歌身份验证器)C#版


控制台

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace GoogleAuthenticator
{
    class Program
    {
        static void Main(string[] args)
        {
            long duration = 30;
            string key = "xeon997@foxmail.com";
            GoogleAuthenticator authenticator = new GoogleAuthenticator(duration, key);
            var mobileKey = authenticator.GetMobilePhoneKey();
            while (true)
            {
                Console.WriteLine("手机端秘钥为:" + mobileKey);
                var code = authenticator.GenerateCode();
                Console.WriteLine("动态验证码为:" + code);
                Console.WriteLine("刷新倒计时:" +
                authenticator.EXPIRE_SECONDS);
                System.Threading.Thread.Sleep(1000);
                Console.Clear();
            }
        }
    }
}

认证器类:

using GoogleAuthorization;
using System;
using System.Security.Cryptography;
using System.Text;
namespace GoogleAuthenticator
{
    public class GoogleAuthenticator
    {
        /// <summary> 
        /// 初始化验证码生成规则 
        /// </summary> 
        /// <param name="key">秘钥(手机使用Base32码)</param> 
        /// <param name="duration">验证码间隔多久刷新一次(默认30秒和google同步)</param> 
        public GoogleAuthenticator(long duration = 30, string key = "xeon997@foxmail.com")
        {
            this.SERECT_KEY = key;
            this.SERECT_KEY_MOBILE = Base32.ToString(Encoding.UTF8.GetBytes(key));
            this.DURATION_TIME = duration;
        }
        /// <summary> 
        /// 间隔时间 
        /// </summary> 
        private long DURATION_TIME { get; set; }
        /// <summary> 
        /// 迭代次数 
        /// </summary> 
        private long COUNTER
        {
            get
            {
                return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds / DURATION_TIME;
            }
        }
        /// <summary> 
        /// 秘钥 
        /// </summary> 
        private string SERECT_KEY { get; set; }
        /// <summary> 
        /// 手机端输入的秘钥 
        /// </summary> 
        private string SERECT_KEY_MOBILE { get; set; }
        /// <summary> 
        /// 到期秒数 
        /// </summary> 
        public long EXPIRE_SECONDS
        {
            get
            {
                return (DURATION_TIME - (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds % DURATION_TIME);
            }
        }
        /// <summary> 
        /// 获取手机端秘钥 
        /// </summary> 
        /// <returns></returns> 
        public string GetMobilePhoneKey()
        {
            if (SERECT_KEY_MOBILE == null)
                throw new ArgumentNullException("SERECT_KEY_MOBILE");
            return SERECT_KEY_MOBILE;
        }
        /// <summary> 
        /// 生成认证码 
        /// </summary> 
        /// <returns>返回验证码</returns> 
        public string GenerateCode()
        {
            return GenerateHashedCode(SERECT_KEY, COUNTER);
        }
        /// <summary> 
        /// 按照次数生成哈希编码 
        /// </summary> 
        /// <param name="secret">秘钥</param> 
        /// <param name="iterationNumber">迭代次数</param> 
        /// <param name="digits">生成位数</param> 
        /// <returns>返回验证码</returns> 
        private string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
        {
            byte[] counter = BitConverter.GetBytes(iterationNumber);
            if (BitConverter.IsLittleEndian)
                Array.Reverse(counter);
            byte[] key = Encoding.ASCII.GetBytes(secret);
            HMACSHA1 hmac = new HMACSHA1(key, true);
            byte[] hash = hmac.ComputeHash(counter);
            int offset = hash[hash.Length - 1] & 0xf;
            int binary =
            ((hash[offset] & 0x7f) << 24)
            | ((hash[offset + 1] & 0xff) << 16)
            | ((hash[offset + 2] & 0xff) << 8)
            | (hash[offset + 3] & 0xff);
            int password = binary % (int)Math.Pow(10, digits); // 6 digits 
            return password.ToString(new string('0', digits));
        }
    }
}

Base32转换类:

using System;
namespace GoogleAuthorization
{
    public static class Base32
    {
        public static byte[] ToBytes(string input)
        {
            if (string.IsNullOrEmpty(input))
            {
                throw new ArgumentNullException("input");
            }
            input = input.TrimEnd('=');
            int byteCount = input.Length * 5 / 8;
            byte[] returnArray = new byte[byteCount];
            byte curByte = 0, bitsRemaining = 8;
            int mask = 0, arrayIndex = 0;
            foreach (char c in input)
            {
                int cValue = CharToValue(c);
                if (bitsRemaining > 5)
                {
                    mask = cValue << (bitsRemaining - 5);
                    curByte = (byte)(curByte | mask);
                    bitsRemaining -= 5;
                }
                else
                {
                    mask = cValue >> (5 - bitsRemaining);
                    curByte = (byte)(curByte | mask);
                    returnArray[arrayIndex++] = curByte;
                    curByte = (byte)(cValue << (3 + bitsRemaining));
                    bitsRemaining += 3;
                }
            }
            if (arrayIndex != byteCount)
            {
                returnArray[arrayIndex] = curByte;
            }
            return returnArray;
        }
        public static string ToString(byte[] input)
        {
            if (input == null || input.Length == 0)
            {
                throw new ArgumentNullException("input");
            }
            int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
            char[] returnArray = new char[charCount];
            byte nextChar = 0, bitsRemaining = 5;
            int arrayIndex = 0;
            foreach (byte b in input)
            {
                nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
                returnArray[arrayIndex++] = ValueToChar(nextChar);
                if (bitsRemaining < 4)
                {
                    nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
                    returnArray[arrayIndex++] = ValueToChar(nextChar);
                    bitsRemaining += 5;
                }
                bitsRemaining -= 3;
                nextChar = (byte)((b << bitsRemaining) & 31);
            }
            if (arrayIndex != charCount)
            {
                returnArray[arrayIndex++] = ValueToChar(nextChar);
                while (arrayIndex != charCount) returnArray[arrayIndex++] = '=';
            }
            return new string(returnArray);
        }
        private static int CharToValue(char c)
        {
            var value = (int)c;
            if (value < 91 && value > 64)
            {
            return value - 65;
            }
            if (value < 56 && value > 49)
            {
                return value - 24;
            }
            if (value < 123 && value > 96)
            {
                 return value - 97;
            }
            throw new ArgumentException("Character is not a Base32 character.", "c");
        }
        private static char ValueToChar(byte b)
        {
            if (b < 26)
            {
                return (char)(b + 65);
            }
            if (b < 32)
            {
                return (char)(b + 24);
            }
            throw new ArgumentException("Byte is not a value Base32 value.", "b");
        }
    }
}

总结

需要注意的坑

移动端下载的认证器的秘钥key是通过base32转码得到的,而程序端是直接输入源码。

如原秘钥为xeon997@foxmail.com生成的base32码

PBSW63RZHE3UAZTPPBWWC2LMFZRW63I=才是移动端需要输入的秘钥。

在网上找了很多资料没有发现关于C#的案例,所以在此记录一下自己遇到的坑,让更多的人能够跳过这个坑

原文地址:

https://www.cnblogs.com/easyauthor/p/11054869.html