Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Masked Passwordbox in WPF

Tags:

passwords

wpf

In iOS when you are typing a password into a field the last letter of the field is displayed but then is obfuscated when you type the next character. Is there a way to duplicate this behavior in WPF?

like image 964
aceinthehole Avatar asked Jul 01 '13 15:07

aceinthehole


1 Answers

If your usage for such a thing in a desktop app is justified, then you can do something like the following.

We had a similar requirement before and this is what I did.

  • I created a custom Passwordbox by deriving from TextBox and adding a new DP of type SecureString to it (pretty much the same concept as a normal PasswordBox). We do not lose any security benefits this way and can customize the visual behavior to our heart's content.
  • With this now we can use the Text of the TextBox as it's display-string and hold the actual password in the back-end SecureString DP and bind it to the VM.
  • We handle the PreviewTextInput and PreviewKeyDown events to manage all text changes in the control, including stuff like Key.Back, Key.Delete and the annoying Key.Space(which does not come through the PreviewTextInput

iOS Feel:

Couple more things to note for an exact iOS behavior.

  1. Last character is only shown while adding new characters to the "end of the current string" (FlowDirection independent)
  2. Editing characters in-between an existing string has no effect on mask.
  3. Last character shown is timer-dependent (becomes an "*" after a certain period if left idle)
  4. All copy-paste operations disabled in the control.

First 2 points can be handled pretty easily when detecting text changes, for the last one we can use a DispatcherTimer to work with the display-string accordingly.

So putting this all together we end up with:

/// <summary>
///   This class contains properties for CustomPasswordBox
/// </summary>
internal class CustomPasswordBox : TextBox {
  #region Member Variables
  /// <summary>
  ///   Dependency property to hold watermark for CustomPasswordBox
  /// </summary>
  public static readonly DependencyProperty PasswordProperty =
    DependencyProperty.Register(
      "Password", typeof(SecureString), typeof(CustomPasswordBox), new UIPropertyMetadata(new SecureString()));

  /// <summary>
  ///   Private member holding mask visibile timer
  /// </summary>
  private readonly DispatcherTimer _maskTimer;
  #endregion

  #region Constructors
  /// <summary>
  ///   Initialises a new instance of the LifeStuffPasswordBox class.
  /// </summary>
  public CustomPasswordBox() {
    PreviewTextInput += OnPreviewTextInput;
    PreviewKeyDown += OnPreviewKeyDown;
    CommandManager.AddPreviewExecutedHandler(this, PreviewExecutedHandler);
    _maskTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 1) };
    _maskTimer.Tick += (sender, args) => MaskAllDisplayText();
  }
  #endregion

  #region Commands & Properties
  /// <summary>
  ///   Gets or sets dependency Property implementation for Password
  /// </summary>
  public SecureString Password {
    get {
      return (SecureString)GetValue(PasswordProperty);
    }

    set {
      SetValue(PasswordProperty, value);
    }
  }
  #endregion

  #region Methods
  /// <summary>
  ///   Method to handle PreviewExecutedHandler events
  /// </summary>
  /// <param name="sender">Sender object</param>
  /// <param name="executedRoutedEventArgs">Event Text Arguments</param>
  private static void PreviewExecutedHandler(object sender, ExecutedRoutedEventArgs executedRoutedEventArgs) {
    if (executedRoutedEventArgs.Command == ApplicationCommands.Copy ||
        executedRoutedEventArgs.Command == ApplicationCommands.Cut ||
        executedRoutedEventArgs.Command == ApplicationCommands.Paste) {
      executedRoutedEventArgs.Handled = true;
    }
  }

  /// <summary>
  ///   Method to handle PreviewTextInput events
  /// </summary>
  /// <param name="sender">Sender object</param>
  /// <param name="textCompositionEventArgs">Event Text Arguments</param>
  private void OnPreviewTextInput(object sender, TextCompositionEventArgs textCompositionEventArgs) {
    AddToSecureString(textCompositionEventArgs.Text);
    textCompositionEventArgs.Handled = true;
  }

  /// <summary>
  ///   Method to handle PreviewKeyDown events
  /// </summary>
  /// <param name="sender">Sender object</param>
  /// <param name="keyEventArgs">Event Text Arguments</param>
  private void OnPreviewKeyDown(object sender, KeyEventArgs keyEventArgs) {
    Key pressedKey = keyEventArgs.Key == Key.System ? keyEventArgs.SystemKey : keyEventArgs.Key;
    switch (pressedKey) {
      case Key.Space:
        AddToSecureString(" ");
        keyEventArgs.Handled = true;
        break;
      case Key.Back:
      case Key.Delete:
        if (SelectionLength > 0) {
          RemoveFromSecureString(SelectionStart, SelectionLength);
        } else if (pressedKey == Key.Delete && CaretIndex < Text.Length) {
          RemoveFromSecureString(CaretIndex, 1);
        } else if (pressedKey == Key.Back && CaretIndex > 0) {
          int caretIndex = CaretIndex;
          if (CaretIndex > 0 && CaretIndex < Text.Length)
            caretIndex = caretIndex - 1;
          RemoveFromSecureString(CaretIndex - 1, 1);
          CaretIndex = caretIndex;
        }

        keyEventArgs.Handled = true;
        break;
    }
  }

  /// <summary>
  ///   Method to add new text into SecureString and process visual output
  /// </summary>
  /// <param name="text">Text to be added</param>
  private void AddToSecureString(string text) {
    if (SelectionLength > 0) {
      RemoveFromSecureString(SelectionStart, SelectionLength);
    }

    foreach (char c in text) {
      int caretIndex = CaretIndex;
      Password.InsertAt(caretIndex, c);
      MaskAllDisplayText();
      if (caretIndex == Text.Length) {
        _maskTimer.Stop();
        _maskTimer.Start();
        Text = Text.Insert(caretIndex++, c.ToString());
      } else {
        Text = Text.Insert(caretIndex++, "*");
      }
      CaretIndex = caretIndex;
    }
  }

  /// <summary>
  ///   Method to remove text from SecureString and process visual output
  /// </summary>
  /// <param name="startIndex">Start Position for Remove</param>
  /// <param name="trimLength">Length of Text to be removed</param>
  private void RemoveFromSecureString(int startIndex, int trimLength) {
    int caretIndex = CaretIndex;
    for (int i = 0; i < trimLength; ++i) {
      Password.RemoveAt(startIndex);
    }

    Text = Text.Remove(startIndex, trimLength);
    CaretIndex = caretIndex;
  }

  private void MaskAllDisplayText() {
    _maskTimer.Stop();
    int caretIndex = CaretIndex;
    Text = new string('*', Text.Length);
    CaretIndex = caretIndex;
  }
  #endregion
}

Working Sample:

Download Link

You can type something into the control and check the stored value shown below it.

In this sample, I've added a new DP of type string to just show that the control works fine. You'd obviously not want to have that DP (HiddenText) in your live-code, but I'd hope the sample helps to analyze if the class actually works :)

like image 138
Viv Avatar answered Oct 04 '22 04:10

Viv