WPFに標準で用意されているPasswordBoxはWinForms時代のものなので機能が貧弱です。一番イケてないのはPasswordBoxに入力したパスワードをViewModelにBindingできないこと。実際にはEventTriggerなどを使えばBindingできることはできるのですが、MVVMパターンが推奨されているWPFの標準UIコンポーネントとしてはあまりにお粗末です。また、デザインのカスタマイズ性もイマイチなので、これはもう自分で作った方が早いということになりました。
ViewModelでPasswordBoxの処理を作りこむ
どうやって作るかというと、普通のTextBoxを使いながらViewModel側で上手くPasswordBoxの振る舞いを作りこみます。TextBoxは特に目立った設定は行いません。
<TextBox Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"/>
このTextBoxにBindingしているプロパティがこちらです。
public string Password { get => new string('●', plainPassword.Length); set { if (value.Length < plainPassword.Length) { plainPassword= plainPassword.Substring(0, value.Length); } else if (value.Length > plainPassword.Length) { plainPassword+= value.Substring(plainPassword.Length, value.Length - plainPassword.Length); } else { plainPassword= value; } PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Password))); } } string plainPassword = string.Empty;
setterで入力値の差分を見て平文のパスワードを更新しています。getterでは平文のパスワードを全てマスキングして返すことで、View上では入力したパスワードが見えないというカラクリです。
平文を暗号化してセキュアにViewModelで保持する
先ほどの例では平文のplainPasswordはstring型でローカルに保持していました。このままでもViewではマスキングされているので覗き見などの防止としては十分です。しかし、string型で保持しているとメモリでは平文で保持されていることになり、要件によってはセキュリティ強度が足りないとなる場合があるかもしれません。この場合は、ローカルのplainPasswordをstring型ではなくSecureString型で保持することで解決できます。
string plainPassword { get => Marshal.PtrToStringUni(Marshal.SecureStringToGlobalAllocUnicode(cipher)); set { cipher.Clear(); foreach (var ch in value) { cipher.AppendChar(ch); } } } SecureString cipher = new SecureString();
SecureString型で保持することでメモリ上では暗号化されるので、もしマルウェア等でPCが監視されていても保持しているデータを解読することはできません。とはいえ、使用する際は復号するので要件によってはこれでもセキュリティ強度は足りないケースもあるかと思います。
実際の動作例
www.youtube.com
操作感やレスポンスは全く問題ないです。ソースコードはこちらに置いています。
github.com