123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132 |
- using System.Buffers.Binary;
- using System.Collections.Concurrent;
- using System.Security.Cryptography;
- namespace PCC.App.Security;
- public class TimestampNonceManager : IDisposable
- {
- public static int TimestampLength => sizeof(long);
- private readonly ConcurrentDictionary<string, DateTime> _holds = new();
- private readonly TimeSpan _expiration;
- private readonly TimeSpan _maxTimeSkew;
- private readonly Timer _cleanupTimer;
- private readonly int _randomLength;
- public int NonceLength { get; }
- private bool _disposed;
- public TimestampNonceManager(int randomLength, TimeSpan expiration, TimeSpan maxTimeSkew)
- {
- NonceLength = TimestampLength + randomLength;
- _randomLength = randomLength;
- _expiration = expiration;
- _maxTimeSkew = maxTimeSkew;
- _cleanupTimer = new Timer(CleanupTimerCallback, null, expiration, expiration);
- }
-
- public (ReadOnlyMemory<byte> nonce, DateTimeOffset timestamp) NewNonce()
- {
- ThrowIfDisposed();
- var nonce = new byte[NonceLength];
-
- var timestamp = DateTimeOffset.UtcNow;
- BinaryPrimitives.TryWriteInt64LittleEndian(nonce, timestamp.ToUnixTimeMilliseconds());
-
- RandomNumberGenerator.Fill(nonce.AsSpan(TimestampLength));
- return (new ReadOnlyMemory<byte>(nonce), timestamp);
- }
-
- public (ReadOnlyMemory<byte> payloadAndNonce, DateTimeOffset timestamp) NewNonceWithPayload(ReadOnlyMemory<byte> payload)
- {
- ThrowIfDisposed();
- var payloadAndNonce = new byte[NonceLength + payload.Length];
-
- var timestamp = DateTimeOffset.UtcNow;
- BinaryPrimitives.TryWriteInt64LittleEndian(payloadAndNonce, timestamp.ToUnixTimeMilliseconds());
-
- RandomNumberGenerator.Fill(payloadAndNonce.AsSpan(TimestampLength, _randomLength));
-
- payload.Span.CopyTo(payloadAndNonce.AsSpan(NonceLength));
- return (new ReadOnlyMemory<byte>(payloadAndNonce), timestamp);
- }
- public TimestampNonceResult CheckValid(ReadOnlyMemory<byte> timestampNonce, out DateTimeOffset timestamp)
- {
- ThrowIfDisposed();
- if (timestampNonce.Length != NonceLength) throw new ArgumentException("invalid length", nameof(timestampNonce));
- timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BinaryPrimitives.ReadInt64LittleEndian(timestampNonce.Span[..TimestampLength]));
-
- if (Math.Abs((DateTimeOffset.UtcNow - timestamp).TotalMilliseconds) > _maxTimeSkew.TotalMilliseconds) return TimestampNonceResult.TimestampSkew;
-
- var nonce = Convert.ToHexString(timestampNonce[TimestampLength..].Span);
-
- return _holds.TryAdd(nonce, DateTime.UtcNow)
- ? TimestampNonceResult.OK
- : TimestampNonceResult.ReplayAttackDetected;
- }
-
- public (TimestampNonceResult, DateTimeOffset timestamp, ReadOnlyMemory<byte> payload) CheckValidAndExtractPayload(ReadOnlyMemory<byte> payloadAndNonce)
- {
- ThrowIfDisposed();
- if (payloadAndNonce.Length < NonceLength) throw new ArgumentException("invalid length", nameof(payloadAndNonce));
-
- var timestamp = DateTimeOffset.FromUnixTimeMilliseconds(BinaryPrimitives.ReadInt64LittleEndian(payloadAndNonce.Span[..TimestampLength]));
- var nonce = Convert.ToHexString(payloadAndNonce.Slice(TimestampLength, _randomLength).Span);
- var payload = payloadAndNonce[NonceLength..];
-
- if (Math.Abs((DateTimeOffset.UtcNow - timestamp).TotalMilliseconds) > _maxTimeSkew.TotalMilliseconds) return (TimestampNonceResult.TimestampSkew, timestamp, payload);
-
- return (_holds.TryAdd(nonce, DateTime.UtcNow) ? TimestampNonceResult.OK : TimestampNonceResult.ReplayAttackDetected, timestamp, payload);
- }
- private void CleanupTimerCallback(object? state)
- {
- var now = DateTime.UtcNow;
- foreach (var item in _holds.Where(p => now - p.Value >= _expiration))
- {
-
- _holds.TryRemove(item.Key, out _);
- }
- }
- private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
- public void Dispose()
- {
- if (_disposed) return;
- _cleanupTimer.Dispose();
- _disposed = true;
- GC.SuppressFinalize(this);
- }
- }
- public enum TimestampNonceResult
- {
- OK = 0,
- TimestampSkew,
- ReplayAttackDetected,
- }
|