mcpField
D
data-protection

data-protection

github.com/wshaddix安全8678 次安装35 周前更新
代码仓库

关于 data-protection

面向 ASP.NET Core 开发者的数据保护实践指南,围绕 Data Protection API 介绍加密、密钥管理和应用隔离。还涵盖静态数据、限时令牌及会话数据保护,并指出硬编码密钥等反模式。

主要功能

  1. 01配置密钥存储与应用隔离
  2. 02保护数据库中的敏感数据
  3. 03创建限时保护令牌
  4. 04规划密钥轮换与管理
  5. 05保护会话数据并识别硬编码密钥风险

适用场景

  1. 01加密存储在数据库中的敏感信息
  2. 02创建会过期的受保护令牌
  3. 03在多应用环境中配置隔离的保护密钥

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill data-protection

安装后在 Claude Code 中输入/data-protection确认已加载。

详细介绍

原理

保护敏感数据对于安全合规和用户隐私至关重要。ASP.NET Core Data Protection API 提供了一个安全、易用的框架,用于加密、密钥管理和数据保护。如果没有恰当的模式,应用程序可能面临数据泄露、密钥管理失败和违规风险。这些模式可确保数据保护实践安全且易于维护。

模式

模式 1:数据保护配置

配置 Data Protection,确保密钥存储和应用程序隔离方式恰当。

// Program.cs - Basic configuration
builder.Services.AddDataProtection()
    .SetApplicationName("MyApp") // Critical for multi-app environments
    .PersistKeysToFileSystem(new DirectoryInfo(@"\shared\keys"))
    .ProtectKeysWithDpapi(); // Windows only

// Cross-platform key protection
builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .PersistKeysToFileSystem(new DirectoryInfo(@"/shared/keys"))
    .ProtectKeysWithCertificate(
        new X509Certificate2("/certs/dataprotection.pfx", "password"));

// Azure Blob Storage for key persistence (production)
builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .PersistKeysToAzureBlobStorage(blobUri)
    .ProtectKeysWithAzureKeyVaultKey(keyVaultKeyId);

// Redis for key storage in containerized environments
builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .PersistKeysToStackExchangeRedis(connection, "DataProtection-Keys")
    .ProtectKeysWithCertificate(certificate);
模式 2:保护静态敏感数据

使用 Data Protection 加密敏感数据,然后再存储到数据库中。

public interface IDataProtectorService
{
    string Protect(string plainText);
    string? Unprotect(string protectedText);
    byte[] Protect(byte[] plainData);
    byte[]? Unprotect(byte[] protectedData);
}

public class DataProtectorService : IDataProtectorService
{
    private readonly IDataProtector _protector;
    private readonly ILogger<DataProtectorService> _logger;

    public DataProtectorService(
        IDataProtectionProvider dataProtectionProvider,
        ILogger<DataProtectorService> logger)
    {
        // Create purpose-specific protector
        _protector = dataProtectionProvider.CreateProtector("MyApp.SensitiveData.v1");
        _logger = logger;
    }

    public string Protect(string plainText)
    {
        if (string.IsNullOrEmpty(plainText))
            return plainText;

        try
        {
            return _protector.Protect(plainText);
        }
        catch (CryptographicException ex)
        {
            _logger.LogError(ex, "Failed to protect data");
            throw;
        }
    }

    public string? Unprotect(string protectedText)
    {
        if (string.IsNullOrEmpty(protectedText))
            return protectedText;

        try
        {
            return _protector.Unprotect(protectedText);
        }
        catch (CryptographicException ex)
        {
            _logger.LogWarning(ex, "Failed to unprotect data - may be corrupted or from different key");
            return null;
        }
    }

    public byte[] Protect(byte[] plainData)
    {
        ArgumentNullException.ThrowIfNull(plainData);
        return _protector.Protect(plainData);
    }

    public byte[]? Unprotect(byte[] protectedData)
    {
        ArgumentNullException.ThrowIfNull(protectedData);
        
        try
        {
            return _protector.Unprotect(protectedData);
        }
        catch (CryptographicException ex)
        {
            _logger.LogWarning(ex, "Failed to unprotect binary data");
            return null;
        }
    }
}

// Entity with encrypted fields
public class PaymentMethod
{
    public Guid Id { get; set; }
    public required string UserId { get; set; }
    
    // Store encrypted card number
    public required string EncryptedCardNumber { get; set; }
    
