Skip to main content

Encryption in C#: AES, RSA & Hashing Complete Guide

Learn encryption in C# with AES, RSA, and hashing. Complete developer guide with runnable code examples, best practices, and security pitfalls. Start coding securely today.

Whether you're storing user passwords, protecting API keys, or transmitting sensitive data, encryption in C# is a skill every .NET developer needs in 2026. The good news? The .NET base class library ships with battle-tested cryptography primitives in the System.Security.Cryptography namespace, so you rarely need third-party packages. The bad news? It's surprisingly easy to use them incorrectly and ship insecure code that looks safe.

This complete developer guide covers the three pillars of applied cryptography — AES encryption in C# (symmetric), RSA encryption in C# (asymmetric), and C# hashing (one-way) — with practical, runnable code examples. More importantly, we'll explain why each technique works the way it does, so you can make secure decisions instead of copy-pasting Stack Overflow snippets that leak data.

Encryption in C#: The Three Categories You Must Know

Before writing a single line of code, you need to understand that "encryption" is an umbrella term covering distinct tools for distinct jobs. Choosing the wrong one is the most common — and most dangerous — mistake in C# cryptography.

  • Symmetric encryption (AES): One shared secret key encrypts and decrypts. It's extremely fast and ideal for bulk data — files, database columns, message payloads.
  • Asymmetric encryption (RSA): A public/private key pair. The public key encrypts; only the private key decrypts. Slower, but solves the key-distribution problem and enables digital signatures.
  • Hashing (SHA-256, PBKDF2): A one-way fingerprint. You can never reverse it back to the original. Used for passwords and integrity checks — not encryption at all, despite often being lumped in.

A simple rule of thumb: if you ever need the original data back, you want encryption (AES or RSA). If you only need to verify data without recovering it — like checking a password — you want hashing.

AES Encryption in C#: The Workhorse of Symmetric Cryptography

AES (Advanced Encryption Standard) is the most widely used symmetric algorithm in the world, trusted by governments and banks. In modern .NET, the recommended way to do AES encryption in C# is with AES-GCM (Galois/Counter Mode), which provides authenticated encryption — it both encrypts your data and guarantees nobody tampered with it.

The older CipherMode.CBC approach you'll find in many tutorials encrypts data but does not detect tampering, leaving you open to padding-oracle and bit-flipping attacks. Prefer GCM whenever your target framework supports it (.NET Core 3.0+ and .NET 5/6/7/8/9).

using System;
using System.Security.Cryptography;
using System.Text;

public static class AesGcmEncryptor
{
    // Returns nonce + tag + ciphertext packed into one Base64 string.
    public static string Encrypt(string plainText, byte[] key)
    {
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);

        // A 12-byte nonce is the recommended size for AES-GCM.
        // CRITICAL: never reuse a nonce with the same key.
        byte[] nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
        byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
        byte[] cipherBytes = new byte[plainBytes.Length];

        using var aes = new AesGcm(key, tag.Length);
        aes.Encrypt(nonce, plainBytes, cipherBytes, tag);

        // Pack: [nonce][tag][ciphertext] so decryption has everything it needs.
        byte[] result = new byte[nonce.Length + tag.Length + cipherBytes.Length];
        Buffer.BlockCopy(nonce, 0, result, 0, nonce.Length);
        Buffer.BlockCopy(tag, 0, result, nonce.Length, tag.Length);
        Buffer.BlockCopy(cipherBytes, 0, result, nonce.Length + tag.Length, cipherBytes.Length);

        return Convert.ToBase64String(result);
    }

    public static string Decrypt(string encryptedBase64, byte[] key)
    {
        byte[] data = Convert.FromBase64String(encryptedBase64);

        int nonceSize = AesGcm.NonceByteSizes.MaxSize;
        int tagSize = AesGcm.TagByteSizes.MaxSize;

        var nonce = data.AsSpan(0, nonceSize);
        var tag = data.AsSpan(nonceSize, tagSize);
        var cipherBytes = data.AsSpan(nonceSize + tagSize);
        byte[] plainBytes = new byte[cipherBytes.Length];

        using var aes = new AesGcm(key, tagSize);
        // Throws CryptographicException if the data was tampered with.
        aes.Decrypt(nonce, cipherBytes, tag, plainBytes);

        return Encoding.UTF8.GetString(plainBytes);
    }
}

// Usage
byte[] key = RandomNumberGenerator.GetBytes(32); // 256-bit key
string secret = AesGcmEncryptor.Encrypt("Sensitive customer data", key);
Console.WriteLine(secret);
Console.WriteLine(AesGcmEncryptor.Decrypt(secret, key));

Why the nonce and key generation matter so much

Notice we use RandomNumberGenerator.GetBytes for both the key and the nonce. This is non-negotiable. Never use System.Random for cryptographic values — it's predictable and seeded from the clock, which means an attacker can reproduce your "random" keys.

The single most catastrophic mistake in AES-GCM is nonce reuse. Encrypting two different messages with the same key and the same nonce completely breaks the security guarantees and can leak the key. Always generate a fresh random nonce for every single encryption operation, as the code above does.

RSA Encryption in C#: Solving the Key Distribution Problem

AES is fast, but it has a chicken-and-egg problem: how do you securely share the secret key with the other party in the first place? That's where RSA encryption in C# shines. With RSA, you freely publish your public key. Anyone can encrypt a message to you, but only your secret private key can decrypt it.

Here's a clean, modern implementation using OAEP padding with SHA-256 — the secure default. Avoid the legacy RSAEncryptionPadding.Pkcs1 padding for new code, as it's vulnerable to certain oracle attacks.

using System;
using System.Security.Cryptography;
using System.Text;

public static class RsaEncryptor
{
    public static (string publicKey, string privateKey) GenerateKeys()
    {
        using var rsa = RSA.Create(2048); // 2048-bit minimum; 3072+ for long-term data
        string publicKey = Convert.ToBase64String(rsa.ExportRSAPublicKey());
        string privateKey = Convert.ToBase64String(rsa.ExportRSAPrivateKey());
        return (publicKey, privateKey);
    }

    public static string Encrypt(string plainText, string publicKeyBase64)
    {
        using var rsa = RSA.Create();
        rsa.ImportRSAPublicKey(Convert.FromBase64String(publicKeyBase64), out _);

        byte[] encrypted = rsa.Encrypt(
            Encoding.UTF8.GetBytes(plainText),
            RSAEncryptionPadding.OaepSHA256);

        return Convert.ToBase64String(encrypted);
    }

    public static string Decrypt(string cipherBase64, string privateKeyBase64)
    {
        using var rsa = RSA.Create();
        rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateKeyBase64), out _);

        byte[] decrypted = rsa.Decrypt(
            Convert.FromBase64String(cipherBase64),
            RSAEncryptionPadding.OaepSHA256);

        return Encoding.UTF8.GetString(decrypted);
    }
}

// Usage
var (pub, priv) = RsaEncryptor.GenerateKeys();
string cipher = RsaEncryptor.Encrypt("Hello from RSA", pub);
Console.WriteLine(RsaEncryptor.Decrypt(cipher, priv));

The hybrid encryption pattern (what the pros actually do)

RSA has a hard limit: a 2048-bit key can only encrypt about 190 bytes with OAEP-SHA256. You cannot RSA-encrypt a large file. The industry-standard solution is hybrid encryption, which is exactly how HTTPS/TLS works:

  • Generate a random AES key and encrypt your large payload with fast AES-GCM.
  • Encrypt that small AES key with the recipient's RSA public key.
  • Send both. The recipient uses their RSA private key to recover the AES key, then decrypts the payload.

This gives you the speed of AES and the key-distribution magic of RSA — the best of both worlds.

C# Hashing: Passwords and Data Integrity Done Right

Now for the technique developers most often get wrong. C# hashing is one-way: SHA256 turns input into a fixed fingerprint that can't be reversed. It's perfect for verifying file integrity:

using System;
using System.Security.Cryptography;
using System.Text;

string ComputeSha256(string input)
{
    byte[] bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
    return Convert.ToHexString(bytes).ToLowerInvariant();
}

