SecureStringクラスは、文字列を保持するStringクラスと似たクラスですが、SecureStringクラスに格納される文字列が自動的に暗号化される点でStringクラスとは異なります。 SecureStringクラスは、パスワードなどの文字列を格納するのに適しています。

SecureStringクラス

SecureStringクラスでは文字列が暗号化される点に加え、Disposeメソッドによって内容を破棄できる点でStringクラスと異なります。 Stringクラスではガベージコレクタによって破棄されるまでは文字列がメモリ上に残りますが、SecureStringクラスでは、Disposeメソッドを呼び出すことで不要になった時点で即座に破棄出来ます。 また、スレッドが中断されたりする場合などでも、確実に破棄されることがランタイムで保証されています(SecureStringクラスはCriticalFinalizerObjectクラスから派生しているため)。

SecureStringはStringからは生成することは出来ず、StringBuilderと似た操作で1文字ずつ追加する必要があります。 文字列の追加にはAppendCharメソッドInsertAtメソッド、削除にはRemoveAtメソッドなどを使います。 変更の必要が無くなった時点でMakeReadOnlyメソッドを呼ぶことで、文字列の内容を読み取り専用にし、変更できないようにできます。

SecureStringは、ProcessStartInfo.Passwordプロパティなどで使われる他、.NET Framework 4からはNetworkCredential.SecurePasswordプロパティでも使用出来るようになっています。

SecureStringの使用例

SecureStringの使用例をNetworkCredentialを使って見てみます。 この例では、HTTPで認証が必要なページにアクセスし、その内容を表示します。 1度目のリクエストではユーザ名・パスワードを指定せずにアクセスし、HTTP Basic, Digest等による認証が必要だった場合(401レスポンスとなった場合)には、NetworkCredentialでユーザ名・パスワードを指定してから再度取得を試みます。

using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Security;

class Sample {
  static void Main()
  {
    Uri requestUri = new Uri("http://example.net/login/");

    try {
      // ユーザ名・パスワードを指定せずにリクエストを送信
      HttpWebRequest req = WebRequest.Create(requestUri) as HttpWebRequest;

      using (HttpWebResponse resp = req.GetResponse() as HttpWebResponse) {
        // 取得できた場合は、レスポンスの内容を表示
        PrintResponse(resp);
      }
    }
    catch (WebException ex) {
      // プロトコルエラーでない場合は例外を再スローして終了
      if (ex.Status != WebExceptionStatus.ProtocolError)
        throw;

      // HTTP 401 Unauthorized以外のレスポンスの場合は例外を再スローして終了
      if ((ex.Response as HttpWebResponse).StatusCode != HttpStatusCode.Unauthorized)
        throw;

      // レスポンスのWWW-Authenticateヘッダの内容を表示
      Console.WriteLine("401 Unauthorized");
      Console.WriteLine("WWW-Authenticate: {0}", ex.Response.Headers["WWW-Authenticate"]);

      HttpWebRequest req = WebRequest.Create(requestUri) as HttpWebRequest;
      NetworkCredential cred = new NetworkCredential();

      // ユーザ名・パスワードを取得してNetworkCredentialに設定
      cred.UserName = ReadUsername();

      using (SecureString password = ReadPassword()) {
        cred.SecurePassword = password;

        // NetworkCredentialを指定して再度リクエストを送信
        req.Credentials = cred;

        using (HttpWebResponse resp = req.GetResponse() as HttpWebResponse) {
          // 取得できた場合は、レスポンスの内容を表示
          PrintResponse(resp);
        }
      }
    }
  }

  // ユーザ名を取得するメソッド
  static string ReadUsername()
  {
    Console.Write("Username: ");

    return Console.ReadLine();
  }

  // パスワードを取得するメソッド
  static SecureString ReadPassword()
  {
    Console.Write("Password: ");

    SecureString password = new SecureString();

    for (;;) {
      // キー入力が行われるまで待機する
      if (!Console.KeyAvailable) {
        Thread.Sleep(50);
        continue;
      }

      // 入力されたキー情報を読む(エコーバックはしない)
      ConsoleKeyInfo keyinfo = Console.ReadKey(true);

      switch (keyinfo.Key) {
        case ConsoleKey.Enter:
          // Enterキーが押された場合は、SecureStringを読み取り専用にして返す
          Console.WriteLine();
          password.MakeReadOnly();
          return password;

        case ConsoleKey.Backspace:
          // BackSpaceキーが押された場合は、SecureStringに格納されている最後の一文字を削除する
          if (0 < password.Length) {
            password.RemoveAt(password.Length - 1);
          }
          break;

        default:
          if (Char.IsLetterOrDigit(keyinfo.KeyChar) || Char.IsSymbol(keyinfo.KeyChar)) {
            // 英数字・記号のキーが押された場合は、SecureStringに文字を追加する
            password.AppendChar(keyinfo.KeyChar);
          }
          else {
            // それ以外の場合はビープ音を鳴らす
            Console.Beep();
          }
          break;
      }
    }
  }

  // レスポンスの内容を表示するメソッド
  static void PrintResponse(HttpWebResponse response)
  {
    Console.WriteLine("----Response----");

    using (Stream stream = response.GetResponseStream()) {
      StreamReader reader = new StreamReader(stream);

      Console.WriteLine(reader.ReadToEnd());
    }
  }
}
Imports System
Imports System.IO
Imports System.Net
Imports System.Threading
Imports System.Security

Class Sample
  Shared Sub Main()
    Dim requestUri As New Uri("http://example.net/login/")

    Try
      ' ユーザ名・パスワードを指定せずにリクエストを送信
      Dim req As HttpWebRequest = DirectCast(WebRequest.Create(requestUri), HttpWebRequest)

      Using resp As HttpWebResponse = DirectCast(req.GetResponse(), HttpWebResponse)
        ' 取得できた場合は、レスポンスの内容を表示
        PrintResponse(resp)
      End Using
    Catch ex As WebException
      ' プロトコルエラーでない場合は例外を再スローして終了
      If ex.Status <> WebExceptionStatus.ProtocolError Then Throw
      ' HTTP 401 Unauthorized以外のレスポンスの場合は例外を再スローして終了
      If DirectCast(ex.Response, HttpWebResponse).StatusCode <> HttpStatusCode.Unauthorized Then Throw

      ' レスポンスのWWW-Authenticateヘッダの内容を表示
      Console.WriteLine("401 Unauthorized")
      Console.WriteLine("WWW-Authenticate: {0}", ex.Response.Headers("WWW-Authenticate"))

      Dim req As HttpWebRequest = DirectCast(WebRequest.Create(requestUri), HttpWebRequest)
      Dim cred As New NetworkCredential()

      ' ユーザ名・パスワードを取得してNetworkCredentialに設定
      cred.UserName = ReadUsername()

      Using password As SecureString = ReadPassword()
        cred.SecurePassword = password

        ' NetworkCredentialを指定して再度リクエストを送信
        req.Credentials = cred

        Using resp As HttpWebResponse = DirectCast(req.GetResponse(), HttpWebResponse)
          ' 取得できた場合は、レスポンスの内容を表示
          PrintResponse(resp)
        End Using
      End Using
    End Try
  End Sub

  ' ユーザ名を取得するメソッド
  Shared Function ReadUsername As String
    Console.Write("Username: ")

    Return Console.ReadLine()
  End Function

  ' パスワードを取得するメソッド
  Shared Function ReadPassword() As SecureString
    Console.Write("Password: ")

    Dim password As New SecureString()

    Do
      ' キー入力が行われるまで待機する
      If Not Console.KeyAvailable Then
        Thread.Sleep(50)
        Continue Do
      End If

      ' 入力されたキー情報を読む(エコーバックはしない)
      Dim keyinfo As ConsoleKeyInfo = Console.ReadKey(True)

      Select Case keyinfo.Key
        Case ConsoleKey.Enter
          ' Enterキーが押された場合は、SecureStringを読み取り専用にして返す
          Console.WriteLine()
          password.MakeReadOnly()
          Return password

        Case ConsoleKey.Backspace
          ' BackSpaceキーが押された場合は、SecureStringに格納されている最後の一文字を削除する
          If 0 < password.Length Then password.RemoveAt(password.Length - 1)

        Case Else
          If Char.IsLetterOrDigit(keyinfo.KeyChar) Or Char.IsSymbol(keyinfo.KeyChar) Then
            ' 英数字・記号のキーが押された場合は、SecureStringに文字を追加する
            password.AppendChar(keyinfo.KeyChar)
          Else
            ' それ以外の場合はビープ音を鳴らす
            Console.Beep()
          End If
      End Select
    Loop
  End Function

  ' レスポンスの内容を表示するメソッド
  Shared Sub PrintResponse(ByVal response As HttpWebResponse)
    Console.WriteLine("----Response----")

    Using stream As Stream = response.GetResponseStream()
      Dim reader As New StreamReader(stream)

      Console.WriteLine(reader.ReadToEnd())
    End Using
  End Sub
End Class
Basic認証が必要なページの場合
401 Unauthorized
WWW-Authenticate: Basic realm="login page"
Username: user
Password:
----Response----
<html>
  <body>
    <h1>hello!</h1>
  </body>
</html>

Digest認証が必要なページの場合
401 Unauthorized
WWW-Authenticate: Digest realm="login page", nonce="30c1448b39ef1a5ec035b7bbcb34f99a", qop="auth"
Username: user
Password:
----Response----
<html>
  <body>
    <h1>hello!</h1>
  </body>