    // Last 4 digits stored in clear for display
    public required string CardLastFourDigits { get; set; }
    
    public required string EncryptedExpirationDate { get; set; }
    public required string CardType { get; set; }
    public DateTimeOffset CreatedAt { get; set; }
    
    [NotMapped]
    public string? CardNumber { get; set; }
    
    [NotMapped]
    public string? ExpirationDate { get; set; }
}

// Repository with encryption/decryption
public class PaymentMethodRepository
{
    private readonly ApplicationDbContext _dbContext;
    private readonly IDataProtectorService _protector;

    public PaymentMethodRepository(
        ApplicationDbContext dbContext,
        IDataProtectorService protector)
    {
        _dbContext = dbContext;
        _protector = protector;
    }

    public async Task<PaymentMethod> AddAsync(PaymentMethod paymentMethod)
    {
        // Encrypt sensitive fields before saving
        paymentMethod.EncryptedCardNumber = _protector.Protect(paymentMethod.CardNumber!);
        paymentMethod.EncryptedExpirationDate = _protector.Protect(paymentMethod.ExpirationDate!);
        
        // Store last 4 digits for display
        paymentMethod.CardLastFourDigits = paymentMethod.CardNumber![^4..];
        
        // Clear plain text fields (they're NotMapped anyway)
        paymentMethod.CardNumber = null;
        paymentMethod.ExpirationDate = null;
        
        _dbContext.PaymentMethods.Add(paymentMethod);
        await _dbContext.SaveChangesAsync();
        
        return paymentMethod;
    }

    public async Task<PaymentMethod?> GetByIdAsync(Guid id)
    {
        var paymentMethod = await _dbContext.PaymentMethods
            .FirstOrDefaultAsync(p => p.Id == id);

        if (paymentMethod != null)
        {
            // Decrypt for use
            paymentMethod.CardNumber = _protector.Unprotect(paymentMethod.EncryptedCardNumber);
            paymentMethod.ExpirationDate = _protector.Unprotect(paymentMethod.EncryptedExpirationDate);
        }

        return paymentMethod;
    }

    public async Task<List<PaymentMethod>> GetByUserIdAsync(string userId)
    {
        // Return without decrypted data for listing
        return await _dbContext.PaymentMethods
            .AsNoTracking()
            .Where(p => p.UserId == userId)
            .Select(p => new PaymentMethod
            {
                Id = p.Id,
                UserId = p.UserId,
                CardLastFourDigits = p.CardLastFourDigits,
                CardType = p.CardType,
                CreatedAt = p.CreatedAt
                // Don't include encrypted fields or decrypted data
            })
            .ToListAsync();
    }
}
模式 3:限时保护

使用限时数据保护器创建会在指定时间后过期的令牌。

public class TokenService
{
    private readonly ITimeLimitedDataProtector _protector;
    private readonly ILogger<TokenService> _logger;

    public TokenService(IDataProtectionProvider dataProtectionProvider, ILogger<TokenService> logger)
    {
        var baseProtector = dataProtectionProvider.CreateProtector("MyApp.TimeLimitedTokens");
        _protector = baseProtector.ToTimeLimitedDataProtector();
        _logger = logger;
    }

    public string GenerateToken(string purpose, string userId, TimeSpan lifetime)
    {
        var payload = JsonSerializer.Serialize(new TokenPayload
        {
            Purpose = purpose,
            UserId = userId,
            IssuedAt = DateTimeOffset.UtcNow
        });

        return _protector.Protect(payload, lifetime);
    }

    public TokenPayload? ValidateToken(string token, string expectedPurpose)
    {
        try
        {
            var payload = _protector.Unprotect(token, out var expiration);
            var data = JsonSerializer.Deserialize<TokenPayload>(payload);

            if (data?.Purpose != expectedPurpose)
            {
                _logger.LogWarning("Token purpose mismatch: expected {Expected}, got {Actual}",
                    expectedPurpose, data?.Purpose);
                return null;
            }

            _logger.LogDebug("Token validated, expires at {Expiration}", expiration);
            return data;
        }
        catch (CryptographicException ex)
        {
            _logger.LogWarning(ex, "Token validation failed");
            return null;
        }
    }

    public string GeneratePasswordResetToken(string userId, string email)
    {
        // 1 hour expiration
        return GenerateToken("password-reset", $"{userId}:{email}", TimeSpan.FromHours(1));
    }

