data-protection について
ASP.NET Core開発者向けのデータ保護実践ガイドです。Data Protection APIによる暗号化、キー管理、アプリケーション分離を扱い、保存データや有効期限付きトークン、セッションデータの保護とハードコードキーなどのアンチパターンを説明します。
主な機能
- 01キー保存とアプリケーション分離の設定
- 02データベース内の機密データ保護
- 03有効期限付き保護トークンの作成
- 04キーのローテーションと管理
- 05セッションデータの保護とハードコードキーの注意点
活用シーン
- 01データベースに保存する機密情報の暗号化
- 02期限切れとなる保護トークンの作成
- 03複数アプリ間で分離された保護キーの設定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill data-protectionインストール後、Claude Code で/data-protectionを入力して読み込まれたか確認します。
詳細
理由
機密データの保護は、セキュリティコンプライアンスとユーザーのプライバシーにとって不可欠です。ASP.NET Core Data Protection API は、暗号化、キー管理、データ保護のための安全で使いやすいフレームワークを提供します。適切なパターンを使用しないと、アプリケーションではデータ漏えい、キー管理の不備、コンプライアンス違反のリスクが生じます。これらのパターンにより、安全で保守しやすいデータ保護を実践できます。
パターン
パターン 1:データ保護の構成
適切なキーの保存場所とアプリケーションの分離を設定して、データ保護を構成します。
// 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:保存データの保護
データベースに保存する前に、データ保護を使用して機密データを暗号化します。
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:保護されたセッションデータ
データ保護を使用して、機密性の高いセッションデータを保護します。
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 対応