谷歌身份驗證 asp.net core和go的實現

一、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 安裝包,Google Authenticator 在 PC 端生成二維碼、手機上生成驗證碼、 PC 端校驗驗證碼,這些過程無需網絡,只需要保證 PC 時間和手機時間正確一致即可。

  Google Authenticator 工具類代碼如下,注意網上的多數是utf8, 我這裏爲了和go兼容 都用base32

using Google.Authenticator;
using QRCoder;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;

namespace GoogleAuthenticatorTest
{
    public class GoogleAuthenticator
    {
        private readonly static DateTime _epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
        private TimeSpan DefaultClockDriftTolerance { get; set; }

        public GoogleAuthenticator()
        {
            DefaultClockDriftTolerance = TimeSpan.FromMinutes(5);
        }

        /// <summary>
        /// Generate a setup code for a Google Authenticator user to scan
        /// </summary>
        /// <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>
        /// <param name="accountTitleNoSpaces">Account Title (no spaces)</param>
        /// <param name="accountSecretKey">Account Secret Key</param>
        /// <param name="QRPixelsPerModule">Number of pixels per QR Module (2 pixels give ~ 100x100px QRCode)</param>
        /// <returns>SetupCode object</returns>
        public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, string accountSecretKey, int QRPixelsPerModule)
        {
            //byte[] key = Encoding.UTF8.GetBytes(accountSecretKey);
            byte[] key = Base32Encoding.ToBytes(accountSecretKey);
            return GenerateSetupCode(issuer, accountTitleNoSpaces, key, QRPixelsPerModule);
        }

        /// <summary>
        /// Generate a setup code for a Google Authenticator user to scan
        /// </summary>
        /// <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>
        /// <param name="accountTitleNoSpaces">Account Title (no spaces)</param>
        /// <param name="accountSecretKey">Account Secret Key as byte[]</param>
        /// <param name="QRPixelsPerModule">Number of pixels per QR Module (2 = ~120x120px QRCode)</param>
        /// <returns>SetupCode object</returns>
        public SetupCode GenerateSetupCode(string issuer, string accountTitleNoSpaces, byte[] accountSecretKey, int QRPixelsPerModule)
        {
            if (accountTitleNoSpaces == null) { throw new NullReferenceException("Account Title is null"); }
            accountTitleNoSpaces = RemoveWhitespace(accountTitleNoSpaces);
            string encodedSecretKey = Base32Encoding.ToString(accountSecretKey);
            string provisionUrl = null;
            //otpauth://totp/Google:[email protected]?secret=xxxx&issuer=Google
            provisionUrl = String.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", accountTitleNoSpaces, encodedSecretKey.Replace("=", ""), UrlEncode(issuer));



            using (QRCodeGenerator qrGenerator = new QRCodeGenerator())
            using (QRCodeData qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.M))
            using (QRCode qrCode = new QRCode(qrCodeData))
            using (Bitmap qrCodeImage = qrCode.GetGraphic(QRPixelsPerModule))
            using (MemoryStream ms = new MemoryStream())
            {
                qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png);

