UZMAN
Veri Koruma — Encryption, Hashing & Masking
Production'da hassas veri (TC kimlik, kart no, şifre) saklama kaçınılmaz. Üç farklı strateji: Encrypt (şifrele/çöz — geri dönüşümlü), Hash (tek yönlü özetle — şifreler için), Mask (görünümü gizle — loglama/raporlama için).
Strateji Karşılaştırması
| Strateji | Geri Dönüşüm | Kullanım Alanı | Performans |
|---|---|---|---|
| Encryption | Decrypt edilebilir | Kart no, TC kimlik, adres | Orta |
| Hashing | Tek yönlü | Şifre, PIN, güvenlik sorusu | Hızlı |
| Masking | Orijinal saklanır | Loglama, API response, raporlama | Hızlı |
1. Application-Level Encryption (Value Converter ile)
GÜVENLİK: AES-CBC modu (varsayılan) padding oracle saldırısına açıktır. Production'da AES-GCM kullanın — hem şifreleme hem authentication sağlar.
// Encrypt/Decrypt servisi — AES-GCM (Authenticated Encryption)
public interface IEncryptionService
{
string Encrypt(string plainText);
string Decrypt(string cipherText);
}
public class AesGcmEncryptionService : IEncryptionService
{
private readonly byte[] _key;
public AesGcmEncryptionService(IConfiguration config)
{
_key = Convert.FromBase64String(config["Encryption:Key"]!);
if (_key.Length != 32)
throw new ArgumentException("Encryption key must be 256 bits (32 bytes).");
}
public string Encrypt(string plainText)
{
var plainBytes = Encoding.UTF8.GetBytes(plainText);
var nonce = new byte[AesGcm.NonceByteSizes.MaxSize]; // 12 bytes
RandomNumberGenerator.Fill(nonce);
var cipherBytes = new byte[plainBytes.Length];
var tag = new byte[AesGcm.TagByteSizes.MaxSize]; // 16 bytes
using var aes = new AesGcm(_key, AesGcm.TagByteSizes.MaxSize);
aes.Encrypt(nonce, plainBytes, cipherBytes, tag);
// Nonce (12) + Tag (16) + CipherText birlikte saklanır
var 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 string Decrypt(string cipherText)
{
var fullBytes = Convert.FromBase64String(cipherText);
if (fullBytes.Length < 29) // Nonce(12) + Tag(16) + en az 1 byte
throw new CryptographicException("Invalid cipher text.");
var nonce = fullBytes[..12];
var tag = fullBytes[12..28];
var cipherBytes = fullBytes[28..];
var plainBytes = new byte[cipherBytes.Length];
using var aes = new AesGcm(_key, AesGcm.TagByteSizes.MaxSize);
aes.Decrypt(nonce, cipherBytes, tag, plainBytes);
// Tampered veri → CryptographicException otomatik fırlatılır (GCM avantajı)
return Encoding.UTF8.GetString(plainBytes);
}
}
// ⚠️ Production'da key yönetimi için Azure Key Vault veya AWS KMS kullanın.
// Hardcoded key veya appsettings.json'da açık key ASLA production'a gitmesin.
// 💡 .NET 8+: AesGcm(key, tagSizeInBytes) constructor'ı kullanılmalı (eski overload deprecated).
// Entity Configuration — Value Converter olarak uygula
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
private readonly IEncryptionService _encryption;
public CustomerConfiguration(IEncryptionService encryption)
=> _encryption = encryption;
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.Property(c => c.NationalId)
.HasMaxLength(500) // Şifreli metin daha uzun olur!
.HasConversion(
v => _encryption.Encrypt(v), // DB'ye yazarken şifrele
v => _encryption.Decrypt(v)); // DB'den okurken çöz
builder.Property(c => c.CreditCardNumber)
.HasMaxLength(500)
.HasConversion(
v => _encryption.Encrypt(v),
v => _encryption.Decrypt(v));
}
}
-- DB'de şifreli olarak saklanır:
SELECT [NationalId] FROM [Customers] WHERE [Id] = 1;
-- Sonuç: 'FmK3x9...(base64 şifreli metin)...'
-- ⚠️ WHERE ile şifreli sütunda arama YAPILAMAZ:
-- WHERE NationalId = '12345678901' ← ÇALIŞMAZ (DB'de şifreli)
-- DB'de şifreli olarak saklanır:
SELECT national_id FROM customers WHERE id = 1;
-- Sonuç: 'FmK3x9...(base64 şifreli metin)...'
-- ⚠️ WHERE ile şifreli sütunda arama YAPILAMAZ:
-- WHERE national_id = '12345678901' ← ÇALIŞMAZ (DB'de şifreli)
Kısıtlamalar:
- Şifreli sütunda
WHERE,ORDER BY,INDEXkullanılamaz (DB şifreli metni görür)- Arama gerekiyorsa → ayrıca hash'lenmiş bir lookup sütunu ekle
- Key yönetimi kritik — Azure Key Vault veya AWS KMS kullan
Update İşlemlerinde Ne Olur?
Value Converter decrypt/re-encrypt işlemini otomatik halleder:
// Update senaryosu — geliştirici perspektifi:
var customer = await context.Customers.FindAsync(1);
// Bu noktada customer.NationalId = "12345678901" (çözülmüş hali — Value Converter çözdü)
customer.NationalId = "98765432101"; // Yeni değeri plain-text olarak ata
await context.SaveChangesAsync();
// EF otomatik olarak: Encrypt("98765432101") → DB'ye şifreli yazar
Perde arkasında olan:
Key Rotation (Anahtar Değişimi) durumunda tüm kayıtlar yeniden şifrelenmelidir:
// Key Rotation — mevcut verileri yeni anahtarla yeniden şifrele
public async Task RotateEncryptionKey(
IEncryptionService oldService,
IEncryptionService newService)
{
var customers = await context.Customers.ToListAsync();
// Value Converter eski key ile decrypt etti → plain-text elimizde
// Yeni key'li converter'a geçiş yapıldıktan sonra SaveChanges
// yeni key ile encrypt eder
foreach (var c in customers)
{
// Property'yi "dirty" işaretle (değer aynı olsa bile yeniden yazılsın)
context.Entry(c).Property(x => x.NationalId).IsModified = true;
context.Entry(c).Property(x => x.CreditCardNumber).IsModified = true;
}
await context.SaveChangesAsync(); // Yeni converter ile re-encrypt
}
Partial Update dikkat:
- Eğer sadece
customer.Namedeğiştirirseniz, EF şifreli sütunlara dokunmaz (zatenIsModified = false)ExecuteUpdateAsync()kullanıyorsanız Value Converter çalışmaz — ham SQL üretilir, şifrelemeyi kendiniz yapmalısınız:
// ❌ YANLIŞ — Value Converter ExecuteUpdate'te çalışmaz!
await context.Customers
.Where(c => c.Id == 1)
.ExecuteUpdateAsync(s => s.SetProperty(c => c.NationalId, "98765432101"));
// DB'ye plain-text "98765432101" yazılır!
// ✅ DOĞRU — Manuel şifrele
var encrypted = encryptionService.Encrypt("98765432101");
await context.Customers
.Where(c => c.Id == 1)
.ExecuteUpdateAsync(s => s.SetProperty(c => c.NationalId, encrypted));
2. SQL Server Always Encrypted (Provider Seviyesi)
// Connection string'e eklenir — EF kodu değişmez!
"Server=.;Database=MyDb;Column Encryption Setting=enabled;"
// SQL Server tarafında sütun tanımı:
// ALTER TABLE Customers ALTER COLUMN NationalId NVARCHAR(11)
// ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC,
// ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256',
// COLUMN_ENCRYPTION_KEY = MyCEK);
| Encryption Type | WHERE Desteği | Kullanım |
|---|---|---|
| Deterministic | Eşitlik (=) | Arama gereken sütunlar (TC kimlik) |
| Randomized | Maksimum güvenlik (kart no, adres) |
Always Encrypted'da şifreleme/çözme client tarafında olur — DB sunucusu bile veriyi göremez.
3. Hashing (Tek Yönlü — Şifre Saklama)
// ⚠️ Hashing bir Value Converter DEĞİLDİR — çünkü geri çözülemez.
// Entity'de hash ve salt birlikte saklanır:
public class User
{
public int Id { get; set; }
public string Email { get; set; } = null!;
public string PasswordHash { get; set; } = null!; // Şifrenin hash'i
public string PasswordSalt { get; set; } = null!; // Tuz
}
// Hash oluşturma (kayıt sırasında)
public static (string hash, string salt) HashPassword(string password)
{
var salt = RandomNumberGenerator.GetBytes(32);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password,
salt,
iterations: 100_000,
HashAlgorithmName.SHA256,
outputLength: 32);
return (Convert.ToBase64String(hash), Convert.ToBase64String(salt));
}
// Doğrulama (login sırasında)
public static bool VerifyPassword(string password, string storedHash, string storedSalt)
{
var salt = Convert.FromBase64String(storedSalt);
var hash = Rfc2898DeriveBytes.Pbkdf2(
password, salt, 100_000, HashAlgorithmName.SHA256, 32);
return CryptographicOperations.FixedTimeEquals(
hash, Convert.FromBase64String(storedHash));
}
// EF Configuration — sadece sütun boyutu ayarlanır
builder.Property(u => u.PasswordHash).HasMaxLength(88).IsRequired(); // Base64(32 byte)
builder.Property(u => u.PasswordSalt).HasMaxLength(88).IsRequired();
builder.Property(u => u.Email).HasMaxLength(256);
builder.HasIndex(u => u.Email).IsUnique(); // Login lookup için
ASLA yapma:
- MD5/SHA1 kullanma (zayıf)
- Salt'sız hash'leme (rainbow table saldırısı)
- Şifreyi plain-text saklama
Kullan:Pbkdf2,BCrypt,Argon2id
Hash'lenmiş Veriyi Güncelleme (Şifre Değiştirme)
Hash tek yönlüdür — eski hash'i "açıp" düzenleyemezsin. Güncelleme = eski şifreyi doğrula + yeni hash oluştur.
public async Task<bool> ChangePassword(int userId, string currentPassword, string newPassword)
{
var user = await context.Users.FindAsync(userId);
if (user == null) return false;
// 1️⃣ Önce mevcut şifreyi doğrula (eski hash ile karşılaştır)
if (!VerifyPassword(currentPassword, user.PasswordHash, user.PasswordSalt))
return false; // Eski şifre yanlış → reddet
// 2️⃣ Yeni şifreyi hash'le (yeni salt ile)
var (newHash, newSalt) = HashPassword(newPassword);
// 3️⃣ Hash ve salt'ı güncelle
user.PasswordHash = newHash;
user.PasswordSalt = newSalt;
await context.SaveChangesAsync();
return true;
}
SQL çıktısı:
-- EF'in ürettiği UPDATE:
UPDATE [Users]
SET [PasswordHash] = @p0, -- Yeni hash (tamamen farklı bir string)
[PasswordSalt] = @p1 -- Yeni salt (her seferinde random)
WHERE [Id] = @p2;
-- EF'in ürettiği UPDATE:
UPDATE users
SET password_hash = @p0, -- Yeni hash (tamamen farklı bir string)
password_salt = @p1 -- Yeni salt (her seferinde random)
WHERE id = @p2;
Neden yeni salt?
Her şifre değişikliğinde yeni salt üretilir. Aynı şifre bile farklı hash üretir → rainbow table saldırısını önler.
Aynı şifre "MyPass123":
Salt_1 + "MyPass123" → Hash: "a8f4e2..."
Salt_2 + "MyPass123" → Hash: "7c9b1d..." ← Tamamen farklı!
Admin tarafından şifre sıfırlama (eski şifre bilinmiyor):
public async Task AdminResetPassword(int userId, string temporaryPassword)
{
var user = await context.Users.FindAsync(userId);
var (hash, salt) = HashPassword(temporaryPassword);
user.PasswordHash = hash;
user.PasswordSalt = salt;
user.MustChangePassword = true; // İlk login'de değiştirmeye zorla
await context.SaveChangesAsync();
}
4. Dynamic Data Masking (SQL Server)
// EF Core'da HasComment ile belgelenir, masking SQL tarafında uygulanır
builder.Property(c => c.Email)
.HasColumnType("nvarchar(256)")
.HasComment("MASKED WITH (FUNCTION = 'email()')");
builder.Property(c => c.CreditCardNumber)
.HasColumnType("nvarchar(20)")
.HasComment("MASKED WITH (FUNCTION = 'partial(0,\"XXXX-XXXX-XXXX-\",4)')");
-- Migration sonrası SQL ile masking ekle:
ALTER TABLE Customers ALTER COLUMN Email ADD MASKED WITH (FUNCTION = 'email()');
ALTER TABLE Customers ALTER COLUMN Phone ADD MASKED WITH (FUNCTION = 'partial(0,"***-",4)');
ALTER TABLE Customers ALTER COLUMN CreditCardNumber ADD MASKED WITH (FUNCTION = 'partial(0,"XXXX-XXXX-XXXX-",4)');
-- Yetkisiz kullanıcının gördüğü:
-- Email: aXXX@XXXX.com
-- Phone: ***-5678
-- CreditCard: XXXX-XXXX-XXXX-1234
-- Yetkili kullanıcı (UNMASK izni olan) orijinal veriyi görür:
GRANT UNMASK TO [AppAdminUser];
-- PostgreSQL'de native dynamic masking yok, VIEW + RLS ile benzer sonuç:
CREATE VIEW customers_masked AS
SELECT id, name,
LEFT(email, 1) || '***@' || SPLIT_PART(email, '@', 2) AS email,
'***-' || RIGHT(phone, 4) AS phone,
'XXXX-XXXX-XXXX-' || RIGHT(credit_card_number, 4) AS credit_card_number
FROM customers;
-- Row Level Security ile erişim kontrolü:
ALTER TABLE customers ENABLE ROW LEVEL SECURITY;
GRANT SELECT ON customers_masked TO app_user;
-- Admin kullanıcı orijinal tabloyu doğrudan görebilir
5. Application-Level Masking (Projection ile)
// API response'da masking — DB'ye dokunmadan
var customers = await context.Customers
.Select(c => new CustomerDto
{
Id = c.Id,
Name = c.Name,
Email = MaskEmail(c.Email), // a***@gmail.com
Phone = MaskPhone(c.Phone), // ***-5678
CardLast4 = c.CreditCardNumber[^4..] // Sadece son 4 hane
})
.ToListAsync();
// Masking helper
static string MaskEmail(string email)
{
var parts = email.Split('@');
return $"{parts[0][0]}***@{parts[1]}";
}
Hangi Stratejiyi Ne Zaman Kullan?
| Senaryo | Strateji | Neden |
|---|---|---|
| Şifre saklama | Hash (Pbkdf2/Argon2) | Geri çözülmemeli |
| TC Kimlik / Kart No (arama gerekli) | Always Encrypted (Deterministic) | DB seviyesi, EF'e şeffaf |
| TC Kimlik / Kart No (arama gereksiz) | App Encryption (Value Converter) | Basit, provider bağımsız |
| API response gizleme | App Masking (Projection) | DB'ye dokunmaz, esnek |
| Rapor/BI kullanıcısı kısıtlama | Dynamic Data Masking | DBA yönetir, kod gerektirmez |
| Log'larda hassas veri | App Masking | Serialize öncesi maskeleme |