兩步驟驗證之相關知識與實作

兩步驟驗證在現在的網站來說,似乎已經慢慢開始流行,許多大型的網站都已經支援這個標準。兩步驟驗證的名稱有很多,例如:兩階驗證、兩階段驗證、動態密碼、一次性密碼、2FA(Two Factor Authentication)、OTP(One Time Password)。一開始我也對這個概念很無知,經過不斷的爬文閱讀之後,才知道所有的概念源自於RFC 6238 TOTP: Time-Based One-Time Password Algorithm與RFC 4226 HOTP: An HMAC-Based One-Time Password Algorithm這兩個標準。而現代的大型網站之兩步驟驗證的終端工具,也是實作這個標準而成。

本來以為這裡面的實作方式有許多的複雜運算、資料庫儲存以及防禦被竊的機制,但經過啃讀RFC後發現,最終就是網站會發給使用者一組ScrectKey,然後使用者要好好的保管這一組密鑰,一旦密鑰被流出去,那麼每個人都可以利用RFC的標準產生跟你一模一樣的動態密碼。我知道這個結論的當下也是很震驚的(如此簡單!),但是仔細再想一想也沒錯。兩步驟驗證就像另外一把會不斷變換樣貌的鑰匙,當這把鑰匙被偷走時,就算你再怎麼樣變換也沒有用吧!總而言之,兩步驟驗證只能夠保護你在公共場所登入時,如果有駭客在終端sniffer你的網路連線,那麼這組密碼他就算拿到了也無法再搞怪了(30秒內失效)。但是兩步驟驗證沒有辦法保護在你產生密鑰的當下,你的密鑰被人家監聽(拿走)了。

兩步驟驗證(2FA, Two Factor Authentication)的流程簡述如下

  1. 系統產生一組Key給使用者,並將這組Key存在資料庫內。
  2. 使用者從介面上,得到一個「otpauth://totp/{0}?secret={1}」的QRCode。其中的{0}是你的系統名稱(可亂取),{1}是一個Base32Encode過的Key值。
  3. 使用者用Google或Microsoft的驗證器App照這組QRcode,會開始得到一個每30秒跳動一次的動態密碼。
  4. 使用者回到系統登入頁面,系統會先通過常態性的「帳號密碼」驗證,來得知使用者是誰。
  5. 得知是哪個使用者後,到資料庫取出該使用者的Key。
  6. 進行RFC 6238 TOTP運算
  7. -取出UTC制之System.DateTime(1970, 1, 1, 0, 0, 0, System.DateTimeKind.Utc)~System.DateTime.UtcNow,取總秒數再去除30秒,即為counter量。
  8. 進行RFC 4226 HOTP運算
  9. -基礎運算System.Security.Cryptography.HMACSHA1(bytKey[]).ComputeHash(bytCounter[])。
  10. -移位運算後,取餘數6個數字(不足者左方補0),得到答案result。
  11. 將使用者看App後所輸入的密碼,與result進行比對,正確的話就是通過了!
  12. ※ 優化你的系統
  13. 由於不可能所有的設備的時間,都與你的伺服器時間完全一致。因此如果要優化使用者的體驗,可以將count進行+1, 0, -1的運算,來得到三組六位數的密碼,再來比對,將會提高命中率,也不會讓使用者覺得輸入的膽顫心驚。
  14. 考慮到二步驟驗證在實體運行時,使用者可能在外面的網路輸入且被監聽,因此程式在正式運行時,應該考量有可能正在被監聽,因此可以加入當使用者輸入正確的密碼並送出後,系統會自動將該組密碼設為Disable,如此一來即使竊聽者仿造一組一模一樣的密碼,並在30秒或更長的時間內送出,依然會被系統判定成false,無法通過驗證。

Console Main程序程式碼如下:

static void Main(string[] args)
{
	string cTemp = System.Guid.NewGuid().ToString("N").ToUpper();
	OneTimePassword oTemp = new OneTimePassword(cTemp);
	Console.WriteLine("--------------------");
	Console.WriteLine("  OTP Application   ");
	Console.WriteLine("--------------------");
	Console.WriteLine("Make QRCode below and add to Google authenticator.");
	Console.WriteLine(oTemp.cQRCode);
	Console.WriteLine("--------------------");
	Console.WriteLine("Choice what do you want to do?");
	Console.WriteLine("[1] Watch real time OTP code every 10 sec.");
	Console.WriteLine("[2] Input your OTP code to verify.");
	Console.Write("Your choice: "); string cInput = Console.ReadLine();
	switch (cInput)
	{
		case "1":
			Console.WriteLine("");	//在這裡顯示
			Console.WriteLine("* Press Ctrl+C to Stop.");
			int iTemp = Console.CursorTop - 2;
			while (true)
			{
				Console.SetCursorPosition(0, iTemp);
				Console.Write(string.Format("OTP code >> {0}", oTemp.GetPassword()));
				System.Threading.Thread.Sleep(TimeSpan.FromSeconds(1));
			}
		default:
			while (true)
			{
				Console.Write("Input right OTP code: "); string cAnswer = Console.ReadLine();
				string cReference = string.Format("OTP code history: {0} {1} {2}",
					oTemp.GetPassword(-1),
					oTemp.GetPassword(0),
					oTemp.GetPassword(1));
				if (cReference.IndexOf(cAnswer) != -1)
				{
					Console.WriteLine("OTP code RIGHT."); break; 
				}	else
				{
					Console.WriteLine("OTP code ERROR. Try it again.");
					Console.WriteLine(cReference);
				}
			}
			break;
	}
	Console.Read();
}

Console程式運行時的畫面a(每秒更新最正確的OTP code)

Console程式運行時的畫面b(讓使用者輸入OTP code並判定是否正確)

OTP demo EXE download: (with .NET Framework 4.5.1)

C# OTP Implement & Demo

OneTimePassword GoogleAuthenticator MicrosoftAuthenticator 二階段驗證 二階驗證