How to bind to a PasswordBox in MVVM

前端 未结 30 2116
执念已碎
执念已碎 2020-11-22 11:50

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

30条回答
  •  小蘑菇
    小蘑菇 (楼主)
    2020-11-22 12:33

    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.

提交回复
热议问题