TOTP 介绍及基于 C# 的简单实现

TOTP 介绍及基于 C# 的简单实现

640?wx_fmt=png

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. namespace WeihanLi.Totp

  5. {

  6. public class Totp

  7. {

  8. private readonly OtpHashAlgorithm _hashAlgorithm;

  9. private readonly int _codeSize;


  10. public Totp() : this(OtpHashAlgorithm.SHA1, 6)

  11. {

  12. }


  13. public Totp(OtpHashAlgorithm otpHashAlgorithm, int codeSize)

  14. {

  15. _hashAlgorithm = otpHashAlgorithm;


  16. // valid input parameter

  17. if (codeSize <= 0 || codeSize > 10)

  18. {

  19. throw new ArgumentOutOfRangeException(nameof(codeSize), codeSize, "length must between 1 and 9");

  20. }

  21. _codeSize = codeSize;

  22. }


  23. private static readonly Encoding Encoding = new UTF8Encoding(false, true);


  24. public virtual string Compute(string securityToken) => Compute(Encoding.GetBytes(securityToken));


  25. public virtual string Compute(byte[] securityToken) => Compute(securityToken, GetCurrentTimeStepNumber());


  26. private string Compute(byte[] securityToken, long counter)

  27. {

  28. HMAC hmac;

  29. switch (_hashAlgorithm)

  30. {

  31. case OtpHashAlgorithm.SHA1:

  32. hmac = new HMACSHA1(securityToken);

  33. break;


  34. case OtpHashAlgorithm.SHA256:

  35. hmac = new HMACSHA256(securityToken);

  36. break;


  37. case OtpHashAlgorithm.SHA512:

  38. hmac = new HMACSHA512(securityToken);

  39. break;


  40. default:

  41. throw new ArgumentOutOfRangeException(nameof(_hashAlgorithm), _hashAlgorithm, null);

  42. }


  43. using (hmac)

  44. {

  45. var stepBytes = BitConverter.GetBytes(counter);

  46. if (BitConverter.IsLittleEndian)

  47. {

  48. Array.Reverse(stepBytes); // need BigEndian

  49. }

  50. // See https://tools.ietf.org/html/rfc4226

  51. var hashResult = hmac.ComputeHash(stepBytes);


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

  53. var p = "";

  54. for (var i = 0; i < 4; i++)

  55. {

  56. p += hashResult[offset + i].ToString("X2");

  57. }

  58. var num = Convert.ToInt64(p, 16) & 0x7FFFFFFF;


  59. //var binaryCode = (hashResult[offset] & 0x7f) << 24

  60. // | (hashResult[offset + 1] & 0xff) << 16

  61. // | (hashResult[offset + 2] & 0xff) << 8

  62. // | (hashResult[offset + 3] & 0xff);


  63. return (num % (int)Math.Pow(10, _codeSize)).ToString();

  64. }

  65. }


  66. public virtual bool Verify(string securityToken, string code) => Verify(Encoding.GetBytes(securityToken), code);


  67. public virtual bool Verify(string securityToken, string code, TimeSpan timeToleration) => Verify(Encoding.GetBytes(securityToken), code, timeToleration);


  68. public virtual bool Verify(byte[] securityToken, string code) => Verify(securityToken, code, TimeSpan.Zero);


  69. public virtual bool Verify(byte[] securityToken, string code, TimeSpan timeToleration)

  70. {

  71. var futureStep = (int)(timeToleration.TotalSeconds / 30);

  72. var step = GetCurrentTimeStepNumber();

  73. for (int i = -futureStep; i <= futureStep; i++)

  74. {

  75. if (step + i < 0)

  76. {

  77. continue;

  78. }

  79. var totp = Compute(securityToken, step + i);

  80. if (totp == code)

  81. {

  82. return true;

  83. }

  84. }

  85. return false;

  86. }


  87. private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);


  88. /// <summary>

  89. /// timestep

  90. /// 30s(Recommend)

  91. /// </summary>

  92. private static readonly long _timeStepTicks = TimeSpan.TicksPerSecond * 30;


  93. // More info: https://tools.ietf.org/html/rfc6238#section-4

  94. private static long GetCurrentTimeStepNumber()

  95. {

  96. var delta = DateTime.UtcNow - _unixEpoch;

  97. return delta.Ticks / _timeStepTicks;

  98. }

  99. }

  100. }

使用方式:

 
 
  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}");

输出示例:

640?wx_fmt=png

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值