</html>

認証が不要なページの場合
----Response----
<html>
  <body>
    <h1>hello!</h1>
  </body>
</html>

認証に失敗した場合
401 Unauthorized
WWW-Authenticate: Basic realm="login page"
Username: user
Password:

ハンドルされていない例外: System.Net.WebException: リモート サーバーがエラーを返しました: (401) 許可されていません
   場所 System.Net.HttpWebRequest.GetResponse()
   場所 Sample.Main()

上記の例において、SecureStringに格納したパスワードが暗号化された状態であるのはリクエストが送信されるまでである点に注意してください。 リクエストがサーバに送信される時点で、SecureStringから暗号化が解除されたパスワードが取り出されます。 Digest認証では取り出されたパスワードがハッシュ化された上で送信されますが、Basic認証ではパスワードが平文で送信されます。

SecureStringはあくまでメモリ上の文字列を暗号化しておくためのものであり、通信経路上での文字列の暗号化には寄与しません。 したがって、上記の例のようにSecureStringが保持している文字列を通信経路を介して送受信する場合は、HTTPではなくHTTPSを使う、SslStreamクラスと組み合わせてSSL/TLSを使うなどにより通信経路の暗号化もあわせて行うようにする必要があります。

この例では、Console.ReadKeyメソッドを使うことにより、入力されたパスワードがコンソール上に表示されない(エコーバックしない)ようにしています。 実装の詳細、より適切な実装方法についてはエコーバックせずに文字列を入力する(Console.ReadKey)を参照してください。

Stringへの変換

StringBuilderとは異なり、SecureString.ToStringメソッドを呼び出してもSecureStringに格納されている文字列を取得することは出来ません。 SecureStringから文字列を取得するには、Marshalクラスのメソッドを使って一度メモリ上にコピーしてから、その内容をStringとして取得します。 次の表は、SecureStringから文字列を取り出すために使用できるMarshalクラスのメソッドの組み合わせです。

Marshalクラスのメソッドの組み合わせ
メモリ上への文字列のコピー
(SecureStringTo*)
ポインタから文字列への変換
(PtrToString*)
コピーした文字列の開放
(ZeroFree*)
SecureStringToBSTR PtrToStringBSTR ZeroFreeBSTR
SecureStringToCoTaskMemAnsi PtrToStringAnsi ZeroFreeCoTaskMemAnsi
SecureStringToCoTaskMemUnicode PtrToStringUni ZeroFreeCoTaskMemUnicode
SecureStringToGlobalAllocAnsi PtrToStringAnsi ZeroFreeGlobalAllocAnsi
SecureStringToGlobalAllocUnicode PtrToStringUni ZeroFreeGlobalAllocUnicode

なお、SecureStringTo*メソッドを呼び出して取得したポインタは、必ず対応するZeroFree*で開放する必要があります。 以下は、SecureStringに文字列を格納し、再びStringとして取得する例です。

using System;
using System.Runtime.InteropServices;
using System.Security;

class Sample {
  static void Main()
  {
    SecureString ss = new SecureString();

    ss.AppendChar('h');
    ss.AppendChar('o');
    ss.AppendChar('g');
    ss.AppendChar('e');
    ss.AppendChar('ほ');
    ss.AppendChar('げ');

    Console.WriteLine(ss.ToString());
    Console.WriteLine(ConvertToString(ss));
  }

  static string ConvertToString(SecureString ss)
  {
    IntPtr ptr = IntPtr.Zero;

    try {
      ptr = Marshal.SecureStringToBSTR(ss);

      return Marshal.PtrToStringBSTR(ptr);
    }
    finally {
      if (ptr != IntPtr.Zero) Marshal.ZeroFreeBSTR(ptr);
    }
  }
}
Imports System
Imports System.Runtime.InteropServices
Imports System.Security

Class Sample
  Shared Sub Main()
    Dim ss As New SecureString()

    ss.AppendChar("h"c)
    ss.AppendChar("o"c)
    ss.AppendChar("g"c)
    ss.AppendChar("e"c)
    ss.AppendChar("ほ"c)
    ss.AppendChar("げ"c)

    Console.WriteLine(ss.ToString())
    Console.WriteLine(ConvertToString(ss))
  End Sub

  Shared Function ConvertToString(ByVal ss As SecureString) As String
    Dim ptr As IntPtr = IntPtr.Zero

    Try
      ptr = Marshal.SecureStringToBSTR(ss)

      Return Marshal.PtrToStringBSTR(ptr)
    Finally
      If ptr <> IntPtr.Zero Then Marshal.ZeroFreeBSTR(ptr)
    End Try
  End Function
End Class
実行結果
System.Security.SecureString
hogeほげ