Console.WriteLine(ComputeSha256("verify this file's integrity"));

Never use plain SHA-256 for passwords

Here is the most important security lesson in this entire guide: do not hash passwords with plain SHA-256 or MD5. They are designed to be fast, which means an attacker with a stolen database can try billions of guesses per second using a GPU. For passwords you need a deliberately slow, salted algorithm like PBKDF2, bcrypt, or Argon2.

.NET ships PBKDF2 built in via Rfc2898DeriveBytes. Always use a unique random salt per password and a high iteration count:

using System;
using System.Security.Cryptography;

public static class PasswordHasher
{
    private const int SaltSize = 16;       // 128-bit salt
    private const int HashSize = 32;       // 256-bit hash
    private const int Iterations = 600_000; // OWASP 2023+ guidance for PBKDF2-SHA256

    public static string HashPassword(string password)
    {
        byte[] salt = RandomNumberGenerator.GetBytes(SaltSize);
        byte[] hash = Rfc2898DeriveBytes.Pbkdf2(
            password, salt, Iterations, HashAlgorithmName.SHA256, HashSize);

        return $"{Iterations}.{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
    }

    public static bool Verify(string password, string stored)
    {
        string[] parts = stored.Split('.');
        int iterations = int.Parse(parts[0]);
        byte[] salt = Convert.FromBase64String(parts[1]);
        byte[] expected = Convert.FromBase64String(parts[2]);

        byte[] actual = Rfc2898DeriveBytes.Pbkdf2(
            password, salt, iterations, HashAlgorithmName.SHA256, expected.Length);

        // Constant-time comparison prevents timing attacks.
        return CryptographicOperations.FixedTimeEquals(actual, expected);
    }
}

// Usage
string stored = PasswordHasher.HashPassword("P@ssw0rd!");
Console.WriteLine(PasswordHasher.Verify("P@ssw0rd!", stored)); // True
Console.WriteLine(PasswordHasher.Verify("wrong", stored));     // False

Notice CryptographicOperations.FixedTimeEquals. A naive == comparison can leak information through how long it takes to return, enabling a timing attack. The constant-time comparison closes that hole.

Best Practices and Common Pitfalls in C# Cryptography

Following these rules will put you ahead of the majority of production .NET code:

  • Use authenticated encryption (AES-GCM). If you only encrypt without authenticating, attackers can silently tamper with ciphertext.
  • Never hard-code keys. Keys in source control or appsettings.json are a breach waiting to happen. Use a secrets manager — Azure Key Vault, AWS KMS, or .NET user-secrets in development.
  • Always use RandomNumberGenerator for keys, salts, and nonces — never System.Random.
  • Don't invent your own algorithms. Stick to AES, RSA, and vetted KDFs. Custom "encryption" is almost always trivially breakable.
  • Rotate keys and store an algorithm/version identifier alongside ciphertext so you can migrate later without breaking old data.
  • Salt every password individually. A shared salt defeats the purpose and re-enables rainbow tables.
  • Dispose of crypto objects with using to release native resources promptly.

Conclusion: Key Takeaways for Encryption in C#

Mastering encryption in C# comes down to picking the right tool for the job and respecting the secure defaults the .NET platform gives you. Let's recap the essentials:

  • AES-GCM is your default for fast, authenticated symmetric encryption of files and bulk data.
  • RSA with OAEP-SHA256 solves key distribution and powers the hybrid encryption pattern for large payloads.
  • PBKDF2 (or bcrypt/Argon2) — never plain SHA-256 — is mandatory for password storage, with a unique salt and high iteration count.
  • Generate all keys, salts, and nonces with RandomNumberGenerator, never reuse a GCM nonce, and store secrets outside your codebase.

Cryptography is unforgiving — small mistakes create big breaches. But by leaning on .NET's built-in primitives and the patterns in this guide, you can ship secure, production-grade C# code with confidence. Bookmark this guide, drop the code samples into your next project, and you'll have AES, RSA, and hashing covered the right way. Happy (secure) coding!

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...