歡迎光臨
每天分享高質量文章

TOTP 介紹及基於 C# 的簡單實現

TOTP 介紹及基於 C# 的簡單實現

Intro

TOTP 是基於時間的一次性密碼生成演演算法,它由 RFC 6238 定義。和基於事件的一次性密碼生成演演算法不同 HOTP,TOTP 是基於時間的,它和 HOTP 具有如下關係:

  1. TOTP = HOTP(K, T)
  2. HOTP(K,C) = Truncate(HMAC-SHA-1(K,C))

其中:

  • T:T = (Current Unix time – T0) / X, T0 = 0,X = 30
  • K:客戶端和服務端的共享金鑰,不同的客戶端的金鑰各不相同。
  • HOTP:該演演算法請參考 RFC,也可參考 理解 HMAC-Based One-Time Password Algorithm

TOTP 演演算法是基於 HOTP 的,對於 HOTP 演演算法來說,HOTP 的輸入一致時始終輸出相同的值,而 TOTP 是基於時間來算出來的一個值,可以在一段時間內(官方推薦是30s)保證這個值是固定以實現,在一段時間內始終是同一個值,以此來達到基於時間的一次性密碼生成演演算法,使用下來整體還不錯,有個小問題,如果需要實現一個密碼只能驗證一次需要自己在業務邏輯裡實現,只能自己實現,TOTP 只負責生成和驗證。

C# 實現 TOTP

實現程式碼

  1. using System;
  2. using System.Security.Cryptography;
  3. using System.Text;
  4.  
  5. namespace WeihanLi.Totp
  6. {
  7. public class Totp
  8. {
  9. private readonly OtpHashAlgorithm _hashAlgorithm;
  10. private readonly int _codeSize;
  11.  
  12. public Totp() : this(OtpHashAlgorithm.SHA1, 6)
  13. {
  14. }
  15.  
  16. public Totp(OtpHashAlgorithm otpHashAlgorithm, int codeSize)
  17. {
  18. _hashAlgorithm = otpHashAlgorithm;
  19.  
  20. // valid input parameter
  21. if (codeSize <= 0 || codeSize > 10)
  22. {
  23. throw new ArgumentOutOfRangeException(nameof(codeSize), codeSize, "length must between 1 and 9");
  24. }
  25. _codeSize = codeSize;
  26. }
  27.  
  28. private static readonly Encoding Encoding = new UTF8Encoding(false, true);
  29.  
  30. public virtual string Compute(string securityToken) => Compute(Encoding.GetBytes(securityToken));
  31.  
  32. public virtual string Compute(byte[] securityToken) => Compute(securityToken, GetCurrentTimeStepNumber());
  33.  
  34. private string Compute(byte[] securityToken, long counter)
  35. {
  36. HMAC hmac;
  37. switch (_hashAlgorithm)
  38. {
  39. case OtpHashAlgorithm.SHA1:
  40. hmac = new HMACSHA1(securityToken);
  41. break;
  42.  
  43. case OtpHashAlgorithm.SHA256:
  44. hmac = new HMACSHA256(securityToken);
  45. break;
  46.  
  47. case OtpHashAlgorithm.SHA512:
  48. hmac = new HMACSHA512(securityToken);
  49. break;
  50.  
  51. default:
  52. throw new ArgumentOutOfRangeException(nameof(_hashAlgorithm), _hashAlgorithm, null);
  53. }
  54.  
  55. using (hmac)
  56. {
  57. var stepBytes = BitConverter.GetBytes(counter);
  58. if (BitConverter.IsLittleEndian)
  59. {
  60. Array.Reverse(stepBytes); // need BigEndian
  61. }
  62. // See https://tools.ietf.org/html/rfc4226
  63. var hashResult = hmac.ComputeHash(stepBytes);
  64.  
  65. var offset = hashResult[hashResult.Length - 1] & 0xf;
  66. var p = "";
  67. for (var i = 0; i < 4; i++)
  68. {
  69. p += hashResult[offset + i].ToString("X2");
  70. }
  71. var num = Convert.ToInt64(p, 16) & 0x7FFFFFFF;
  72.  
  73. //var binaryCode = (hashResult[offset] & 0x7f) << 24
  74. // | (hashResult[offset + 1] & 0xff) << 16
  75. // | (hashResult[offset + 2] & 0xff) << 8
  76. // | (hashResult[offset + 3] & 0xff);
  77.  
  78. return (num % (int)Math.Pow(10, _codeSize)).ToString();
  79. }
  80. }
  81.  
  82. public virtual bool Verify(string securityToken, string code) => Verify(Encoding.GetBytes(securityToken), code);
  83.  
  84. public virtual bool Verify(string securityToken, string code, TimeSpan timeToleration) => Verify(Encoding.GetBytes(securityToken), code, timeToleration);
  85.  
  86. public virtual bool Verify(byte[] securityToken, string code) => Verify(securityToken, code, TimeSpan.Zero);
  87.  
  88. public virtual bool Verify(byte[] securityToken, string code, TimeSpan timeToleration)
  89. {
  90. var futureStep = (int)(timeToleration.TotalSeconds / 30);
  91. var step = GetCurrentTimeStepNumber();
  92. for (int i = -futureStep; i <= futureStep; i++)
  93. {
  94. if (step + i < 0)
  95. {
  96. continue;
  97. }
  98. var totp = Compute(securityToken, step + i);
  99. if (totp == code)
  100. {
  101. return true;
  102. }
  103. }
  104. return false;
  105. }
  106.  
  107. private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
  108.  
  109. ///
  110. /// timestep
  111. /// 30s(Recommend)
  112. ///
  • private static readonly long _timeStepTicks = TimeSpan.TicksPerSecond * 30;
  •  
  • // More info: https://tools.ietf.org/html/rfc6238#section-4
  • private static long GetCurrentTimeStepNumber()
  • {
  • var delta = DateTime.UtcNow - _unixEpoch;
  • return delta.Ticks / _timeStepTicks;
  • }
  • }
  • }

使用方式:

  1. var otp = new Totp(OtpHashAlgorithm.SHA1, 4); // 使用 SHA1演演算法,輸出4位
  2. var secretKey = "12345678901234567890";
  3. var output = otp.Compute(secretKey);
  4. Console.WriteLine($"output: {output}");
  5. Thread.Sleep(1000 * 30);
  6. var verifyResult = otp.Verify(secretKey, output); // 使用預設的驗證方式,30s內有效
  7. Console.WriteLine($"Verify result: {verifyResult}");
  8. verifyResult = otp.Verify(secretKey, output, TimeSpan.FromSeconds(60)); // 指定可容忍的時間差,60s內有效
  9. Console.WriteLine($"Verify result: {verifyResult}");

輸出示例:

Reference

  • https://tools.ietf.org/html/rfc4226
  • https://tools.ietf.org/html/rfc6238
  • http://wsfdl.com/algorithm/2016/04/05/%E7%90%86%E8%A7%A3HOTP.html
  • http://wsfdl.com/algorithm/2016/04/14/%E7%90%86%E8%A7%A3TOTP.html
  • https://www.cnblogs.com/voipman/p/6216328.html

 


    已同步到看一看
    贊(0)

    分享創造快樂