Bind SecurePassword to ViewModel

2019-02-19 15:09发布

问题:

I try to bind the SecurePassword property of a PasswordBox to my ViewModel with a custom Behavior. Sadly it doesn't work properly.

Basically I added a property to the Behavior which contains the target property of my ViewModel.

Any ideas why it doesn't work?

PS: I am currently on the way home without my laptop, I gonna update the question with my code in about 15 minutes. But would be nice if someone would post ideas or sth.

EDIT

As I promised, here is some code :)

The Behavior first:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Interactivity;
using System.Security;

namespace Knerd.Behaviors {
    public class PasswordChangedBehavior : Behavior<PasswordBox> {

        protected override void OnAttached() {
            AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
            base.OnAttached();
        }

        private void AssociatedObject_PasswordChanged(object sender, RoutedEventArgs e) {
            if (AssociatedObject.Password != null)
                TargetPassword = AssociatedObject.SecurePassword;
        }

        protected override void OnDetaching() {
            AssociatedObject.PasswordChanged -= AssociatedObject_PasswordChanged;
            base.OnDetaching();
        }

        public SecureString TargetPassword {
            get { return (SecureString)GetValue(TargetPasswordProperty); }
            set { SetValue(TargetPasswordProperty, value); }
        }

        // Using a DependencyProperty as the backing store for TargetPassword.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetPasswordProperty = DependencyProperty.Register("TargetPassword", typeof(SecureString), typeof(PasswordChangedBehavior), new PropertyMetadata(default(SecureString)));
    }
}

The PasswordBox:

<PasswordBox Grid.Column="1" Grid.Row="1" Margin="5" Width="300" MinWidth="200">
    <i:Interaction.Behaviors>
        <behaviors:PasswordChangedBehavior TargetPassword="{Binding Password}" />
    </i:Interaction.Behaviors>
</PasswordBox>

And last, the part of my ViewModel.

private SecureString password;

public SecureString Password {
    get { return password; }
    set {
        if (password != value) {
            password = value;
            OnPropertyChanged("Password");
        }
    }
}

I hope anyone can help, atm I use the codebehind version but I rather wouldn't.

EDIT 2

What actually doesn't work is, that the TargetPassword property doesn't update the property of my ViewModel

回答1:

Create an attach property

public static class PasswordBoxAssistant
{
 public static readonly DependencyProperty BoundPassword =
      DependencyProperty.RegisterAttached("BoundPassword", typeof(string), typeof(PasswordBoxAssistant), new PropertyMetadata(string.Empty, OnBoundPasswordChanged));

  public static readonly DependencyProperty BindPassword = DependencyProperty.RegisterAttached(
      "BindPassword", typeof (bool), typeof (PasswordBoxAssistant), new PropertyMetadata(false, OnBindPasswordChanged));


  private static readonly DependencyProperty UpdatingPassword =
      DependencyProperty.RegisterAttached("UpdatingPassword", typeof(bool), typeof(PasswordBoxAssistant), new PropertyMetadata(false));

  private static void OnBoundPasswordChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
      PasswordBox box = d as PasswordBox;

      // only handle this event when the property is attached to a PasswordBox
      // and when the BindPassword attached property has been set to true
      if (d == null || !GetBindPassword(d))
      {
          return;
      }

      // avoid recursive updating by ignoring the box's changed event
      box.PasswordChanged -= HandlePasswordChanged;

      string newPassword = (string)e.NewValue;

      if (!GetUpdatingPassword(box))
      {
          box.Password = newPassword;
      }