    public string GenerateEmailConfirmationToken(string userId, string email)
    {
        // 24 hour expiration
        return GenerateToken("email-confirmation", $"{userId}:{email}", TimeSpan.FromHours(24));
    }

    public (string? UserId, string? Email)? ValidatePasswordResetToken(string token)
    {
        var payload = ValidateToken(token, "password-reset");
        if (payload == null) return null;

        var parts = payload.UserId.Split(':');
        return parts.Length == 2 ? (parts[0], parts[1]) : null;
    }
}

public class TokenPayload
{
    public required string Purpose { get; set; }
    public required string UserId { get; set; }
    public DateTimeOffset IssuedAt { get; set; }
}

// Usage in Razor Page
public class ResetPasswordModel : PageModel
{
    private readonly TokenService _tokenService;
    private readonly IUserService _userService;

    [BindProperty]
    public ResetPasswordInput Input { get; set; } = new();

    public string? ErrorMessage { get; set; }

    public async Task<IActionResult> OnGetAsync(string token)
    {
        var validation = _tokenService.ValidatePasswordResetToken(token);
        
        if (validation == null)
        {
            ErrorMessage = "Invalid or expired reset token";
            return Page();
        }

        Input.Token = token;
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        var validation = _tokenService.ValidatePasswordResetToken(Input.Token);
        
        if (validation == null)
        {
            ErrorMessage = "Invalid or expired reset token";
            return Page();
        }

        var (userId, email) = validation.Value;
        await _userService.ResetPasswordAsync(userId, Input.NewPassword);
        
        return RedirectToPage("/Account/ResetPasswordConfirmation");
    }
}
模式 4:密钥轮换与管理

实施恰当的密钥轮换策略,同时确保已有受保护数据仍可使用。

// Custom key management with rotation policies
public class KeyRotationConfiguration
{
    public void Configure(DataProtectionOptions options)
    {
        options.ApplicationDiscriminator = "MyApp.Production";
    }
}

// Key storage with versioning
public class VersionedKeyManager
{
    private readonly ILogger<VersionedKeyManager> _logger;

    public VersionedKeyManager(ILogger<VersionedKeyManager> logger)
    {
        _logger = logger;
    }

    public void RotateKeys(IDataProtectionProvider provider)
    {
        // Data Protection automatically handles key rotation
        // Keys are valid for 90 days by default, with 7 day activation delay
        
        // Log current key status
        var keyManager = provider.GetService<IKeyManager>();
        if (keyManager != null)
        {
            var allKeys = keyManager.GetAllKeys();
            
            _logger.LogInformation("Current keys: {KeyCount}", allKeys.Count());
            
            foreach (var key in allKeys)
            {
                _logger.LogDebug(
                    "Key {KeyId}: Created {Created}, Activation {Activation}, Expiration {Expiration}",
                    key.KeyId,
                    key.CreationDate,
                    key.ActivationDate,
                    key.ExpirationDate);
            }
        }
    }
}

// Configuration for production
builder.Services.AddDataProtection()
    .SetApplicationName("MyApp")
    .SetDefaultKeyLifetime(TimeSpan.FromDays(90)) // Rotate every 90 days
    .PersistKeysToFileSystem(new DirectoryInfo(@"/shared/keys"))
    .ProtectKeysWithCertificate(LoadCertificate())
    .DisableAutomaticKeyGeneration(); // For controlled rotation environments

// Certificate loading
X509Certificate2 LoadCertificate()
{
    // From file
    return new X509Certificate2("dataprotection.pfx", "password");
    
    // Or from store
    using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
    store.Open(OpenFlags.ReadOnly);
    var certs = store.Certificates.Find(
        X509FindType.FindByThumbprint, 
        "thumbprint", 
        validOnly: false);
    return certs[0];
}
模式 5:受保护的会话数据

使用 Data Protection 保护敏感会话数据。

public class ProtectedSessionService
{
    private readonly IDataProtector _protector;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ProtectedSessionService(
        IDataProtectionProvider dataProtectionProvider,
        IHttpContextAccessor httpContextAccessor)
    {
        _protector = dataProtectionProvider.CreateProtector("MyApp.SessionData");
        _httpContextAccessor = httpContextAccessor;
    }

    public void SetProtected<T>(string key, T value)
    {
        var session = _httpContextAccessor.HttpContext?.Session;
        if (session == null) return;

        var json = JsonSerializer.Serialize(value);
        var protectedData = _protector.Protect(json);
        session.SetString($"protected:{key}", protectedData);
    }

