LoginSignup
21
22

More than 5 years have passed since last update.

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

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

PasswordBox コントロールのPasswordプロパティにバインドできない

―のは、仕様です。

  • PasswordプロパティにはセキュリティリスクがあるのでSecurePasswordプロパティを使いましょう(MSDNよりSecurePasswordプロパティSecureString )
  • Passwordプロパティがパインドできないのは、この辺りに理由がありそうですが、ちょっと不便です。
  • MVVMライブラリのLivet を使えばバインドできますし(PasswordBoxBindingSupportBehavior)、他にも「PasswordBox Password バインド」等でググればバインドする方法はすぐに調べることができます。

でも、せっかくだからSecurePasswordをセキュアのままバインドしたい

ということで、SecurePasswordをバインド可能なビヘイビアを作ってみました。

SecureStringの扱い

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

SecureStringの解説 にあるように、SecureStrngは、System.Runtime.InteropServices.Marshalクラスのメソッドを使って文字列をアンマネージドメモリに展開して扱う必要があります。

SecureStringの比較
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にコピー
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
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での使用例
<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に入力されたパスワードとアプリ側で保管しているパスワードとの比較にも、今回作成した比較の拡張メソッドが使えると思います。
ただ、アプリ側で保管しているパスワードを保管先から取り出して平文で扱う際に、マネージドな領域に保持してしまうと意味が無いので、こちらの処理にも気を遣う必要がありそうです。

21
22
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
22