以C#實作Google Authenticator 產生一次性驗證碼
之前寫了自動登入到合作夥伴公司的B2B平台網站抓取每日報表,
該平台原本是透過圖形驗證碼的方式來阻擋爬蟲,但其選擇圖形其實是很單純,
難易度太低,有心人要解開其實是很簡單的,
後來該平台應該有意識到這個問題,最近更新為使用Google Authenticator的一次性密碼驗證碼TOPT ( Time-based One-time Password ) 進行二階段驗證
1. 先下載Google Authenticator app:
Android ,
iOS
2. 使用該 app 掃瞄讀取B2B平台所提供的QRCode後便會新增一個自動產生一次性驗證碼的項目,驗證碼每30秒就會變動一次
不得不說這的確是增強了網站登入的防護,要給予掌聲鼓勵!
但這種方式對良善之心要運用自動登入的老百姓該怎麼辦?
難道要回到原始時代人工作業嗎?
想來想去,心有不甘!
走~!想解決方案去!!!
解決方案一:
即然我們可以看到這個驗證碼,那只要在
VM 虛擬機器上掛個Android系統
再安裝Google Authenticator 來產生驗登碼,
寫個程式自動擷取該視窗驗證碼的畫面後進行OCR辦識,這樣就解決了。
這是我一開始的想法,後來也實作出來了(擷取視窗驗證碼的方式之後再寫另一篇)。
這樣的缺點是:
1. 擷取畫面時如果該視窗突然被其他視窗蓋到就會失敗
2. 須安裝VM,而且要一直開著…有點浪費系統資源
有沒有方式可以解決浪費資源這點呢?後來想說會不會Chrome有相關外掛呢?
這樣就能在要執行時再開啟Browser進行截取就好了
google了一下
還真有耶 !
但這個網站外掛並不是google所開發的,為什麼他產生的也可以適用?
這代表演算法一定是有公開
(Google Authenticator 也是程式碼開源的,有興趣可以到
GitHub Google Authenticator 參考 )
所以接下來就生出了更好的解決方案了:
解決方案二:
直接實作
演算法 (我是參考
Tin Isles的javascript實作 改用c#東抄西抄來實現而已...
只要有了金鑰,透過演算法就可以得出一次性碼囉~
那金鑰哪來?其實上面所說的QRCode只是一個紀錄私密金鑰的載體而已:
程式碼參考如下:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Program
{
static void Main(string[] args)
{
Program.getGACode();
}
private static String getGACode()
{
double totalMilliseconds = DateTime.Now.ToUniversalTime().Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
double epoch = totalMilliseconds / 1000.0;
String epochHex = Utilities.Dec2Hex(Math.Floor(epoch / 30), 20).PadLeft(16, '0');
HMACSHA1 hmac = new HMACSHA1();
String hexKey = Utilities.Base32ToDec("SDFEDS6PE46G5HJ2"); //這邊的字串須提供金鑰
hmac.Key = Utilities.HexToByte(hexKey);
byte[] resultArray = hmac.ComputeHash(Utilities.HexToByte(epochHex));
String resultText = Utilities.ByteToString(resultArray);
int offset = Convert.ToInt32(Utilities.Hex2Dec(resultText.Substring(resultText.Length - 1)));
String otp = (Utilities.Hex2Dec(resultText.Substring(offset * 2, 8)) & Utilities.Hex2Dec("7fffffff")).ToString();
otp = (otp).Substring(otp.Length - 6, 6);
return otp;
}
}
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
class Utilities
{
public static String Dec2Hex(double value, int maxDecimals)
{
string result = string.Empty;
if (value < 0)
{
result += "-";
value = -value;
}
if (value > ulong.MaxValue)
{
result += double.PositiveInfinity.ToString();
return result;
}
ulong trunc = (ulong)value;
result += trunc.ToString("X");
value -= trunc;
if (value == 0)
{
return result;
}
result += ".";
byte hexdigit;
while ((value != 0) && (maxDecimals != 0))
{
value *= 16;
hexdigit = (byte)value;
result += hexdigit.ToString("X");
value -= hexdigit;
maxDecimals--;
}
return result;
}
public static String Base32ToDec(String base32)
{
String base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
String bits = "";
String hex = "";
for (var i = 0; i < base32.Length; i++)
{
int val = base32chars.IndexOf(base32.Substring(i, 1).ToUpper());
bits += Convert.ToString(val, 2).PadLeft(5, '0');
}
for (var i = 0; i + 4 <= bits.Length; i += 4)
{
String chunk = bits.Substring(i, 4);
hex = hex + Convert.ToString(Convert.ToInt32(chunk, 2), 16);
}
return hex;
}
public static byte[] HexToByte(string hexString)
{
byte[] byteOUT = new byte[hexString.Length / 2];
for (int i = 0; i < hexString.Length; i = i + 2)
{
byteOUT[i / 2] = Convert.ToByte(hexString.Substring(i, 2), 16);
}
return byteOUT;
}
public static string ByteToString(byte[] buff)
{
string sbinary = "";
for (int i = 0; i < buff.Length; i++)
{
sbinary += buff[i].ToString("X2"); // hex format
}
return (sbinary);
}
public static int Hex2Dec(String s)
{
return Convert.ToInt32(s, 16);
}
}
參考資料: