• Stars
    star
    115
  • Rank 304,439 (Top 7 %)
  • Language
    C#
  • License
    MIT License
  • Created over 3 years ago
  • Updated 4 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Modern, fast, safe, cryptographically strong .NET replacement for Random and RandomNumberGenerator.

CryptoRandom GitHub Actions NuGet

by Stan Drapkin

CryptoRandom : Random

  • .NET Random done right
  • [Fast], [Thread-safe], [Cryptographically strong]: choose 3
  • Subclasses and replaces System.Random
  • Also replaces System.Security.Cryptography.RandomNumberGenerator (link)
  • CryptoRandom is (unlike System.Random):
    • Fast (much faster than Random or RandomNumberGenerator)
    • Thread-safe (all APIs)
    • Cryptographically strong (seeded or unseeded)
  • Implements Fast-Key-Erasure RNG for seeded CryptoRandom
  • Produces the same sequence of seeded CryptoRandom values on all .NET versions (unlike Random)
  • Wraps RandomNumberGenerator for unseeded (with additional smarts)
  • Provides backtracking resistance: internal state cannot be used to recover the output
  • Achieves ~1.3cpb (cycles-per-byte) performance, similar to AES-NI
  • Scales per-CPU/Core
  • Example: CryptoRandom.NextGuid() vs. Guid.NewGuid() [BenchmarkDotNet]:
    • 4~5x faster on Windows-x64
    • 5~30x faster on Linux-x64
    • 4~5x faster on Linux-ARM64 (AWS Graviton-2)
  • Built for .NET 5.0+ and 6.0+
  • Extensive test coverage & correctness validation (110+ tests)
    • CI runs on Linux-latest & Windows-latest

CryptoRandom API:

  • All APIs of System.Random (link)
  • CryptoRandom.Shared static shared instance for convenience (thread-safe of course)
  • Seeded constructors:
    • CryptoRandom(ReadOnlySpan<byte> seedKey)
    • CryptoRandom(int Seed) (just like seeded Random ctor)
  • byte[] NextBytes(int count)
  • Guid NextGuid()
    • 5x (500%) faster than Guid.NewGuid() on Windows
    • 15x (1500%) faster than Guid.NewGuid() on Linux
    • 128 random bits, instead of 122
  • Guid SqlServerGuid()
    • Returns new Guid well-suited to be used as a SQL-Server clustered key
    • Guid structure is [8 random bytes][8 bytes of SQL-Server-ordered DateTime.UtcNow]
    • Each Guid should be sequential within 100-nanoseconds UtcNow precision limits
    • 64-bit entropy for reasonable unguessability and protection against online brute-force attacks
    • ~15% faster than Guid.NewGuid()
  • long NextInt64()
  • long NextInt64(long maxValue)
  • long NextInt64(long minValue, long maxValue)
  • Random struct 's (make sure you know what you're doing):
    • void Next<T>(ref T @struct) where T : unmanaged
    • T Next<T>() where T : unmanaged

Utils API (SecurityDriven.Core.Utils):

  • static Span<byte> AsSpan<T>(ref T @struct) where T : unmanaged
    • Casts unmanaged struct T as equivalent Span<byte>
  • static ref T AsStruct<T>(Span<byte> span) where T : unmanaged
    • Casts Span<byte> as equivalent unmanaged struct T
  • int StructSizer<T>.Size
    • Returns byte-size of struct T

Quick benchmark:

using SecurityDriven.Core;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

Console.WriteLine($"[{RuntimeInformation.FrameworkDescription}]");
Stopwatch sw1 = new(), sw2 = new();
const long ITER = 100_000_000, REPS = 5;

for (int i = 0; i++ < REPS;)
{
    sw1.Restart();
    Parallel.For(0, ITER, static i => CryptoRandom.Shared.NextGuid());
    sw1.Stop();
    Console.WriteLine($"{sw1.Elapsed} cryptoRandom.NextGuid()");

    sw2.Restart();
    Parallel.For(0, ITER, static i => Guid.NewGuid());
    sw2.Stop();

    var ratio = sw2.Elapsed / sw1.Elapsed;
    Console.WriteLine($"{sw2.Elapsed} Guid.NewGuid() [{ratio:N2}x slower]");
}
Output:
[.NET 6.0.0-rc.1.21451.13]
00:00:00.5486513 cryptoRandom.NextGuid()
00:00:02.0587652 Guid.NewGuid() [3.75x slower]
00:00:00.4117180 cryptoRandom.NextGuid()
00:00:02.0485556 Guid.NewGuid() [4.98x slower]
00:00:00.4103378 cryptoRandom.NextGuid()
00:00:02.0534771 Guid.NewGuid() [5.00x slower]
00:00:00.4100701 cryptoRandom.NextGuid()
00:00:02.0823213 Guid.NewGuid() [5.08x slower]
00:00:00.4017192 cryptoRandom.NextGuid()
00:00:02.0488105 Guid.NewGuid() [5.10x slower]

What's wrong with Random and RandomNumberGenerator?

  • Random is slow and not thread-safe (fails miserably and silently on concurrent access)
  • Random is incorrectly implemented:
Random r = new Random(); // new CryptoRandom();
const int mod = 2;
int[] hist = new int[mod];
for (int i = 0; i < 10000000; i++)
{
	int num = r.Next(0x55555555);
	int num2 = num % 2;
	++hist[num2];
}
for (int i = 0; i < mod; i++)
	Console.WriteLine($"{i}: {hist[i]}");
// Run this on .NET 5 or below. Surprised? Now change to CryptoRandom
// Fails on .NET 6 if you use seeded "new Random(seed)"
  • Random/.NET 6 unseeded is fast (new algorithm), with a safe .Shared property, but instances are not thread-safe
  • Random/.NET 6 seeded falls back to legacy slow non-thread-safe .NET algorithm
  • Neither Random implementation aims for cryptographically-strong results
  • RandomNumberGenerator can be much faster with intelligent wrapping and more useful Random API

Throughput (single-threaded) .NET 6:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1165 (20H2/October2020Update)
Intel Core i7-10510U CPU 1.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100-preview.7.21379.14
  [Host] : .NET 6.0.0 (6.0.21.37719), X64 RyuJIT
Method BYTES Mean Error StdDev Ratio RatioSD Throughput
SystemRandom 32 7.483 μs 0.2714 μs 0.0149 μs 0.21 0.00 4,176 MB/s
SystemSharedRandom 32 13.197 μs 3.3861 μs 0.1856 μs 0.38 0.00 2,368 MB/s
SeededSystemRandom 32 261.568 μs 12.8670 μs 0.7053 μs 7.46 0.04 119 MB/s
CryptoRandom 32 35.083 μs 1.6305 μs 0.0894 μs 1.00 0.00 891 MB/s
SeededCryptoRandom 32 27.551 μs 0.9383 μs 0.0514 μs 0.79 0.00 1,134 MB/s
RNG_Fill 32 106.507 μs 14.8172 μs 0.8122 μs 3.04 0.02 293 MB/s
SystemRandom 1024 132.600 μs 4.5819 μs 0.2511 μs 0.35 0.00 7,541 MB/s
SystemSharedRandom 1024 139.345 μs 28.1093 μs 1.5408 μs 0.37 0.00 7,176 MB/s
SeededSystemRandom 1024 8,260.379 μs 265.3543 μs 14.5450 μs 21.79 0.01 121 MB/s
CryptoRandom 1024 379.137 μs 8.8802 μs 0.4868 μs 1.00 0.00 2,638 MB/s
SeededCryptoRandom 1024 320.513 μs 21.3931 μs 1.1726 μs 0.85 0.00 3,120 MB/s
RNG_Fill 1024 447.914 μs 15.4506 μs 0.8469 μs 1.18 0.00 2,233 MB/s

Throughput (single-threaded) .NET 5:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19042.1165 (20H2/October2020Update)
Intel Core i7-10510U CPU 1.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.100-preview.7.21379.14
  [Host] : .NET 5.0.9 (5.0.921.35908), X64 RyuJIT
Method BYTES Mean Error StdDev Ratio RatioSD Throughput
SystemRandom 32 252.89 μs 53.609 μs 2.938 μs 7.30 0.09 124 MB/s
SeededSystemRandom 32 259.82 μs 3.966 μs 0.217 μs 7.50 0.03 120 MB/s
CryptoRandom 32 34.66 μs 2.193 μs 0.120 μs 1.00 0.00 902 MB/s
SeededCryptoRandom 32 26.71 μs 3.703 μs 0.203 μs 0.77 0.01 1,170 MB/s
RNG_Fill 32 105.98 μs 9.724 μs 0.533 μs 3.06 0.02 295 MB/s
SystemRandom 1024 8,403.73 μs 510.750 μs 27.996 μs 22.35 0.06 119 MB/s
SeededSystemRandom 1024 8,262.47 μs 721.788 μs 39.564 μs 21.98 0.13 121 MB/s
CryptoRandom 1024 375.98 μs 8.603 μs 0.472 μs 1.00 0.00 2,660 MB/s
SeededCryptoRandom 1024 318.11 μs 219.420 μs 12.027 μs 0.85 0.03 3,144 MB/s
RNG_Fill 1024 450.76 μs 32.919 μs 1.804 μs 1.20 0.00 2,218 MB/s