      box.PasswordChanged += HandlePasswordChanged;
  }

  private static void OnBindPasswordChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
  {
      // when the BindPassword attached property is set on a PasswordBox,
      // start listening to its PasswordChanged event

      PasswordBox box = dp as PasswordBox;

      if (box == null)
      {
          return;
      }

      bool wasBound = (bool)(e.OldValue);
      bool needToBind = (bool)(e.NewValue);

      if (wasBound)
      {
          box.PasswordChanged -= HandlePasswordChanged;
      }

      if (needToBind)
      {
          box.PasswordChanged += HandlePasswordChanged;
      }
  }

  private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
  {
      PasswordBox box = sender as PasswordBox;

      // set a flag to indicate that we're updating the password
      SetUpdatingPassword(box, true);
      // push the new password into the BoundPassword property
      SetBoundPassword(box, box.Password);
      SetUpdatingPassword(box, false);
  }

  public static void SetBindPassword(DependencyObject dp, bool value)
  {
      dp.SetValue(BindPassword, value);
  }

  public static bool GetBindPassword(DependencyObject dp)
  {
      return (bool)dp.GetValue(BindPassword);
  }

  public static string GetBoundPassword(DependencyObject dp)
  {
      return (string)dp.GetValue(BoundPassword);
  }

  public static void SetBoundPassword(DependencyObject dp, string value)
  {
      dp.SetValue(BoundPassword, value);
  }

  private static bool GetUpdatingPassword(DependencyObject dp)
  {
      return (bool)dp.GetValue(UpdatingPassword);
  }

  private static void SetUpdatingPassword(DependencyObject dp, bool value)
  {
      dp.SetValue(UpdatingPassword, value);
  }
}

And in your XAML

<Page xmlns:ff="clr-namespace:FunctionalFun.UI">
<!-- [Snip] -->
  <PasswordBox x:Name="PasswordBox"
      ff:PasswordBoxAssistant.BindPassword="true"  ff:PasswordBoxAssistant.BoundPassword="{Binding Path=Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">

</Page>

You probably don't want to do this anyway but if you really want to go ahead.

The reason the WPF/Silverlight PasswordBox doesn't expose a DP for the Password property is security related. If WPF/Silverlight were to keep a DP for Password it would require the framework to keep the password itself unencrypted in memory. Which is considered quite a troublesome security attack vector. The PasswordBox uses encrypted memory (of sorts) and the only way to access the password is through the CLR property.

I would suggest that when accessing the PasswordBox.Password CLR property you'd refrain from placing it in any variable or as a value for any property. Keeping your password in plain text on the client machine RAM is a security no-no.

SecurePassword cannot be done with bindings.

.NET documentation explains why the PasswordBox was not made bindable in the first place.

An alternative solution is to put the PasswordBox in your ViewModelpublic class LoginViewModel

public class LoginViewModel
{
   // other properties here

   public PasswordBox Password
   {
      get { return m_passwordBox; }
   }

   // Executed when the Login button is clicked.
   private void LoginExecute()
   {
      var password = Password.SecurePassword;

      // do more stuff...
   }
}

Yes, you are violating ViewModel best practices here, but

  1. best practices are "recommendations that work well in most cases" rather than strict rules and
  2. writing simple, easy-to-read, maintainable code and avoiding unnecessary complexity is also one of those "best practice" rules (which might be violated slightly by the "attached property" workaround).


回答2:

I think I found a kinda weird solution. Please improve if there is sth to improve :)

I just changed it like so:

The Behavior:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Interactivity;
using System.Security;

namespace Knerd.Behaviors {
    public class PasswordChangedBehavior : Behavior<PasswordBox> {

        protected override void OnAttached() {
            AssociatedObject.PasswordChanged += AssociatedObject_PasswordChanged;
            base.OnAttached();
        }

        private void AssociatedObject_PasswordChanged(object sender, RoutedEventArgs e) {
            if (AssociatedObject.SecurePassword != null)
                AssociatedObject.DataContext = AssociatedObject.SecurePassword.Copy();
        }

        protected override void OnDetaching() {
            AssociatedObject.PasswordChanged -= AssociatedObject_PasswordChanged;
            base.OnDetaching();
        }

        // Using a DependencyProperty as the backing store for TargetPassword.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TargetPasswordProperty = DependencyProperty.Register("TargetPassword", typeof(SecureString), typeof(PasswordChangedBehavior), new PropertyMetadata(default(SecureString)));
    }
}

The ViewModel didn't change at all, but here is my View:

<PasswordBox Grid.Column="1" Grid.Row="1" Margin="5" Width="300" MinWidth="200" DataContext="{Binding Password, Mode=TwoWay}">
    <i:Interaction.Behaviors>
        <behaviors:PasswordChangedBehavior />
    </i:Interaction.Behaviors>
</PasswordBox>

That works just perfect, without exposing the plaintext password.



标签: c# wpf mvvm