Skip to content
ExponaBLOG
.NET & ASP.NET CORE · DAY 082 / 100

Data protection and encryption: best practices explained

Protect sensitive data at rest and in transit.

Expona Admin7 min read

Protecting Sensitive Data: A Foundation for Trust

Data breaches cost organizations millions in remediation, legal fees, and reputation damage. When you're running production systems handling user credentials, payment information, or regulatory data, data protection and encryption: best practices explained becomes not a nice-to-have but a critical operational requirement. We'll walk through the concrete strategies that teams use across e-commerce, SaaS, and fintech platforms to keep sensitive information safe both at rest and in transit.

Hash Passwords, Never Encrypt

This is the most common mistake we see: storing passwords in encrypted form. Encryption is reversible. If someone compromises your keys, they decrypt and own every password. Hashing is irreversible by design—you hash an input and can never recover the original. This is the only acceptable approach for passwords.

ASP.NET Core Identity includes PasswordHasher<TUser>, which uses PBKDF2 with a salted hash. Use it:

csharp
var hasher = new PasswordHasher<ApplicationUser>();
var hashedPassword = hasher.HashPassword(user, plainTextPassword);
user.PasswordHash = hashedPassword;
await _userManager.UpdateAsync(user);

// On login, verify
var result = hasher.VerifyHashedPassword(user, user.PasswordHash, submittedPassword);
if (result == PasswordVerificationResult.Success)
{
    // Grant access
}

If you're not using Identity, reach for vetted libraries only. Argon2 is state-of-the-art and available via the Konscious.Security.Cryptography NuGet package. Never roll your own hashing scheme. The temptation to "add an extra layer" by encrypting a hash is also unnecessary—hashing alone is sufficient and correct.

Store the hash in your database. On login, hash the submitted password and compare the hashes. This way, even a full database dump doesn't yield passwords; attackers get hashes, which they cannot reverse.

Data Protection API for Cookies and Tokens

ASP.NET Core's Data Protection API (DPAPI) encrypts and decrypts sensitive short-lived data like authentication cookies, anti-forgery tokens, and session tokens. DPAPI uses symmetric encryption and handles key rotation automatically.

By default, DPAPI stores keys in the file system at %APPDATA%\Microsoft\UserSecrets on Windows or ~/.aspnet/DataProtection on Linux. This works for a single instance. When you scale horizontally—running multiple app servers—each instance has its own keys and cannot decrypt data encrypted by another instance. Users lose sessions on every request if they're load-balanced to different servers.

Persist keys to a shared store. For Azure, use Azure Key Vault:

csharp
services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(new Uri("https://<account>.blob.core.windows.net/<container>/keys.xml"))
    .ProtectKeysWithAzureKeyVault(keyVaultUri, new DefaultAzureCredential());

For on-premises or non-Azure environments, SQL Server is a solid choice:

csharp
services.AddDataProtection()
    .PersistKeysToSqlServer(
        "Server=myserver;Database=DataProtection;...",
        "DataProtectionKeys",
        "Value",
        "FriendlyName");

Configure this in your ConfigureServices method. DPAPI then encrypts cookies transparently—middleware handles decryption on request. You don't call DPAPI directly in most scenarios; the framework does it for you. When you do need manual encryption (e.g., storing a refresh token temporarily), inject IDataProtectionProvider:

csharp
public class TokenService
{
    private readonly IDataProtector _protector;

    public TokenService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("TokenService.RefreshToken");
    }

    public string ProtectRefreshToken(string token)
    {
        return _protector.Protect(token);
    }

    public string UnprotectRefreshToken(string protectedToken)
    {
        return _protector.Unprotect(protectedToken);
    }
}

Call this before storing tokens in a cache or database, and unprotect on retrieval. The IDataProtector is scoped to your purpose (here, "TokenService.RefreshToken"), so keys are isolated per use case.

Encrypt Sensitive Columns at the Application or Database Layer

Passwords get hashed. Other sensitive data—national ID numbers, tax IDs, API keys, or payment tokens—often needs to be both stored and retrieved intact. This calls for encryption, not hashing.

You have two options: encrypt at the application layer or leverage database-native encryption.

Application-Layer Encryption

Use symmetric encryption (AES-256) with .NET's System.Security.Cryptography namespace or a library like NSecure. Here's a practical example:

csharp
public class EncryptionService
{
    private readonly byte[] _key; // 32 bytes for AES-256
    private readonly byte[] _iv;

    public EncryptionService(string keyBase64, string ivBase64)
    {
        _key = Convert.FromBase64String(keyBase64);
        _iv = Convert.FromBase64String(ivBase64);
    }

    public string Encrypt(string plainText)
    {
        using (var aes = Aes.Create())
        {
            aes.Key = _key;
            aes.IV = _iv;

            using (var encryptor = aes.CreateEncryptor())
            using (var ms = new MemoryStream())
            {
                using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                {
                    using (var sw = new StreamWriter(cs))
                    {
                        sw.Write(plainText);
                    }
                    return Convert.ToBase64String(ms.ToArray());
                }
            }
        }
    }

    public string Decrypt(string cipherText)
    {
        using (var aes = Aes.Create())
        {
            aes.Key = _key;
            aes.IV = _iv;

            using (var decryptor = aes.CreateDecryptor())
            using (var ms = new MemoryStream(Convert.FromBase64String(cipherText)))
            {
                using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                {
                    using (var sr = new StreamReader(cs))
                    {
                        return sr.ReadToEnd();
                    }
                }
            }
        }
    }
}

Inject this service and call it before inserting or after retrieving sensitive columns. Store the key in Azure Key Vault, AWS Secrets Manager, or your secure configuration provider—never in appsettings.json.

The advantage: you control the encryption and can audit exactly which data is encrypted. The disadvantage: you encrypt and decrypt on every query, adding latency and CPU cost.

Database-Level Encryption

SQL Server offers Transparent Data Encryption (TDE), which encrypts the entire database file at the storage layer. You don't change application code; the database handles it transparently. PostgreSQL has pgcrypto for column-level encryption. Azure SQL Database includes Always Encrypted, allowing encrypted columns to be queried without decrypting on the server.

For Always Encrypted in Entity Framework Core:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>()
        .Property(c => c.NationalId)
        .HasColumnType("char(11)");
    
    // Configure in Azure portal or via SSMS to enable encryption
}

Then configure the connection string with key store details. Encrypted columns can still be searched and indexed if you use deterministic encryption (but this reduces security slightly; avoid if possible).

TLS Everywhere, Including Database Connections

Data in transit is vulnerable. A man-in-the-middle (MITM) attacker on the network—even your internal network—can intercept unencrypted traffic. Many teams assume internal networks are safe and skip TLS between app servers and databases. This is a risk.

Enable TLS for all database connections. For SQL Server:

csharp
var connectionString = "Server=mydb.database.windows.net;Database=prod;User Id=admin;Password=...;Encrypt=true;TrustServerCertificate=false;Connection Timeout=30;";
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

Set Encrypt=true and TrustServerCertificate=false to enforce certificate validation. For PostgreSQL with Npgsql:

csharp
var connectionString = "Host=mydb.postgres.database.azure.com;Database=prod;Username=admin;Password=...;SSL Mode=Require;";
services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

Enforce TLS on all external API calls as well. Using HttpClient, HTTPS is already required unless you explicitly opt out. For internal service-to-service communication, use mutual TLS (mTLS) with certificate pinning if the infrastructure supports it.

Also enable HTTPS for your ASP.NET Core app itself. Configure it in Program.cs:

csharp
app.UseHttpsRedirection();
app.UseHsts(); // Enforce HTTPS via Strict-Transport-Security header

HSTS tells browsers to always use HTTPS for your domain, preventing downgrade attacks.

Common Mistakes

  • Reversible encryption for passwords: Teams encrypt passwords thinking it's more flexible. It isn't. Hash and move on.
  • Keys in source control: Storing encryption keys in appsettings.json or committed to Git is a critical vulnerability. Always use a secrets manager (Azure Key Vault, AWS Secrets Manager, or HashiCorp Vault).
  • Ignoring key rotation: Encryption keys should be rotated regularly. DPAPI and managed services handle this; if you roll your own, implement a rotation strategy.
  • Single-instance DPAPI across multiple servers: Not configuring a shared key store causes session loss and authentication failures in scaled environments.
  • Assuming internal networks are safe: Insider threats and compromised containers can access unencrypted traffic. Use TLS everywhere, including between internal services.
  • Over-encrypting non-sensitive data: Encrypting everything is tempting but adds overhead. Encrypt only what regulations or risk assessment demand.

Key Takeaways

  • Hash passwords with a vetted library like PasswordHasher<T> or Argon2. Never encrypt passwords.
  • Configure DPAPI with a shared key store (Azure Key Vault, SQL Server) when running multiple instances.
  • Encrypt sensitive columns at the application layer or database layer depending on your query and performance requirements.
  • Use TLS for all network traffic, including database connections and internal APIs. Disable TrustServerCertificate in production.
  • Store encryption keys in a secrets manager, never in source code or configuration files.
  • Balance security with performance; not all data needs encryption, but sensitive personal or financial data always does.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles