次の方法で共有


セキュリティ保護された ASP.NET アプリケーションの構築 : 認証、認定、および通信のセキュリティ保護 DPAPI ライブラリを作成する方法

patterns and practices home

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#® 開発ツールの知識も必要です。

要約

ここでは、次の手順について説明します。

  1. C# クラス ライブラリを作成する。
  2. アセンブリに厳密名を付ける (オプション)。

1. C# クラス ライブラリを作成する

ここでは、Encrypt メソッドおよび Decrypt メソッドを公開する C# クラス ライブラリを作成し、Win32 DPAPI 関数の呼び出しをカプセル化する手順について説明します。

■ C# クラス ライブラリを作成するには

  1. Visual Studio .NET を起動し、"DataProtection" という名前で新しい Visual C# クラス ライブラリを作成します。
  2. ソリューション エクスプローラで、class1.cs の名前を DataProtection.cs に変更します。
  3. DataProtection.cs 内の class1 の名前を DataProtector に変更し、それに応じて既定のコンストラクタの名前も変更します。
  4. ソリューション エクスプローラで、DataProtection を右クリックして、[プロパティ] をクリックします。
  5. [構成プロパティ] フォルダをクリックして、[セーフ モード以外のコード ブロックの許可] を [True] に設定します。
  6. [OK] をクリックして、プロパティ ダイアログ ボックスを閉じます。
  7. DataProtection.cs の先頭の既存の using ステートメントの下に次の using ステートメントを追加します。
using System.Text;
using System.Runtime.InteropServices;

  1. 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);

  1. 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;
    
  2. Store という名前でパブリック列挙型をクラスに追加します。この列挙型は、DPAPI がコンピュータ ストアまたはユーザー ストアと組み合わせて使用されるかどうかを示します。

public enum Store {USE_MACHINE_STORE = 1, USE_USER_STORE};
  1. Store 型のプライベート メンバ変数をクラスに追加します。
private Store store;
  1. クラスの既定のコンストラクタを、次のコンストラクタに置き換えます。次のコンストラクタでは、Store 型のパラメータを受け取り、その値を store プライベート メンバ変数に代入します。
public DataProtector(Store tempStore)
{
  store = tempStore;
}

  1. 次のパブリック 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;
    }
    
  2. 次のパブリック 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;
}

  1. 次のプライベート ヘルパ メソッドをクラスに追加します。

    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. [ビルド] メニューの [ソリューションのビルド] をクリックします。

2. アセンブリに厳密名を付ける (オプション)

マネージ DPAPI クラス ライブラリを (厳密名の付いた) Enterprise Services アプリケーションから呼び出す場合、DPAPI クラス ライブラリにも厳密名を付ける必要があります。ここでは、クラス ライブラリに厳密名を付ける手順について説明します。

マネージ DPAPI クラス ライブラリが (厳密名の付いていない) ASP.NET Web アプリケーションから直接呼び出される場合、この手順はスキップできます。

■ アセンブリに厳密名を付けるには

  1. コマンド プロンプトを開き、DataProtection プロジェクト フォルダに移動します。
  2. sn.exe ユーティリティを使用して、アセンブリの署名に使用するキーのペアを生成します。
sn -k dataprotection.snk
  1. Visual Studio .NET に戻って、Assemblyinfo.cs を開きます。
  2. AssemblyKeyFile 属性を探し、プロジェクト フォルダ内のキー ファイルへのパスを追加します。
[assembly: AssemblyKeyFile(@"..\..\dataprotection.snk")]
  1. [ビルド] メニューの [ソリューションのビルド] をクリックします。

参考資料

詳細については、次の関連ドキュメントを参照してください。

patterns and practices home