你知道怎麼使用Google兩步驗證保護賬戶安全嗎?

福祿網路研發團隊發表於2021-11-01

為什麼我們需要使用它?

網際網路是一個極其危險的地方,有很多不懷好意的人想要訪問我們的線上賬戶。通過使用雙因素身份驗證,可以為我們的賬號提供額外的安全。使用者名稱密碼方式的登入變得越來越不安全,你肯定聽說過“撞庫”這個名詞,是黑客圈的術語,即網路黑客將網際網路上已洩露的賬號密碼,拿到其他網站批量登入,從而“撞出”其他網站的賬號密碼。很不幸的是,由於許多網民習慣多個網站使用一個賬號密碼,所以“撞庫”有著不低的成功率。

b7003af33a87e950c595a81310385343faf2b4c11

對有些人來說,盜取密碼比您想象的更簡單

以下任意一種常見操作都可能讓您面臨密碼被盜的風險:

  • 在多個網站上使用同一密碼
  • 從網際網路上下載軟體
  • 點選電子郵件中的連結
    兩步驗證可以將別有用心的人阻擋在外,即使他們知道您的密碼也無可奈何。

什麼是Google兩步驗證?

藉助Google兩步驗證,通過密碼和手機為帳戶提供雙重保護

b7003af33a87e950c595a81310385343faf2b4c12
Q

第一步:您需要輸入密碼

每當您登入賬戶時,都需要照常輸入賬號密碼。

第二步:還需要執行其他操作

接著,驗證碼將會以簡訊的形式傳送到手機上或通過語音電話告知,或者通過Google Authenticator App生成提供。

多一道安全防線

大多數使用者的帳戶只有密碼這一道安全防線。啟用兩步驗證後,即使有人破解了您的密碼,他們仍需要藉助您的手機或安全金鑰,才能登入您的帳戶。

b7003af33a87e950c595a81310385343faf2b4c13

什麼是Google Authenticator ?

Google Authenticator(Wiki)是谷歌推出的基於時間的動態口令app(谷歌身份驗證),只需要在手機上安裝該APP,就可以生成一個隨著時間變化的一次性口令,解決大家的賬戶遭到惡意攻擊的問題,在手機端生成動態口令後,除了用正常使用者名稱和密碼外,需要輸入一次動態口令才能驗證成功。

Google Authenticator採用的演算法是TOTP(Time-Based One-Time Password基於時間的一次性密碼),其核心內容包括以下三點:

  • 一個共享金鑰(一個位元組序列);
  • 當前時間輸入;
  • 一個簽名函式。

具體原理推薦大家閱讀:

Google賬戶兩步驗證的工作原理

詳解Google Authenticator工作原理

我在這裡準備了一個完整可執行的C# WinForm程式,感興趣的朋友請 點選這裡 進行檢視,提取碼:hemd。

Dingtalk_202110311327026

Dingtalk_20211031134446

  • Account Name:對應我們的賬號名,可以是手機號、郵箱等
  • Secret Key:這個是我們的金鑰Key,用於生成金鑰。一般我們將這個值存放在使用者表中的某個欄位中。
  • Encoded Key:這個是最終生成的金鑰,使用者如果無法掃碼二維碼,我們可以將金鑰傳送至使用者手機。

上圖中,可以看出有3個口令,即我們在程式碼中設定的漂移為30s,主要是防止出現如下問題:

  • 由於網路延時,使用者輸入延遲等因素,可能當伺服器端接收到一次性密碼時,T的數值已經改變,這樣就會導致伺服器計算的一次性密碼值與使用者輸入的不同,驗證失敗。解決這個問題個一個方法是,伺服器計算當前時間片以及前面的n個時間片內的一次性密碼值,只要其中有一個與使用者輸入的密碼相同,則驗證通過。當然,n不能太大,否則會降低安全性。
  • 我們知道如果客戶端和伺服器的時鐘有偏差,會造成與上面類似的問題,也就是客戶端生成的密碼和服務端生成的密碼不一致。但是,如果伺服器通過計算前n個時間片的密碼並且成功驗證之後,伺服器就知道了客戶端的時鐘偏差。因此,下一次驗證時,伺服器就可以直接將偏差考慮在內進行計算,而不需要進行n次計算。

下面是C#原始碼提供:

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

    public TwoFactorAuthenticator()
    {
        DefaultClockDriftTolerance = TimeSpan.FromSeconds(30);  //建議此處將時間漂移設定為30s,即允許前後各一個時間片。不能不建議設定太大,否則會降低安全性
    }

    public TwoFactorAuthenticator(TimeSpan defaultClockDriftTolerance)
    {
        DefaultClockDriftTolerance = defaultClockDriftTolerance;
    }

    /// <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="accountName">Account Name (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 accountName, string accountSecretKey, int qrPixelsPerModule)
    {
        var key = Encoding.UTF8.GetBytes(accountSecretKey);
        return GenerateSetupCode(issuer, accountName, 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="accountName">Account Name (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 accountName, byte[] accountSecretKey, int qrPixelsPerModule)
    {
        if (accountName == null) { throw new NullReferenceException("Account Title is null"); }
        accountName = accountName.Trim();
        var encodedSecretKey = Base32Encode(accountSecretKey);
        var provisionUrl = string.IsNullOrWhiteSpace(issuer) ? $"otpauth://totp/{accountName}?secret={encodedSecretKey}" : string.Format("otpauth://totp/{2}:{0}?secret={1}&issuer={2}", accountName, encodedSecretKey, HttpUtility.UrlEncode(issuer, Encoding.UTF8));
        using (var qrGenerator = new QRCodeGenerator())
        using (var qrCodeData = qrGenerator.CreateQrCode(provisionUrl, QRCodeGenerator.ECCLevel.Q))
        using (var qrCode = new QRCode(qrCodeData))
        using (var qrCodeImage = qrCode.GetGraphic(qrPixelsPerModule))
        using (var ms = new MemoryStream())
        {
            qrCodeImage.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
            return new SetupCode(accountName, encodedSecretKey, Convert.ToBase64String(ms.ToArray()));
        }
    }

    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)
    {
        var key = Encoding.UTF8.GetBytes(secret);
        return GenerateHashedCode(key, iterationNumber, digits);
    }

    /// <summary>
    /// 
    /// </summary>
    /// <param name="key"></param>
    /// <param name="iterationNumber"></param>
    /// <param name="digits">The digits parameter may have the values 6 or 8, and determines how long of a one-time passcode to display to the user. The default is 6.</param>
    /// <returns></returns>
    internal string GenerateHashedCode(byte[] key, long iterationNumber, int digits = 6)
    {
        var counter = BitConverter.GetBytes(iterationNumber);

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

        var hmac = new HMACSHA1(key);

        var hash = hmac.ComputeHash(counter);

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

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

        var 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 GetCurrentPIN(string accountSecretKey)
    {
        return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter());
    }

    public string GetCurrentPIN(string accountSecretKey, DateTime now)
    {
        return GeneratePINAtInterval(accountSecretKey, GetCurrentCounter(now, _epoch, 30));
    }

    public string[] GetCurrentPINs(string accountSecretKey)
    {
        return GetCurrentPINs(accountSecretKey, DefaultClockDriftTolerance);
    }

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

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

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

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

        return codes.ToArray();
    }

    private string Base32Encode(byte[] data)
    {
        const int inByteSize = 8;
        const int outByteSize = 5;
        var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();

        int i = 0, index = 0;
        var result = new StringBuilder((data.Length + 7) * inByteSize / outByteSize);

        while (i < data.Length)
        {
            var currentByte = data[i];

            /* Is the current digit going to span a byte boundary? */
            int digit;
            if (index > (inByteSize - outByteSize))
            {
                var nextByte = (i + 1) < data.Length ? data[i + 1] : 0;

                digit = currentByte & (0xFF >> index);
                index = (index + outByteSize) % inByteSize;
                digit <<= index;
                digit |= nextByte >> (inByteSize - index);
                i++;
            }
            else
            {
                digit = (currentByte >> (inByteSize - (index + outByteSize))) & 0x1F;
                index = (index + outByteSize) % inByteSize;
                if (index == 0)
                    i++;
            }
            result.Append(alphabet[digit]);
        }

        return result.ToString();
    }
}

示例程式程式碼:

public partial class FrmMain : Form
{
    public FrmMain()
    {
        InitializeComponent();
    }

    private void FrmMain_Load(object sender, EventArgs e)
    {

    }

    private void btnSetup_Click(object sender, EventArgs e)
    {
        if (string.IsNullOrEmpty(txtSecretKey.Text.Trim()) || string.IsNullOrEmpty(txtSecretKey.Text.Trim()) || string.IsNullOrEmpty(txtAccountName.Text.Trim())) return;
        var tfA = new TwoFactorAuthenticator();
        var setupCode = tfA.GenerateSetupCode(txtIssuer.Text.Trim(), txtAccountName.Text.Trim(), this.txtSecretKey.Text.Trim(), 3);
        var ms = new MemoryStream(Convert.FromBase64String(setupCode.QrCodeSetupImageUrl));
        this.pbQR.Image = Image.FromStream(ms);
        ms.Dispose();
        this.txtSetupCode.Text = $@"Account: {setupCode.Account}{System.Environment.NewLine}Secret Key: {this.txtSecretKey.Text.Trim()}{System.Environment.NewLine}Encoded Key: {setupCode.ManualEntryKey}";
    }

    private void btnGetCurrentCode_Click(object sender, EventArgs e)
    {
        this.txtCurrentCodes.Text = string.Join(System.Environment.NewLine, new TwoFactorAuthenticator().GetCurrentPINs(this.txtSecretKey.Text));
    }

    private void btnTest_Click(object sender, EventArgs e)
    {
        var tfA = new TwoFactorAuthenticator();
        var result = tfA.ValidateTwoFactorPIN(txtSecretKey.Text, this.txtCode.Text);
        MessageBox.Show(result ? "Validated" : "Incorrect", "Result");
    }
}

使用Google兩步驗證的好處

  • 接入使用簡單,門檻低,零成本
  • 保護系統賬戶安全
  • 節省企業成本(如簡訊、郵件需要額外費用)
  • 只需一部手機,同時管理多個賬戶

實際專案效果演示

sdd

歡迎與我討論交流!

福祿·研發中心 福祿娃

相關文章