2017年11月2日 星期四

以C#實作Google Authenticator 產生一次性驗證碼

以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只是一個紀錄私密金鑰的載體而已:



程式碼參考如下:
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;
}
}
view raw Program.cs hosted with ❤ by GitHub
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);
}
}
view raw Utilities.cs hosted with ❤ by GitHub

參考資料: