Safeguard
Security Guides

C# Cryptography Best Practices in .NET

The right way to do cryptography in C#: authenticated encryption with AesGcm, secure randomness, PBKDF2 password hashing, constant-time comparison, and the legacy APIs to stop using.

Marcus Chen
AppSec Engineer
5 min read

Cryptography is unforgiving: code that compiles, runs, and passes tests can still be catastrophically insecure because the flaw is in a choice the compiler can't see — a reused nonce, a fast hash used for passwords, a System.Random token. .NET gives you strong, modern primitives in System.Security.Cryptography, but it also still ships decades of legacy APIs that look equally valid. This guide covers the choices that matter in C#, with the correct API for each and the anti-pattern it replaces. The overarching rule: use high-level, misuse-resistant primitives, and never invent your own scheme.

Use a cryptographically secure random source

The most common crypto bug in C# is generating tokens, salts, or keys with System.Random. Random is a predictable pseudo-random generator seeded from the clock — fine for shuffling a list, disastrous for anything an attacker shouldn't guess. Use RandomNumberGenerator:

// Anti-pattern
var token = new Random().Next().ToString();

// Pattern: cryptographically secure bytes
byte[] bytes = RandomNumberGenerator.GetBytes(32);
string token = Convert.ToBase64String(bytes);

RandomNumberGenerator.GetBytes (and GetInt32 for bounded integers) draws from the OS CSPRNG. Anything that acts as a secret — session tokens, password-reset codes, API keys, IVs, salts — must come from here.

Hash passwords with a slow, salted KDF

Passwords must never be stored with a general-purpose hash like SHA-256 or MD5. Fast hashes are the problem: an attacker who steals the database can try billions of guesses per second. Use a purpose-built, deliberately slow key-derivation function with a per-password salt. .NET's built-in choice is PBKDF2:

byte[] salt = RandomNumberGenerator.GetBytes(16);
byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
    password:  Encoding.UTF8.GetBytes(userPassword),
    salt:      salt,
    iterations: 210_000,
    hashAlgorithm: HashAlgorithmName.SHA256,
    outputLength: 32);
// store salt + iterations + hash together

If you're on ASP.NET Core, prefer PasswordHasher<TUser> from Identity — it wraps PBKDF2, handles salting and format versioning, and supports rehashing on upgrade. Where your threat model justifies it and a vetted library is available, memory-hard functions like Argon2 or scrypt raise the cost further; .NET doesn't ship Argon2 in the box, so use a reputable, maintained package rather than a hand-rolled implementation.

Encrypt with authenticated encryption, not bare AES-CBC

Encryption without authentication lets an attacker tamper with ciphertext undetected (the padding-oracle class of attacks against AES-CBC is the classic example). Use authenticated encryption — AesGcm — which provides confidentiality and integrity together:

byte[] key = RandomNumberGenerator.GetBytes(32);        // 256-bit key
byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
byte[] ciphertext = new byte[plaintext.Length];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];

using var aes = new AesGcm(key, tag.Length);
aes.Encrypt(nonce, plaintext, ciphertext, tag);
// transmit/store: nonce + ciphertext + tag

The one rule you must never break with GCM: never reuse a nonce with the same key. A repeated nonce breaks the scheme entirely. Generate a fresh random nonce (or use a strict counter) per message, and store it alongside the ciphertext — it's not secret.

Compare secrets in constant time

Comparing a submitted token or HMAC to the expected value with == or SequenceEqual leaks timing information: the comparison returns faster on an early mismatch, and an attacker can measure that to recover the value byte by byte. Use the constant-time helper:

bool ok = CryptographicOperations.FixedTimeEquals(
    computedHmac, providedHmac);

For message authentication over data you're not encrypting, use HMACSHA256 rather than a plain hash, and compare the result with FixedTimeEquals.

Let the platform handle TLS

Application code should not hardcode SslProtocols to a specific version. The safe default is to let the OS negotiate the highest mutually supported protocol, which keeps you current as TLS 1.0/1.1 are removed and TLS 1.3 becomes standard. Hardcoding a version is how apps get stuck on a deprecated protocol years after it should have been dropped. Validate server certificates — never disable certificate validation with a callback that returns true, a shortcut that turns up constantly in "make it work" commits and silently defeats TLS.

The legacy APIs to retire

Stop usingUse insteadWhy
System.Random for secretsRandomNumberGeneratorPredictable seed
MD5, SHA1 for securitySHA256/SHA512Collisions / weakness
SHA-256 for passwordsPBKDF2 / Argon2Fast = brute-forceable
AesManaged + CBC, no MACAesGcmUnauthenticated, padding oracles
== on tokens/HMACsFixedTimeEqualsTiming side channel
TripleDES, DES, RC2AESBroken / weak
Hardcoded SslProtocolsOS default negotiationStuck on deprecated TLS

A cryptography checklist

  • All secret material from RandomNumberGenerator
  • Passwords via PBKDF2/Argon2 with per-password salt and high work factor
  • AesGcm for encryption; unique nonce per message
  • FixedTimeEquals for all secret/tag comparisons
  • No MD5/SHA-1/DES/3DES/RC2 in security paths
  • TLS negotiated by the OS; certificate validation never disabled
  • No hand-rolled crypto — use platform primitives or vetted libraries

How Safeguard Helps

Cryptographic misuse is easy to write and hard to spot in review, so Safeguard automates the detection. Its static analysis flags the high-signal anti-patterns above — System.Random producing security tokens, MD5/SHA-1 in a hashing path, disabled certificate validation, non-constant-time secret comparisons — directly in your C# code, and Griffin AI explains why each is exploitable and what the corrected pattern is, in plain language your developers can act on. It also covers the cryptographic libraries you depend on through software composition analysis, catching known weaknesses in crypto-related NuGet packages, and confirms exposure end-to-end with dynamic testing against the deployed service. Teams comparing this to older SAST-only tooling often review the Safeguard vs Veracode page.

Start free at app.safeguard.sh/register and read the crypto rule catalog at docs.safeguard.sh.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.