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:
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:
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:
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:
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:
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:
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:
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:
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:
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.






Comments
Loading comments…