I have come across a problem with binding to a PasswordBox
. It seems it\'s a security risk but I am using the MVVM pattern so I wish to bypass this. I found som
I spent a great deal of time looking at various solutions. I didn't like the decorators idea, behaviors mess up the validation UI, code behind... really?
The best one yet is to stick to a custom attached property and bind to your SecureString
property in your view model. Keep it in there for as long as you can. Whenever you'll need quick access to the plain password, temporarily convert it to an unsecure string using the code below:
namespace Namespace.Extensions
{
using System;
using System.Runtime.InteropServices;
using System.Security;
///
/// Provides unsafe temporary operations on secured strings.
///
[SuppressUnmanagedCodeSecurity]
public static class SecureStringExtensions
{
///
/// Converts a secured string to an unsecured string.
///
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;
}
///
/// Copies the existing instance of a secure string into the destination, clearing the destination beforehand.
///
public static void CopyInto(this SecureString source, SecureString destination)
{
destination.Clear();
foreach (var chr in source.ToUnsecuredString())
{
destination.AppendChar(chr);
}
}
///
/// Converts an unsecured string to a secured string.
///
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;
}
}
}
Make sure you allow the GC to collect your UI element, so resist the urge of using a static event handler for the PasswordChanged
event on the PasswordBox
.
I also discovered an anomaly where the control wasn't updating the UI when using the SecurePassword
property for setting it up, reason why I'm copying the password into Password
instead.
namespace Namespace.Controls
{
using System.Security;
using System.Windows;
using System.Windows.Controls;
using Namespace.Extensions;
///
/// Creates a bindable attached property for the property.
///
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);
}
///
/// Encapsulated event logic
///
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;
}
}
}
}
}
And the XAML usage:
My property in the view model looked like this:
[RequiredSecureString]
public SecureString LogonPassword
{
get
{
return _logonPassword;
}
set
{
_logonPassword = value;
NotifyPropertyChanged(nameof(LogonPassword));
}
}
The RequiredSecureString
is just a simple custom validator that has the following logic:
[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;
}
}
Here you have it. A complete and tested pure MVVM solution.