[WPF]PasswordBoxのセキュアなパスワードSecurePasswordにバインドしたい

- 2018/10/21 @junerさんのご指摘によりSecureStringExクラスのEqualsメソッドの名前をBStrEqualsに修正しました

### PasswordBox コントロールのPasswordプロパティにバインドできない ###
―のは、仕様です。

- PasswordプロパティにはセキュリティリスクがあるのでSecurePasswordプロパティを使いましょう(MSDNより[SecurePasswordプロパティ](http://msdn.microsoft.com/ja-jp/library/system.windows.controls.passwordbox.securepassword.aspx)、[SecureString](http://msdn.microsoft.com/ja-jp/library/system.security.securestring.aspx) )
- Passwordプロパティがパインドできないのは、この辺りに理由がありそうですが、ちょっと不便です。
- MVVMライブラリの[Livet](http://ugaya40.net/livet) を使えばバインドできますし(PasswordBoxBindingSupportBehavior)、他にも「PasswordBox Password バインド」等でググればバインドする方法はすぐに調べることができます。

### でも、せっかくだからSecurePasswordをセキュアのままバインドしたい ###
ということで、SecurePasswordをバインド可能なビヘイビアを作ってみました。

#### SecureStringの扱い####
まず、準備段階として扱いの面倒なSecureStringを操作する拡張メソッドを作ります。 

[SecureStringの解説](http://msdn.microsoft.com/ja-jp/library/system.security.securestring.aspx) にあるように、SecureStrngは、[System.Runtime.InteropServices.Marshal]()クラスのメソッドを使って文字列をアンマネージドメモリに展開して扱う必要があります。  
  
##### SecureStringの比較 #####

```csharp
public static class SecureStringEx
{
    public static bool BStrEquals(this SecureString a, SecureString b)
    {
        if (a == null && b == null)
         { return true; }

        if (a == null || b == null)
        { return false; }

        if (a.Length != b.Length)
        { return false; }

        var aPtr = Marshal.SecureStringToBSTR(a);
        var bPtr = Marshal.SecureStringToBSTR(b);
        try
        {
            return Enumerable.Range(0, a.Length)
              .All(i => Marshal.ReadInt16(aPtr, i) == Marshal.ReadInt16(bPtr, i));
        }
        finally
        {
            Marshal.ZeroFreeBSTR(aPtr);
            Marshal.ZeroFreeBSTR(bPtr);
        }
    }
}
```

- 比較するSecureStringの文字列をアンマネージドな領域にBSTRとしてコピーします。
- SecureStringから文字列を取得するにはMarshal.SecureStringToBSTR()メソッドを使用します。このメソッドの戻り値は、アンマネージ領域にコピーしたBSTRの先頭文字を指すポインタです。
- 次に、Marshal.ReadInt16()メソッドを使ってBSTRから2Byte(Charオブジェクト1つ分に相当)ずつ読みだして比較します。
- 文字数はSecureString.Lengthで取得します(Length=Charオブジェクト数)
- BSTRは用が済んだらMarshal.ZeroFreeBSTRを使って0で上書きしたうえでアンマネージドメモリを解放します

##### BSTRの文字列をSecureStringにコピー #####

```csharp
public static class SecureStringEx
{
    public static void CopyFromBSTR(this SecureString self, IntPtr bstr, int count)
    {
        self.Clear();
        var chars = Enumerable.Range(0, count)
            .Select(i => (char)Marshal.ReadInt16(bstr, i * 2));
        foreach (var c in chars)
        {
            self.AppendChar(c);
        }
    }
}
```
- コピー元のSecureStringからMarshal.SecureStringToBSTR()で取得したBSTRをパラメータとして渡します。
- コピー先のSecureStringへは一旦Clearで空にした後、AppendChar()メソッドで1文字ずつ追加していきます。
- 追加する文字はコピー元BSTRからMarshal.ReadInt16()で2Byteずつ取り出したCharオブジェクトです。
- 引数に渡したBSTRは、用が済んだらMarshal.ZeroFreeBSTR()できれいにしておきましょう。

##### SecurePasswordをバインド可能にするBehavior #####

```csharp
using System.Runtime.InteropServices;
using System.Security;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

public class SecurePasswordBindingBehavior : Behavior<PasswordBox>
{
    public SecureString SecurePassword
    {
        get { return (SecureString)GetValue(SecurePasswordProperty); }
        set { SetValue(SecurePasswordProperty, value); }
    }
    
    public static readonly DependencyProperty SecurePasswordProperty =
        DependencyProperty.Register("SecurePassword", 
        typeof(SecureString), 
        typeof(SecurePasswordBindingBehavior), 
        new PropertyMetadata(new SecureString(), SecurePasswordPropertyChanged));
       
    private static void SecurePasswordPropertyChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        var behavior = d as SecurePasswordBindingBehavior;
        if (d == null)
        { return; }

        var passwordBox = behavior.AssociatedObject as PasswordBox;
        if (passwordBox == null)
        { return; }

        var newPassword = e.NewValue as SecureString;
        if (newPassword == null)
        { return; }

        var oldPassword = e.OldValue as SecureString;
        if (newPassword.BStrEquals(oldPassword))
        { return; }

        var bstr = Marshal.SecureStringToBSTR(newPassword);
        try
        {
            passwordBox.SecurePassword.CopyFromBSTR(bstr, newPassword.Length);
        }
        finally
        {
            Marshal.ZeroFreeBSTR(bstr);
        }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PasswordChanged += PasswordBox_PasswordChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PasswordChanged -= PasswordBox_PasswordChanged;
        base.OnDetaching();
    }

    private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e)
    {
        SecurePassword = AssociatedObject.SecurePassword.Copy();
    }
}
```
- Behavior\<PasswordBox\>を継承したクラスを作ります。
- このクラスに、バインド対象となるSecurePasswordプロパティを依存関係プロパティとして追加します。
- Behaviorに関連付けたPasswordBoxのPasswordChangedイベントにハンドラを追加します。
- PasswordBox_PasswordChanged()ハンドラでは、依存関係プロパティにPawordBoxのSecurePasswordの値をコピーします。ここではCopy()メソッドの結果をそのまま代入しています。
- SecurePasswordPropertyChanged()メソッドは、依存関係プロパティ(バインディングソース側)の更新時に呼ばれます。
- ここで、準備しておいた拡張メソッドを使ってPasswordBox側のSecurePasswordプロパティを更新します。
- 更新前の値と比較して(SecureStringEx.BStrEquals())変化があった場合のみ、PasswordBoxのSecurePasswordプロパティに更新後の値をコピーします(SecureStringEx.CopyFromBSTR())。

##### XAMLでの使用例 #####

```xml
<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        Title="Input Password" Height="420" Width="800">
    <Grid>
        <PasswordBox Width="120" PasswordChar="*">
            <i:Interaction.Behaviors>
                <my:SecurePasswordBindingBehavior 
                   SecurePassword="{Binding SecurePassword, Mode=TwoWay}"/>
             </i:Interaction.Behaviors>
        </PasswordBox>
    </Grid>
</Window>
```
### さいごに ###
PasswoerdBoxに入力されたパスワードとアプリ側で保管しているパスワードとの比較にも、今回作成した比較の拡張メソッドが使えると思います。
ただ、アプリ側で保管しているパスワードを保管先から取り出して平文で扱う際に、マネージドな領域に保持してしまうと意味が無いので、こちらの処理にも気を遣う必要がありそうです。