私はさまざまな解決策を検討するのに長い時間を費やしました。私はデコレーターのアイデアが好きではなかった、ビヘイビアーが検証UIを台無しにした、コードビハインド...本当に?
最善の方法は、カスタムアタッチプロパティに固執し、SecureString
ビューモデルのプロパティにバインドすることです。できるだけ長くそこに保管してください。プレーンなパスワードにすばやくアクセスする必要がある場合は、以下のコードを使用して一時的に安全でない文字列に変換します。
namespace Namespace.Extensions
{
using System;
using System.Runtime.InteropServices;
using System.Security;
/// <summary>
/// Provides unsafe temporary operations on secured strings.
/// </summary>
[SuppressUnmanagedCodeSecurity]
public static class SecureStringExtensions
{
/// <summary>
/// Converts a secured string to an unsecured string.
/// </summary>
public static string ToUnsecuredString(this SecureString secureString)
{
// copy&paste from the internal System.Net.UnsafeNclNativeMethods
IntPtr bstrPtr = IntPtr.Zero;
if (secureString != null)
{
if (secureString.Length != 0)
{
try
{
bstrPtr = Marshal.SecureStringToBSTR(secureString);
return Marshal.PtrToStringBSTR(bstrPtr);
}
finally
{
if (bstrPtr != IntPtr.Zero)
Marshal.ZeroFreeBSTR(bstrPtr);
}
}
}
return string.Empty;
}
/// <summary>
/// Copies the existing instance of a secure string into the destination, clearing the destination beforehand.
/// </summary>
public static void CopyInto(this SecureString source, SecureString destination)
{
destination.Clear();
foreach (var chr in source.ToUnsecuredString())
{
destination.AppendChar(chr);
}
}
/// <summary>
/// Converts an unsecured string to a secured string.
/// </summary>
public static SecureString ToSecuredString(this string plainString)
{
if (string.IsNullOrEmpty(plainString))
{
return new SecureString();
}
SecureString secure = new SecureString();
foreach (char c in plainString)
{
secure.AppendChar(c);
}
return secure;
}
}
}
GCがUI要素を収集できるようにしてください。そうすれば、のPasswordChanged
イベントに静的イベントハンドラーを使用する衝動に抵抗できますPasswordBox
。また、SecurePassword
プロパティを使用して設定するときにコントロールがUIを更新しないという異常を発見しPassword
ました。代わりに、パスワードをにコピーする理由です。
namespace Namespace.Controls
{
using System.Security;
using System.Windows;
using System.Windows.Controls;
using Namespace.Extensions;
/// <summary>
/// Creates a bindable attached property for the <see cref="PasswordBox.SecurePassword"/> property.
/// </summary>
public static class PasswordBoxHelper
{
// an attached behavior won't work due to view model validation not picking up the right control to adorn
public static readonly DependencyProperty SecurePasswordBindingProperty = DependencyProperty.RegisterAttached(
"SecurePassword",
typeof(SecureString),
typeof(PasswordBoxHelper),
new FrameworkPropertyMetadata(new SecureString(),FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, AttachedPropertyValueChanged)
);
private static readonly DependencyProperty _passwordBindingMarshallerProperty = DependencyProperty.RegisterAttached(
"PasswordBindingMarshaller",
typeof(PasswordBindingMarshaller),
typeof(PasswordBoxHelper),
new PropertyMetadata()
);
public static void SetSecurePassword(PasswordBox element, SecureString secureString)
{
element.SetValue(SecurePasswordBindingProperty, secureString);
}
public static SecureString GetSecurePassword(PasswordBox element)
{
return element.GetValue(SecurePasswordBindingProperty) as SecureString;
}
private static void AttachedPropertyValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// we'll need to hook up to one of the element's events
// in order to allow the GC to collect the control, we'll wrap the event handler inside an object living in an attached property
// don't be tempted to use the Unloaded event as that will be fired even when the control is still alive and well (e.g. switching tabs in a tab control)
var passwordBox = (PasswordBox)d;
var bindingMarshaller = passwordBox.GetValue(_passwordBindingMarshallerProperty) as PasswordBindingMarshaller;
if (bindingMarshaller == null)
{
bindingMarshaller = new PasswordBindingMarshaller(passwordBox);
passwordBox.SetValue(_passwordBindingMarshallerProperty, bindingMarshaller);
}
bindingMarshaller.UpdatePasswordBox(e.NewValue as SecureString);
}
/// <summary>
/// Encapsulated event logic
/// </summary>
private class PasswordBindingMarshaller
{
private readonly PasswordBox _passwordBox;
private bool _isMarshalling;
public PasswordBindingMarshaller(PasswordBox passwordBox)
{
_passwordBox = passwordBox;
_passwordBox.PasswordChanged += this.PasswordBoxPasswordChanged;
}
public void UpdatePasswordBox(SecureString newPassword)
{
if (_isMarshalling)
{
return;
}
_isMarshalling = true;
try
{
// setting up the SecuredPassword won't trigger a visual update so we'll have to use the Password property
_passwordBox.Password = newPassword.ToUnsecuredString();
// you may try the statement below, however the benefits are minimal security wise (you still have to extract the unsecured password for copying)
//newPassword.CopyInto(_passwordBox.SecurePassword);
}
finally
{
_isMarshalling = false;
}
}
private void PasswordBoxPasswordChanged(object sender, RoutedEventArgs e)
{
// copy the password into the attached property
if (_isMarshalling)
{
return;
}
_isMarshalling = true;
try
{
SetSecurePassword(_passwordBox, _passwordBox.SecurePassword.Copy());
}
finally
{
_isMarshalling = false;
}
}
}
}
}
そして、XAMLの使用法:
<PasswordBox controls:PasswordBoxHelper.SecurePassword="{Binding LogonPassword, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}">
ビューモデルのプロパティは次のようになりました。
[RequiredSecureString]
public SecureString LogonPassword
{
get
{
return _logonPassword;
}
set
{
_logonPassword = value;
NotifyPropertyChanged(nameof(LogonPassword));
}
}
これRequiredSecureString
は、次のロジックを持つ単純なカスタムバリデータです。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class RequiredSecureStringAttribute:ValidationAttribute
{
public RequiredSecureStringAttribute()
:base("Field is required")
{
}
public override bool IsValid(object value)
{
return (value as SecureString)?.Length > 0;
}
}
ここにあります。テスト済みの完全なMVVMソリューション。