TimestampNonceManager.cs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. using System.Buffers.Binary;
  2. using System.Collections.Concurrent;
  3. using System.Security.Cryptography;
  4. namespace PCC.App.Security;
  5. public class TimestampNonceManager : IDisposable
  6. {
  7. public static int TimestampLength => sizeof(long);
  8. private readonly ConcurrentDictionary<string, DateTime> _holds = new();
  9. private readonly TimeSpan _expiration;
  10. private readonly TimeSpan _maxTimeSkew;
  11. private readonly Timer _cleanupTimer;
  12. private readonly int _randomLength;
  13. public int NonceLength { get; }
  14. private bool _disposed;
  15. public TimestampNonceManager(int randomLength, TimeSpan expiration, TimeSpan maxTimeSkew)
  16. {
  17. NonceLength = TimestampLength + randomLength;
  18. _randomLength = randomLength;
  19. _expiration = expiration;
  20. _maxTimeSkew = maxTimeSkew;
  21. _cleanupTimer = new Timer(CleanupTimerCallback, null, expiration, expiration);
  22. }
  23. /// <summary> 生成一个新的nonce </summary>
  24. public (ReadOnlyMemory<byte> nonce, DateTimeOffset timestamp) NewNonce()
  25. {
  26. ThrowIfDisposed();
  27. var nonce = new byte[NonceLength];
  28. // 把时间戳放在nonce开头
  29. var timestamp = DateTimeOffset.UtcNow;
  30. BinaryPrimitives.TryWriteInt64LittleEndian(nonce, timestamp.ToUnixTimeMilliseconds());
  31. // 剩下的字节作为随机 nonce
  32. RandomNumberGenerator.Fill(nonce.AsSpan(TimestampLength));
  33. return (new ReadOnlyMemory<byte>(nonce), timestamp);
  34. }
  35. /// <summary> 生成一个新的nonce并追加payload </summary>
  36. public (ReadOnlyMemory<byte> payloadAndNonce, DateTimeOffset timestamp) NewNonceWithPayload(ReadOnlyMemory<byte> payload)
  37. {
  38. ThrowIfDisposed();
  39. var payloadAndNonce = new byte[NonceLength + payload.Length];
  40. // 把时间戳放在nonce开头
  41. var timestamp = DateTimeOffset.UtcNow;
  42. BinaryPrimitives.TryWriteInt64LittleEndian(payloadAndNonce, timestamp.ToUnixTimeMilliseconds());
  43. // 剩下的字节作为随机 nonce
  44. RandomNumberGenerator.Fill(payloadAndNonce.AsSpan(TimestampLength, _randomLength));
  45. // 追加 payload
  46. payload.Span.CopyTo(payloadAndNonce.AsSpan(NonceLength));
  47. return (new ReadOnlyMemory<byte>(payloadAndNonce), timestamp);
  48. }
  49. public TimestampNonceResult CheckValid(ReadOnlyMemory<byte> timestampNonce, out DateTimeOffset timestamp)
  50. {
  51. ThrowIfDisposed();
  52. if (timestampNonce.Length != NonceLength) throw new ArgumentException("invalid length", nameof(timestampNonce));
  53. timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BinaryPrimitives.ReadInt64LittleEndian(timestampNonce.Span[..TimestampLength]));
  54. // 检查时间戳是否在允许的时间窗口内,超过最大时间差则拒绝
  55. if (Math.Abs((DateTimeOffset.UtcNow - timestamp).TotalMilliseconds) > _maxTimeSkew.TotalMilliseconds) return TimestampNonceResult.TimestampSkew;
  56. // 剩下的部分为真正的随机 nonce
  57. var nonce = Convert.ToHexString(timestampNonce[TimestampLength..].Span);
  58. // 如果 nonce 已存在,则为重放攻击
  59. return _holds.TryAdd(nonce, DateTime.UtcNow)
  60. ? TimestampNonceResult.OK
  61. : TimestampNonceResult.ReplayAttackDetected;
  62. }
  63. /// <summary> 验证nonce并提取payload </summary>
  64. public (TimestampNonceResult, DateTimeOffset timestamp, ReadOnlyMemory<byte> payload) CheckValidAndExtractPayload(ReadOnlyMemory<byte> payloadAndNonce)
  65. {
  66. ThrowIfDisposed();
  67. if (payloadAndNonce.Length < NonceLength) throw new ArgumentException("invalid length", nameof(payloadAndNonce));
  68. //提取时间戳、nonce、payload
  69. var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BinaryPrimitives.ReadInt64LittleEndian(payloadAndNonce.Span[..TimestampLength]));
  70. var nonce = Convert.ToHexString(payloadAndNonce.Slice(TimestampLength, _randomLength).Span);
  71. var payload = payloadAndNonce[NonceLength..];
  72. // 检查时间戳是否在允许的时间窗口内, 超过最大时间差则拒绝
  73. if (Math.Abs((DateTimeOffset.UtcNow - timestamp).TotalMilliseconds) > _maxTimeSkew.TotalMilliseconds) return (TimestampNonceResult.TimestampSkew, timestamp, payload);
  74. // 如果 nonce 已存在,则为重放攻击
  75. return (_holds.TryAdd(nonce, DateTime.UtcNow) ? TimestampNonceResult.OK : TimestampNonceResult.ReplayAttackDetected, timestamp, payload);
  76. }
  77. private void CleanupTimerCallback(object? state)
  78. {
  79. var now = DateTime.UtcNow;
  80. foreach (var item in _holds.Where(p => now - p.Value >= _expiration)) _holds.TryRemove(item.Key, out _);
  81. }
  82. private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
  83. public void Dispose()
  84. {
  85. if (_disposed) return;
  86. _cleanupTimer.Dispose();
  87. _disposed = true;
  88. GC.SuppressFinalize(this);
  89. }
  90. }
  91. public enum TimestampNonceResult
  92. {
  93. OK = 0,
  94. TimestampSkew,
  95. ReplayAttackDetected,
  96. }