| name | dotnet-wpf-secure-config |
| description | WPF アプリに DPAPI 暗号化された設定管理を追加し、資格情報を安全に保存する。 WPF アプリケーションで暗号化済み設定を用意するとき。
|
WPFアプリケーションへのセキュア設定管理の追加
.NET WPFアプリケーションにWindows DPAPI暗号化設定管理を追加するワークフロー:資格情報の暗号化保存、%LOCALAPPDATA%へのJSON永続化、拡張可能なAppConfigModel、DI登録。
ゴール駆動で使うため、最初に達成したいゴール、成功条件、確認手段を短く固定します。
When to Use This Skill
このスキルを使用する場面:
- WPFアプリケーションにセキュアな資格情報保存(パスワード、APIキー、トークン)を追加する場合
- Oracle、Dify等のサービス統合前に、DPAPI暗号化設定基盤をセットアップする場合
- 複数の統合スキルが共有する再利用可能な
SecureConfigServiceを作成する場合
- 既存の
Infrastructure/Configuration/層を新しいWPFプロジェクトに移植する場合
- 平文の
appsettings.json資格情報をDPAPI暗号化に置き換える場合
このスキルが前提となるスキル:
dotnet — WPF 連携系の入口
dotnet-wpf-mvvm-patterns — このセキュリティ基盤を利用する UI 層
Related Skills
dotnet — WPF 連携系の入口
dotnet-wpf-mvvm-patterns — このセキュリティ基盤を利用する UI 層
git-commit — 生成コードを原子的変更で管理
Core Principles
- Security by Default — すべての秘密情報にDPAPI暗号化を適用。平文保存は禁止(ニュートラル)
- Reusable Foundation — DpapiEncryptorとSecureConfigServiceはどのWPFプロジェクトでも動作する再利用可能な基盤(成長の複利)
- Extensible Config — AppConfigModelは既存の設定を壊さずに新しい統合を追加できる(基礎と型)
- Layered Architecture — 設定はInfrastructure層に配置し、インターフェース経由で利用(基礎と型)
- Single Source of Truth — 1つの
config.jsonファイルで全サービスの資格情報を管理(継続は力)
Workflow: WPFへのセキュア設定追加
Step 1 — Set Up Configuration Structure
設定の暗号化に必要なフォルダ構造とNuGet依存関係を初期化する場合に使用。
Infrastructure/Configuration/フォルダを作成し、必要なパッケージをインストールする。
YourApp/
└── Infrastructure/
└── Configuration/
├── DpapiEncryptor.cs # DPAPI暗号化/復号化ユーティリティ
├── ISecureConfigService.cs # サービスインターフェース
├── SecureConfigService.cs # JSON永続化 + 暗号化
└── AppConfigModel.cs # 拡張可能な設定ルート
# DPAPI(System.Security.Cryptography.ProtectedData)に必要
Install-Package System.Security.Cryptography.ProtectedData
# DI登録に必要
Install-Package Microsoft.Extensions.DependencyInjection
Values: 基礎と型 / 成長の複利
Step 2 — Implement DpapiEncryptor
すべての設定モデルが共有するWindows DPAPI暗号化ユーティリティを追加する場合に使用。
Encrypt、Decrypt、MaskSensitiveメソッドを持つ静的暗号化ヘルパーを作成する。
// Infrastructure/Configuration/DpapiEncryptor.cs — シグネチャ概要
public static class DpapiEncryptor
{
private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("YourApp_Config_Salt_2026");
public static string Encrypt(string plainText) // DPAPI Protect → Base64
public static string Decrypt(string encryptedText) // Base64 → DPAPI Unprotect
public static string MaskSensitive(string value) // ログ用マスク "abcd****"
}
完全な実装は references/detailed-patterns.md を参照。
なぜ完全なエラーハンドリングが必要か:CryptographicExceptionはユーザーAで暗号化したデータをユーザーBが復号化しようとした場合(プロファイル移行後など)に発生する。これをキャッチしないと、再入力を促す代わりにアプリが起動時にクラッシュする。
Values: ニュートラル / 基礎と型
Step 3 — Define Config Models
拡張可能な設定データ構造を定義する場合に使用。
AppConfigModelをルートとし、ドメイン固有モデルをプロパティとして追加する。各統合スキルが独自のモデルを追加する。
AppConfigModel.cs — 拡張可能なルート:
namespace YourApp.Infrastructure.Configuration
{
public class AppConfigModel
{
public string Version { get; set; } = "1.0";
}
}
設定モデルのパターン — 各サービスは以下のテンプレートに従う:
public class ServiceConfigModel
{
public string Endpoint { get; set; } = string.Empty;
public string CredentialEncrypted { get; set; } = string.Empty;
public string GetDecryptedCredential()
=> DpapiEncryptor.Decrypt(CredentialEncrypted);
public void SetCredential(string plainCredential)
=> CredentialEncrypted = DpapiEncryptor.Encrypt(plainCredential);
public bool IsValid()
=> !string.IsNullOrWhiteSpace(Endpoint)
&& !string.IsNullOrWhiteSpace(CredentialEncrypted);
}
なぜ拡張可能なルートにするか:OracleとDifyを同時に使用する場合、AppConfigModelが両方を保持する:
{
"OracleDb": {
"UserId": "SCOTT",
"PasswordEncrypted": "AQAAANCMnd8B..."
},
"DifyApi": {
"BaseUrl": "https://api.dify.ai",
"ApiKeyEncrypted": "AQAAANCMnd8B..."
},
"Version": "1.0"
}
Values: 成長の複利 / 基礎と型
Step 4 — Build SecureConfigService
DPAPI暗号化資格情報サポート付きのJSON永続化サービスを実装する場合に使用。
ISecureConfigServiceインターフェースとSecureConfigService実装を作成する。サービスはAppConfigModel全体を管理し、各統合向けの型付きload/saveメソッドを公開する。
ISecureConfigService.cs:
using System.Threading.Tasks;
namespace YourApp.Infrastructure.Configuration
{
public interface ISecureConfigService
{
bool ConfigExists();
Task ResetConfigAsync();
}
}
SecureConfigService.cs:
// Infrastructure/Configuration/SecureConfigService.cs — シグネチャ概要
public class SecureConfigService : ISecureConfigService
{
// ✅ "YourAppName"を変更 — Step 6参照
// 設定を %LOCALAPPDATA%/YourAppName/config/config.json に保存
public bool ConfigExists()
public Task ResetConfigAsync()
// ✅ 統合ごとに型付きload/saveメソッドを追加
protected Task<AppConfigModel> LoadAppConfigAsync()
protected Task SaveAppConfigAsync(AppConfigModel appConfig)
}
完全な実装は references/detailed-patterns.md を参照。
なぜLoad/SaveAppConfigAsyncがprotectedか:統合スキル(Oracle、Dify)が型付きメソッドを追加してこれらの内部ヘルパーを呼び出す。protectedにすることでサブクラス化を可能にしつつ、公開APIをクリーンに保つ。
Values: 基礎と型 / 継続は力
Step 5 — Register DI Container
SecureConfigServiceをWPFアプリケーションの依存性注入に接続する場合に使用。
App.xaml.csでISecureConfigServiceをシングルトンとして登録する:
using Microsoft.Extensions.DependencyInjection;
public partial class App : Application
{
private ServiceProvider? _serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var services = new ServiceCollection();
services.AddSingleton<ISecureConfigService, SecureConfigService>();
_serviceProvider = services.BuildServiceProvider();
}
protected override void OnExit(ExitEventArgs e)
{
_serviceProvider?.Dispose();
base.OnExit(e);
}
}
なぜシングルトンか:すべての統合が同じconfig.jsonファイルを共有する。複数インスタンスは同時書き込みの競合リスクを生む。
Values: 成長の複利 / 基礎と型
Step 6 — Customize for Your App
生成コードを本番デプロイ用に準備する場合に使用。
出荷前に以下のプレースホルダーを置き換える:
| 項目 | ファイル | 変更内容 | 未変更時の影響 |
|---|
| アプリ名 | SecureConfigService.cs | "YourAppName" → 実際のアプリ名 | 設定が間違ったフォルダに保存 |
| ソルト | DpapiEncryptor.cs | Entropyのバイト配列値 | 共有ソルトで分離が弱まる |
| 名前空間 | 全.csファイル | YourApp → 実際の名前空間 | ビルドエラー |
カスタマイズ確認:
# プレースホルダーが残っていないか確認
Select-String -Path "Infrastructure/Configuration/*.cs" -Pattern "YourApp" -SimpleMatch
# カスタマイズ後の期待結果:0件
⚠️ 重要:暗号化済みデータがある状態でEntropy値を変更すると、既存の暗号化データは回復不可能になる。初回使用前に一度だけ設定すること。
Values: ニュートラル / 基礎と型
Common Pitfalls
1. 暗号化後のエントロピー変更
問題:資格情報が保存された後にDpapiEncryptorのソルト値を更新してしまう。
解決策:初回デプロイ前にエントロピー値を一度設定する。変更が必要な場合は、旧ソルトで復号化し新ソルトで再暗号化するマイグレーションを実装する。
2. CurrentUserの代わりにLocalMachineスコープを使用
問題:DataProtectionScope.LocalMachineは同じPC上の全ユーザーが復号化可能。
解決策:ユーザー単位の資格情報分離には常にDataProtectionScope.CurrentUserを使用する。
ProtectedData.Protect(data, entropy, DataProtectionScope.LocalMachine);
ProtectedData.Protect(data, entropy, DataProtectionScope.CurrentUser);
3. 復号化された秘密情報のログ出力
問題:平文のパスワードやAPIキーをログファイルに書き込む。
解決策:ログ出力には常にDpapiEncryptor.MaskSensitive()を使用する。
logger.LogInformation($"Password: {password}");
logger.LogInformation($"Password: {DpapiEncryptor.MaskSensitive(password)}");
4. CryptographicExceptionの無視
問題:復号化エラーを無視して空文字列を返す。
解決策:エラーをユーザーに表示し、資格情報の再入力を促す。
Anti-Patterns
appsettings.jsonへの資格情報保存
何が問題か:平文のパスワードやAPIキーをソース管理対象の設定ファイルに保存すること。
なぜ問題か:リポジトリアクセス権を持つ全員が読める。デプロイ成果物も暗号化されていないことが多い。
正しいアプローチ:DpapiEncryptor + SecureConfigServiceを使い、暗号化値を%LOCALAPPDATA%に保存する。
サービスごとに別々の設定ファイル
何が問題か:oracle-config.json、dify-config.jsonなど統合ごとに別ファイルを作成すること。
なぜ問題か:ファイルI/Oロジックが重複し、競合状態が発生し、バックアップ/リセットが困難になる。
正しいアプローチ:サービスごとに型付きプロパティを持つ単一のAppConfigModelを1つのconfig.jsonに永続化する。
ソースコードへの資格情報ハードコード
何が問題か:接続文字列やAPIキーをC#コードに直接埋め込むこと。
なぜ問題か:削除後もバージョン管理の履歴に資格情報が残る。
正しいアプローチ:実行時に常にISecureConfigServiceから読み取る。
Quick Reference
Migration Checklist(新アプリへの移植)
Security Checklist
What to Encrypt — 暗号化対象判定表
| データ種別 | 暗号化? | 理由 |
|---|
| パスワード | ✅ 必須 | 認証資格情報 |
| APIキー | ✅ 必須 | サービスアクセストークン |
| トークン/秘密鍵 | ✅ 必須 | ベアラー資格情報 |
| URL/エンドポイント | ❌ 不要 | 公開情報 |
| ユーザーID/名前 | ⚠️ ポリシー依存 | 個人情報の可能性 |
| タイムアウト値 | ❌ 不要 | 非機密設定 |
DPAPI Security Properties
| 項目 | 値 |
|---|
| 暗号化スコープ | ユーザー単位(CurrentUser) |
| 鍵管理 | 自動(Windows管理) |
| ポータブル? | ❌ 別PC・別ユーザーでは復号化不可 |
| クラウド同期? | ❌ 非対応(マシン鍵が異なるため) |
| バックアップ戦略 | リストア後に資格情報を再入力 |
Resources