セキュリティ保護された ASP.NET アプリケーションの構築 : 認証、認定、および通信のセキュリティ保護 DPAPI ライブラリを作成する方法
J.D. Meier, Alex Mackman, Michael Dunner, and Srinath Vasireddy
Microsoft Corporation
November 2002
日本語版最終更新日 2003 年 3 月 17 日
適用対象:
Microsoft® ASP.NET
Microsoft Visual Studio® .NET
Microsoft Windows® 2000
全体の概要については、「セキュリティ保護された ASP.NET アプリケーションの構築」の開始ページを参照してください。
要約 : ここでは、データベース接続文字列やアカウント資格情報などのデータを暗号化するアプリケーションに対して DPAPI 関数を公開するマネージ クラス ライブラリを作成する方法について説明します。
目次
Web アプリケーションでは、データベースの接続文字列やサービス アカウントの資格情報など、機密扱いのデータをアプリケーション構成ファイルに記述しなければならないことがあります。セキュリティ上の観点から、この種のデータはプレーン テキストで格納せずに、暗号化した状態で格納する必要があります。
ここでは、DPAPI (Data Protection API) の呼び出しをカプセル化して、データの暗号化と解読を行うマネージ クラス ライブラリを作成する方法について説明します。このライブラリは、ASP.NET Web アプリケーション、Web サービス アプリケーション、Enterprise Services アプリケーションなど、他のマネージ アプリケーションから使用することもできます。
このドキュメントで作成した DPAPI ライブラリの使用を取り上げた関連資料については、このガイドの「パート IV : 参照」の次のドキュメントを参照してください。
メモ
Microsoft Windows 2000 以降のオペレーティング システムには、データの暗号化と解読を行うための Win32® Data Protection API (DPAPI) が用意されています。
DPAPI は、Cryptography API (Crypto API) の一部として crypt32.dll に組み込まれています。DPAPI は、CryptProtectData と CryptUnprotectData の 2 つのメソッドで構成されています。
DPAPI を使用すると、暗号化を使用するアプリケーションで不可避なキー管理の問題が解消されます。暗号化によりデータはセキュリティ保護されますが、別の方法でキーのセキュリティも保護する必要があります。DPAPI では、DPAPI の関数を呼び出したコードに関連付けられているユーザー アカウントのパスワードを使用して、暗号化キーを導出します。その結果を受けて、(アプリケーションではなく) オペレーティング システムがキーを管理します。
DPAPI は、コンピュータ ストアまたはユーザー ストアで使用できます。ユーザー ストアの場合は、ユーザー プロファイルが読み込まれている必要があります。既定では、ユーザー ストアが使用されます。コンピュータ ストアを使用するには、DPAPI 関数に CRYPTPROTECT_LOCAL_MACHINE フラグを渡します。
ユーザー ストアの場合は、機密データにアクセスできるユーザーを制限できるため、セキュリティ的にはより強固であるといえます。データを暗号化したユーザーのみがそのデータを解読できます。ただし、ASP.NET Web アプリケーションからユーザー ストア ベースで DPAPI を使用する場合は、ユーザー プロファイルの読み込みとその解除を実行するコードを別途作成する必要があります。ASP.NET には、自動的にユーザー プロファイルを読み込むしくみがないためです。
コンピュータ ストアの場合は、ユーザー プロファイルの管理が必要ないため、開発が比較的容易です。ただし、コンピュータのすべてのユーザーがデータを解読できるため、エントロピ パラメータを追加しない限り、セキュリティが脆弱になります。エントロピとは、機密データの解読を困難にするために設計されたランダム値です。エントロピ パラメータを追加する場合、エントロピ パラメータもセキュリティ保護された状態で格納する必要が生じ、キー管理の問題がもう 1 つ増える形になります。
メモ DPAPI をコンピュータ ストアで使用した場合は、暗号化した文字列がそのコンピュータ固有のものになります。したがって、それぞれのコンピュータでデータを暗号化する必要があります。あるコンピュータで暗号化したデータを、ファームまたはクラスタ内の別のコンピュータにコピーして使用することはできません。
DPAPI をユーザー ストアで使用した場合は、ローミング ユーザー プロファイルに基づいてすべてのコンピュータでデータを解読できます。
必要条件
ハードウェア、ソフトウェア、ネットワーク インフラストラクチャ、スキル、知識、サービス パックなどの要件は、以下のとおりです。
- Microsoft Windows 2000
- Microsoft Visual Studio .NET 開発システム
また、Microsoft Visual C#® 開発ツールの知識も必要です。
要約
ここでは、次の手順について説明します。
- C# クラス ライブラリを作成する。
- アセンブリに厳密名を付ける (オプション)。
1. C# クラス ライブラリを作成する
ここでは、Encrypt メソッドおよび Decrypt メソッドを公開する C# クラス ライブラリを作成し、Win32 DPAPI 関数の呼び出しをカプセル化する手順について説明します。
■ C# クラス ライブラリを作成するには
- Visual Studio .NET を起動し、"DataProtection" という名前で新しい Visual C# クラス ライブラリを作成します。
- ソリューション エクスプローラで、class1.cs の名前を DataProtection.cs に変更します。
- DataProtection.cs 内の class1 の名前を DataProtector に変更し、それに応じて既定のコンストラクタの名前も変更します。
- ソリューション エクスプローラで、DataProtection を右クリックして、[プロパティ] をクリックします。
- [構成プロパティ] フォルダをクリックして、[セーフ モード以外のコード ブロックの許可] を [True] に設定します。
- [OK] をクリックして、プロパティ ダイアログ ボックスを閉じます。
- DataProtection.cs の先頭の既存の using ステートメントの下に次の using ステートメントを追加します。
using System.Text;
using System.Runtime.InteropServices;
- DataProtector クラスの先頭に次の DllImport ステートメントを追加して、FormatMessage ユーティリティ関数と一緒に Win32 DPAPI 関数が P/Invoke を通じて呼び出されるようにします。
[DllImport("Crypt32.dll", SetLastError=true,
CharSet=System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool CryptProtectData(
ref DATA_BLOB pDataIn,
String szDataDescr,
ref DATA_BLOB pOptionalEntropy,
IntPtr pvReserved,
ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
int dwFlags,
ref DATA_BLOB pDataOut);
[DllImport("Crypt32.dll", SetLastError=true,
CharSet=System.Runtime.InteropServices.CharSet.Auto)]
private static extern bool CryptUnprotectData(
ref DATA_BLOB pDataIn,
String szDataDescr,
ref DATA_BLOB pOptionalEntropy,
IntPtr pvReserved,
ref CRYPTPROTECT_PROMPTSTRUCT pPromptStruct,
int dwFlags,
ref DATA_BLOB pDataOut);
[DllImport("kernel32.dll",
CharSet=System.Runtime.InteropServices.CharSet.Auto)]
private unsafe static extern int FormatMessage(int dwFlags,
ref IntPtr lpSource,
int dwMessageId,
int dwLanguageId,
ref String lpBuffer, int nSize,
IntPtr *Arguments);
DPAPI 関数で使用する次の構造体定義と定数を追加します。
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] internal struct DATA_BLOB { public int cbData; public IntPtr pbData; } [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] internal struct CRYPTPROTECT_PROMPTSTRUCT { public int cbSize; public int dwPromptFlags; public IntPtr hwndApp; public String szPrompt; } static private IntPtr NullPtr = ((IntPtr)((int)(0))); private const int CRYPTPROTECT_UI_FORBIDDEN = 0x1; private const int CRYPTPROTECT_LOCAL_MACHINE = 0x4;
Store という名前でパブリック列挙型をクラスに追加します。この列挙型は、DPAPI がコンピュータ ストアまたはユーザー ストアと組み合わせて使用されるかどうかを示します。
public enum Store {USE_MACHINE_STORE = 1, USE_USER_STORE};
- Store 型のプライベート メンバ変数をクラスに追加します。
private Store store;
- クラスの既定のコンストラクタを、次のコンストラクタに置き換えます。次のコンストラクタでは、Store 型のパラメータを受け取り、その値を store プライベート メンバ変数に代入します。
public DataProtector(Store tempStore)
{
store = tempStore;
}
次のパブリック Encrypt メソッドをクラスに追加します。
public byte[] Encrypt(byte[] plainText, byte[] optionalEntropy) { bool retVal = false; DATA_BLOB plainTextBlob = new DATA_BLOB(); DATA_BLOB cipherTextBlob = new DATA_BLOB(); DATA_BLOB entropyBlob = new DATA_BLOB(); CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT(); InitPromptstruct(ref prompt); int dwFlags; try { try { int bytesSize = plainText.Length; plainTextBlob.pbData = Marshal.AllocHGlobal(bytesSize); if(IntPtr.Zero == plainTextBlob.pbData) { throw new Exception("Unable to allocate plaintext buffer."); } plainTextBlob.cbData = bytesSize; Marshal.Copy(plainText, 0, plainTextBlob.pbData, bytesSize); } catch(Exception ex) { throw new Exception("Exception marshalling data. " + ex.Message); } if(Store.USE_MACHINE_STORE == store) {//Using the machine store, should be providing entropy. dwFlags = CRYPTPROTECT_LOCAL_MACHINE|CRYPTPROTECT_UI_FORBIDDEN; //Check to see if the entropy is null if(null == optionalEntropy) {//Allocate something optionalEntropy = new byte[0]; } try { int bytesSize = optionalEntropy.Length; entropyBlob.pbData = Marshal.AllocHGlobal(optionalEntropy.Length);; if(IntPtr.Zero == entropyBlob.pbData) { throw new Exception("Unable to allocate entropy data buffer."); } Marshal.Copy(optionalEntropy, 0, entropyBlob.pbData, bytesSize); entropyBlob.cbData = bytesSize; } catch(Exception ex) { throw new Exception("Exception entropy marshalling data. " + ex.Message); } } else {//Using the user store dwFlags = CRYPTPROTECT_UI_FORBIDDEN; } retVal = CryptProtectData(ref plainTextBlob, "", ref entropyBlob, IntPtr.Zero, ref prompt, dwFlags, ref cipherTextBlob); if(false == retVal) { throw new Exception("Encryption failed. " + GetErrorMessage(Marshal.GetLastWin32Error())); } } catch(Exception ex) { throw new Exception("Exception encrypting. " + ex.Message); } byte[] cipherText = new byte[cipherTextBlob.cbData]; Marshal.Copy(cipherTextBlob.pbData, cipherText, 0, cipherTextBlob.cbData); return cipherText; }
次のパブリック Decrypt メソッドをクラスに追加します。
public byte[] Decrypt(byte[] cipherText, byte[] optionalEntropy)
{
bool retVal = false;
DATA_BLOB plainTextBlob = new DATA_BLOB();
DATA_BLOB cipherBlob = new DATA_BLOB();
CRYPTPROTECT_PROMPTSTRUCT prompt = new CRYPTPROTECT_PROMPTSTRUCT();
InitPromptstruct(ref prompt);
try
{
try
{
int cipherTextSize = cipherText.Length;
cipherBlob.pbData = Marshal.AllocHGlobal(cipherTextSize);
if(IntPtr.Zero == cipherBlob.pbData)
{
throw new Exception("Unable to allocate cipherText buffer.");
}
cipherBlob.cbData = cipherTextSize;
Marshal.Copy(cipherText, 0, cipherBlob.pbData, cipherBlob.cbData);
}
catch(Exception ex)
{
throw new Exception("Exception marshalling data. " + ex.Message);
}
DATA_BLOB entropyBlob = new DATA_BLOB();
int dwFlags;
if(Store.USE_MACHINE_STORE == store)
{//Using the machine store, should be providing entropy.
dwFlags = CRYPTPROTECT_LOCAL_MACHINE|CRYPTPROTECT_UI_FORBIDDEN;
//Check to see if the entropy is null
if(null == optionalEntropy)
{//Allocate something
optionalEntropy = new byte[0];
}
try
{
int bytesSize = optionalEntropy.Length;
entropyBlob.pbData = Marshal.AllocHGlobal(bytesSize);
if(IntPtr.Zero == entropyBlob.pbData)
{
throw new Exception("Unable to allocate entropy buffer.");
}
entropyBlob.cbData = bytesSize;
Marshal.Copy(optionalEntropy, 0, entropyBlob.pbData, bytesSize);
}
catch(Exception ex)
{
throw new Exception("Exception entropy marshalling data. " +
ex.Message);
}
}
else
{//Using the user store
dwFlags = CRYPTPROTECT_UI_FORBIDDEN;
}
retVal = CryptUnprotectData(ref cipherBlob, null, ref entropyBlob,
IntPtr.Zero, ref prompt, dwFlags,
ref plainTextBlob);
if(false == retVal)
{
throw new Exception("Decryption failed. " +
GetErrorMessage(Marshal.GetLastWin32Error()));
}
//Free the blob and entropy.
if(IntPtr.Zero != cipherBlob.pbData)
{
Marshal.FreeHGlobal(cipherBlob.pbData);
}
if(IntPtr.Zero != entropyBlob.pbData)
{
Marshal.FreeHGlobal(entropyBlob.pbData);
}
}
catch(Exception ex)
{
throw new Exception("Exception decrypting. " + ex.Message);
}
byte[] plainText = new byte[plainTextBlob.cbData];
Marshal.Copy(plainTextBlob.pbData, plainText, 0, plainTextBlob.cbData);
return plainText;
}
次のプライベート ヘルパ メソッドをクラスに追加します。
private void InitPromptstruct(ref CRYPTPROTECT_PROMPTSTRUCT ps) { ps.cbSize = Marshal.SizeOf(typeof(CRYPTPROTECT_PROMPTSTRUCT)); ps.dwPromptFlags = 0; ps.hwndApp = NullPtr; ps.szPrompt = null; } private unsafe static String GetErrorMessage(int errorCode) { int FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100; int FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200; int FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000; int messageSize = 255; String lpMsgBuf = ""; int dwFlags = FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS; IntPtr ptrlpSource = new IntPtr(); IntPtr prtArguments = new IntPtr(); int retVal = FormatMessage(dwFlags, ref ptrlpSource, errorCode, 0, ref lpMsgBuf, messageSize, &prtArguments); if(0 == retVal) { throw new Exception("Failed to format message for error code " + errorCode + ". "); } return lpMsgBuf; }
[ビルド] メニューの [ソリューションのビルド] をクリックします。
2. アセンブリに厳密名を付ける (オプション)
マネージ DPAPI クラス ライブラリを (厳密名の付いた) Enterprise Services アプリケーションから呼び出す場合、DPAPI クラス ライブラリにも厳密名を付ける必要があります。ここでは、クラス ライブラリに厳密名を付ける手順について説明します。
マネージ DPAPI クラス ライブラリが (厳密名の付いていない) ASP.NET Web アプリケーションから直接呼び出される場合、この手順はスキップできます。
■ アセンブリに厳密名を付けるには
- コマンド プロンプトを開き、DataProtection プロジェクト フォルダに移動します。
- sn.exe ユーティリティを使用して、アセンブリの署名に使用するキーのペアを生成します。
sn -k dataprotection.snk
- Visual Studio .NET に戻って、Assemblyinfo.cs を開きます。
- AssemblyKeyFile 属性を探し、プロジェクト フォルダ内のキー ファイルへのパスを追加します。
[assembly: AssemblyKeyFile(@"..\..\dataprotection.snk")]
- [ビルド] メニューの [ソリューションのビルド] をクリックします。
参考資料
詳細については、次の関連ドキュメントを参照してください。
- このガイドの「パート IV : 参照」の「ASP.NET から DPAPI (コンピュータ ストア) を使用する方法」
- このガイドの「パート IV : 参照」の「ASP.NET と Enterprise Services から DPAPI (ユーザー ストア) を使用する方法」