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