                return new SetupCode(accountTitleNoSpaces, encodedSecretKey, String.Format("data:image/png;base64,{0}", Convert.ToBase64String(ms.ToArray())));
            }

        }

        private static string RemoveWhitespace(string str)
        {
            return new string(str.Where(c => !Char.IsWhiteSpace(c)).ToArray());
        }

        private string UrlEncode(string value)
        {
            StringBuilder result = new StringBuilder();
            string validChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

            foreach (char symbol in value)
            {
                if (validChars.IndexOf(symbol) != -1)
                {
                    result.Append(symbol);
                }
                else
                {
                    result.Append('%' + String.Format("{0:X2}", (int)symbol));
                }
            }

            return result.ToString().Replace(" ", "%20");
        }

        public string GeneratePINAtInterval(string accountSecretKey, long counter, int digits = 6)
        {
            return GenerateHashedCode(accountSecretKey, counter, digits);
        }

        internal string GenerateHashedCode(string secret, long iterationNumber, int digits = 6)
        {
            // byte[] key = Encoding.UTF8.GetBytes(secret);
            byte[] key = Base32Encoding.ToBytes(secret);
            return GenerateHashedCode(key, iterationNumber, digits);
        }

        internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6)
        {
            byte[] counter = BitConverter.GetBytes(iterationNumber);

            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(counter);
            }

            HMACSHA1 hmac = new HMACSHA1(key);

            byte[] hash = hmac.ComputeHash(counter);

            int offset = hash[hash.Length - 1] & 0xf;

            // Convert the 4 bytes into an integer, ignoring the sign.
            int binary =
                ((hash[offset] & 0x7f) << 24)
                | (hash[offset + 1] << 16)
                | (hash[offset + 2] << 8)
                | (hash[offset + 3]);

            int password = binary % (int)Math.Pow(10, digits);
            return password.ToString(new string('0', digits));
        }

        private long GetCurrentCounter()
        {
            return GetCurrentCounter(DateTime.UtcNow, _epoch, 30);
        }

        private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep)
        {
            return (long)(now - epoch).TotalSeconds / timeStep;
        }

        public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient)
        {
            return ValidateTwoFactorPIN(accountSecretKey, twoFactorCodeFromClient, DefaultClockDriftTolerance);
        }

        public bool ValidateTwoFactorPIN(string accountSecretKey, string twoFactorCodeFromClient, TimeSpan timeTolerance)
        {
            var codes = GetCurrentPINs(accountSecretKey, timeTolerance);
            return codes.Any(c => c == twoFactorCodeFromClient);
        }

        public string[] GetCurrentPINs(string accountSecretKey, TimeSpan timeTolerance)
        {
            List<string> codes = new List<string>();
            long iterationCounter = GetCurrentCounter();
            int iterationOffset = 0;

            if (timeTolerance.TotalSeconds > 30)
            {
                iterationOffset = Convert.ToInt32(timeTolerance.TotalSeconds / 30.00);
            }

            long iterationStart = iterationCounter - iterationOffset;
            long iterationEnd = iterationCounter + iterationOffset;

            for (long counter = iterationStart; counter <= iterationEnd; counter++)
            {
                codes.Add(GeneratePINAtInterval(accountSecretKey, counter));
            }

            return codes.ToArray();
        }

        /// <summary>
        /// Writes a string into a bitmap
        /// </summary>
        /// <param name="qrCodeSetupImageUrl"></param>
        /// <returns></returns>
        public static Image GetQRCodeImage(string qrCodeSetupImageUrl)
        {
            // data:image/png;base64,
            qrCodeSetupImageUrl = qrCodeSetupImageUrl.Replace("data:image/png;base64,", "");
            Image img = null;
            byte[] buffer = Convert.FromBase64String(qrCodeSetupImageUrl);
            using (MemoryStream ms = new MemoryStream(buffer))
            {
                img = Image.FromStream(ms);
            }
            return img;
        }
    }

    public class Base32Encoding
    {
        /// <summary>
        /// Base32 encoded string to byte[]
        /// </summary>
        /// <param name="input">Base32 encoded string</param>
        /// <returns>byte[]</returns>
        public static byte[] ToBytes(string input)
        {
            if (string.IsNullOrEmpty(input))
            {
                throw new ArgumentNullException("input");
            }

            input = input.TrimEnd('='); //remove padding characters
            int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
            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 we didn't end with a full byte
            if (arrayIndex != byteCount)
            {
                returnArray[arrayIndex] = curByte;
            }

            return returnArray;
        }

        /// <summary>
        /// byte[] to Base32 string, if starting from an ordinary string use Encoding.UTF8.GetBytes() to convert it to a byte[]
        /// </summary>
        /// <param name="input">byte[] of data to be Base32 encoded</param>
        /// <returns>Base32 String</returns>
        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 we didn't end with a full char
            if (arrayIndex != charCount)
            {
                returnArray[arrayIndex++] = ValueToChar(nextChar);
                while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
            }

            return new string(returnArray);
        }

        private static int CharToValue(char c)
        {
            int value = (int)c;

            //65-90 == uppercase letters
            if (value < 91 && value > 64)
            {
                return value - 65;
            }
            //50-55 == numbers 2-7
            if (value < 56 && value > 49)
            {
                return value - 24;
            }
            //97-122 == lowercase letters
            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");
        }
    }
}

使用和驗證:默認的index.cshtml

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
    string key = "LC42VPXL3VUMBCAN";
    string issuer = "Google";
    string user = "[email protected]";

    // 生成 SetupCode
    var code = new GoogleAuthenticator().GenerateSetupCode(issuer, user, key, 5);
}

<div class="text-center">
    <h1 class="display-4">GoogleAuthenticator</h1><br />
    <p>Account:@code.Account</p><br/>
    <p>Key:@code.ManualEntryKey</p><br />
    <img  src="@code.QrCodeSetupImageUrl"/>
</div>

新建token.cshtml來驗證token:

@page
@model GoogleAuthenticatorTest.Pages.tokenModel
@{
    ViewData["Title"] = "Home page";

    string key = "LC42VPXL3VUMBCAN";
    string token = HttpContext.Request.Query["token"];
    string result = "token驗證失敗";
    if (!string.IsNullOrEmpty(token)){
        GoogleAuthenticator gat = new GoogleAuthenticator();
        if (gat.ValidateTwoFactorPIN(key, token)) {
            result = "token驗證成功";
        }
    }

}

<div class="text-center">
    <p>@result</p>

</div>

運行效果(環境: win10 + asp.netcore5.0 + vscode):

三、GO 直接實現

首先二維碼otpauth://totp/Google:[email protected]?secret=LC42VPXL3VUMBCAN&issuer=Google我們可以用草料來生成 , 然後用手機來掃碼,然後在調用API來驗證:

googleAuthenticator.go

package googleAuthenticator

import (
    "crypto/hmac"
    "crypto/sha1"
    "encoding/base32"
    "encoding/hex"
    "errors"
    "fmt"
    "math"
    "math/rand"
    "strconv"
    "strings"
    "time"
)