    public T? GetProtected<T>(string key)
    {
        var session = _httpContextAccessor.HttpContext?.Session;
        if (session == null) return default;

        var protectedData = session.GetString($"protected:{key}");
        if (string.IsNullOrEmpty(protectedData)) return default;

        try
        {
            var json = _protector.Unprotect(protectedData);
            return JsonSerializer.Deserialize<T>(json);
        }
        catch (CryptographicException)
        {
            // Data was tampered with or keys changed
            session.Remove($"protected:{key}");
            return default;
        }
    }

    public void RemoveProtected(string key)
    {
        _httpContextAccessor.HttpContext?.Session.Remove($"protected:{key}");
    }
}

// Usage in Razor Page
public class CheckoutModel : PageModel
{
    private readonly ProtectedSessionService _session;

    public async Task<IActionResult> OnPostAsync()
    {
        // Store sensitive checkout data securely
        _session.SetProtected("checkout:payment-intent", new PaymentIntentData
        {
            IntentId = paymentIntent.Id,
            Amount = Input.Amount,
            Currency = Input.Currency,
            CustomerId = Input.CustomerId
        });

        // Later, retrieve it
        var intentData = _session.GetProtected<PaymentIntentData>("checkout:payment-intent");
        
        // Clear after completion
        _session.RemoveProtected("checkout:payment-intent");
    }
}

反模式

// ❌ BAD: Hard-coded encryption keys
public class BadEncryptionService
{
    private readonly byte[] _key = Convert.FromBase64String("hardcoded-key-here");
    
    public string Encrypt(string data)
    {
        using var aes = Aes.Create();
        aes.Key = _key; // Never do this!
        // ...
    }
}

// ✅ GOOD: Use Data Protection
public class GoodEncryptionService
{
    private readonly IDataProtector _protector;
    
    public GoodEncryptionService(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("MyApp.Data");
    }
    
    public string Encrypt(string data) => _protector.Protect(data);
}

// ❌ BAD: Not setting application name
builder.Services.AddDataProtection();
// Keys may conflict with other apps on the same server!

// ✅ GOOD: Always set application discriminator
builder.Services.AddDataProtection()
    .SetApplicationName("MyApp.Production");

// ❌ BAD: Ignoring key storage in production
builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo("keys"));
// Keys lost on container restart!

// ✅ GOOD: Persistent key storage
builder.Services.AddDataProtection()
    .PersistKeysToAzureBlobStorage(blobUri)
    .ProtectKeysWithAzureKeyVaultKey(keyId);

// ❌ BAD: Exposing protected data format
public string ProtectUserId(string userId)
{
    return $"protected:{userId}"; // Not actually protected!
}

// ✅ GOOD: Use actual protection
public string ProtectUserId(string userId)
{
    return _protector.Protect(userId); // Opaque, encrypted token
}

// ❌ BAD: Swallowing all cryptographic exceptions
public string? Unprotect(string data)
{
    try
    {
        return _protector.Unprotect(data);
    }
    catch (Exception) // Too broad!
    {
        return null;
    }
}

// ✅ GOOD: Handle specific exceptions appropriately
public string? Unprotect(string data)
{
    try
    {
        return _protector.Unprotect(data);
    }
    catch (CryptographicException ex)
    {
        _logger.LogWarning(ex, "Data unprotection failed");
        return null;
    }
}

// ❌ BAD: Protecting already encrypted data
public string DoubleProtect(string data)
{
    var encrypted = _aes.Encrypt(data);
    return _protector.Protect(encrypted); // Unnecessary overhead!
}

// ✅ GOOD: Use Data Protection alone
public string Protect(string data)
{
    return _protector.Protect(data);
}

// ❌ BAD: No key backup strategy
// Keys stored in single location with no backup

// ✅ GOOD: Implement key backup
public async Task BackupKeysAsync()
{
    var keyDirectory = new DirectoryInfo("/shared/keys");
    var keys = keyDirectory.GetFiles("*.xml");
    
    foreach (var key in keys)
    {
        await UploadToBackupStorageAsync(key);
    }
}

// ❌ BAD: Sharing keys between different applications
// App1 and App2 both use same key path

// ✅ GOOD: Application-specific keys
// App1: .SetApplicationName("MyApp.Web")
// App2: .SetApplicationName("MyApp.Api")

参考

评论

支持 Markdown