type GAuth struct {
    codeLen float64
    table   map[string]int
}

var (
    ErrSecretLengthLss     = errors.New("secret length lss 6 error")
    ErrSecretLength        = errors.New("secret length error")
    ErrPaddingCharCount    = errors.New("padding char count error")
    ErrPaddingCharLocation = errors.New("padding char Location error")
    ErrParam               = errors.New("param error")
)

var (
    Table = []string{
        "A", "B", "C", "D", "E", "F", "G", "H", // 7
        "I", "J", "K", "L", "M", "N", "O", "P", // 15
        "Q", "R", "S", "T", "U", "V", "W", "X", // 23
        "Y", "Z", "2", "3", "4", "5", "6", "7", // 31
        "=", // padding char
    }

    allowedValues = map[int]string{
        6: "======",
        4: "====",
        3: "===",
        1: "=",
        0: "",
    }
)

func NewGAuth() *GAuth {
    return &GAuth{
        codeLen: 6,
        table:   arrayFlip(Table),
    }
}

// SetCodeLength Set the code length, should be >=6
func (this *GAuth) SetCodeLength(length float64) error {
    if length < 6 {
        return ErrSecretLengthLss
    }
    this.codeLen = length
    return nil
}

// CreateSecret create new secret
// 16 characters, randomly chosen from the allowed base32 characters.
func (this *GAuth) CreateSecret(lens ...int) (string, error) {
    var (
        length int
        secret []string
    )
    // init length
    switch len(lens) {
    case 0:
        length = 16
    case 1:
        length = lens[0]
    default:
        return "", ErrParam
    }
    for i := 0; i < length; i++ {
        secret = append(secret, Table[rand.Intn(len(Table))])
    }
    return strings.Join(secret, ""), nil
}

// VerifyCode Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now
func (this *GAuth) VerifyCode(secret, code string, discrepancy int64) (bool, error) {
    // now time
    curTimeSlice := time.Now().Unix() / 30
    for i := -discrepancy; i <= discrepancy; i++ {
        calculatedCode, err := this.GetCode(secret, curTimeSlice+i)
        if err != nil {
            return false, err
        }
        if calculatedCode == code {
            return true, nil
        }
    }
    return false, nil
}

// GetCode Calculate the code, with given secret and point in time
func (this *GAuth) GetCode(secret string, timeSlices ...int64) (string, error) {
    var timeSlice int64
    switch len(timeSlices) {
    case 0:
        timeSlice = time.Now().Unix() / 30
    case 1:
        timeSlice = timeSlices[0]
    default:
        return "", ErrParam
    }
    secret = strings.ToUpper(secret)
    secretKey, err := base32.StdEncoding.DecodeString(secret)
    if err != nil {
        return "", err
    }
    tim, err := hex.DecodeString(fmt.Sprintf("%016x", timeSlice))
    if err != nil {
        return "", err
    }
    hm := HmacSha1(secretKey, tim)
    offset := hm[len(hm)-1] & 0x0F
    hashpart := hm[offset : offset+4]
    value, err := strconv.ParseInt(hex.EncodeToString(hashpart), 16, 0)
    if err != nil {
        return "", err
    }
    value = value & 0x7FFFFFFF
    modulo := int64(math.Pow(10, this.codeLen))
    format := fmt.Sprintf("%%0%dd", int(this.codeLen))
    return fmt.Sprintf(format, value%modulo), nil
}

func arrayFlip(oldArr []string) map[string]int {
    newArr := make(map[string]int, len(oldArr))
    for key, value := range oldArr {
        newArr[value] = key
    }
    return newArr
}

func HmacSha1(key, data []byte) []byte {
    mac := hmac.New(sha1.New, key)
    mac.Write(data)
    return mac.Sum(nil)
}

func (this *GAuth) GetOtpAuth(issuer, accountTitleNoSpaces, secret string) (string, error) {
    secretbyte, err := base32.StdEncoding.DecodeString(secret)
    if err != nil {
        return "", err
    }

    secret = base32.StdEncoding.EncodeToString(secretbyte)
    auth := fmt.Sprintf("otpauth://totp/%v:%v?secret=%v&issuer=%v", issuer, accountTitleNoSpaces, secret, issuer)

    return auth, nil
}

使用

package main

import (
    "fmt"
    "gotest/googleAuthenticator"
)

func main() {

    // 用草料二維碼 https://cli.im/ 生成 以下地址:
    //otpauth://totp/Google:[email protected]?secret=LC42VPXL3VUMBCAN&issuer=Google
    ga := googleAuthenticator.NewGAuth()
    secret, err := ga.CreateSecret(16)
    if err != nil {
        fmt.Println(err)
    }
    secret = "LC42VPXL3VUMBCAN"

    auth, err := ga.GetOtpAuth("Google", "[email protected]", secret)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(auth)

    code, err := ga.GetCode(secret)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(code)
    //code := "027093"

    ret, err := ga.VerifyCode(secret, code, 1)
    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(ret)

}

最後運行通過, 用Go生成驗證碼, C# 這邊來驗證

下載地址:

https://github.com/dz45693/GoogleAuthenticatorTest.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章