PasswordBox Bindings
Что я неправильно делаю? View:
<PasswordBox Name="SecretKey">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PasswordChanged">
<i:InvokeCommandAction Command="{Binding SecretKeyChangedCommand}" CommandParameter="{Binding ElementName=SecretKey}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</PasswordBox>
ViewModel:
private void ExecutePasswordChangedCommand(PasswordBox obj)
{
if (obj != null)
Password = obj.Password;
log.Debug(Password);
}
Я новичок во всем этом и наверняка я что-то не так делаю. По крайней мере меня смущает то, что я нигде не говорю про этот метод, поэтому логично что он никогда не произойдет.
Сам ViewModel, в котором я хочу ловить пароль наследуется от:
public class MainVM : BindableBase
Единственное что мне нужно - передать в MainVM значение. Можно по кнопке или как угодно.
Ответы (1 шт):
PasswordBox так задуман, чтобы не хранить вводимый пароль в string, так как string является неизменяемым (immutable) типом и может храниться неуправляемое количество времени в памяти. Чтобы этого избежать, разработчики WPF пошли на изврат (который сами же потом признали) - SecureString. Но несмотря на то, что они всеми силами попытаются избавиться от SecureString, при использовании PasswordBox без него не обойтись. Ну если конечно хочется сохранить безопасность на уровне.
Так как я не знаю Prism.Mvvm, я попробую угадать правильный код, но не гарантирую, что в нем не будет ошибок, если что - поправьте.
И первое, что я вам предложу сделать (ради безопасности конечно) - сознательно нарушить MVVM. То есть передать ссылку на экземпляр PasswordBox в View Model.
<StackPanel>
<PasswordBox Name="PwdBox"/>
<Button Content="Submit" Command="{Binding SumbitCommand}" CommandParameter="{Binding ElementName=PwdBox}"/>
</StackPanel>
А код View Model будет такой
private ICommand _submitCommand;
public ICommand SumbitCommand => _submitCommand ??= new DelegateCommand<object>(parameter =>
{
PasswordBox box = (PasswordBox)parameter;
// безопасный способ получить хэш
string password = GetSecureHash(box.SecurePassword);
log.Debug(password);
// НЕБЕЗОПАСНЫЙ способ получить хэш
string password2 = HashToString(SHA256.HashData(Encoding.Unicode.GetBytes(box.Password)));
log.Debug(password2);
}, parameter => parameter is PasswordBox box && box.SecurePassword.Length > 0);
private string GetSecureHash(SecureString secureString)
{
SHA256 sha256 = SHA256.Create();
Span<byte> hashBytes = stackalloc byte[sha256.HashSize >> 3];
IntPtr ptr = Marshal.SecureStringToBSTR(secureString);
unsafe
{
ReadOnlySpan<byte> source = new ReadOnlySpan<byte>((void*)ptr, secureString.Length * sizeof(char));
sha256.TryComputeHash(source, hashBytes, out _);
}
Marshal.ZeroFreeBSTR(ptr);
return HashToString(hashBytes);
}
private string HashToString(ReadOnlySpan<byte> bytes)
{
StringBuilder sb = new StringBuilder(bytes.Length * 2);
for (int i = 0; i < bytes.Length; i++)
sb.Append(bytes[i].ToString("x2"));
return sb.ToString();
}
Безопасность безопасного способа заключается в том, что мы получаем доступ к данным SecureString на ровно нужное нам время, а затем использованный участок памяти затираем нулями Marshal.ZeroFreeBSTR.
Директива unsafe позволяет работать напрямую с памятью как в C++, но требует разрешение на использование небезопасного кода. В свойствах проекта вам нужно будет поставить галочку "Разрешить небезопасный код", иначе unsafe не скомпилируется.
НЕБЕЗОПАСНЫЙ способ получить пароль создает string в памяти с открытым паролем, то есть сводит все старания разработчиков WPF достичь требуемого уровня безопасности на нет.
Мой вывод в лог показывает, что оба способа работают одинаково
06e44dc1b95c469f43aaccb49e93c36827626266eed5575eced74af9a016c9cd
06e44dc1b95c469f43aaccb49e93c36827626266eed5575eced74af9a016c9cd
Почему я не передал в команду как аргумент SecurePassword - да потому что это не работает, в команду прилетит пустой экземпляр данного класса, то же самое и с Password.
Почему я не передаю пароль каждый раз когда он изменяется а считываю только по кнопке - потому что зачем?
P.S. Используемая кодировка при генерации хэша - родная кодировка для string в C# Encoding.Unicode или по-другому она называется UTF-16. Но код можно доработать для использования UTF-8, если это критично, например, если вы хотите считать хеши паролей не только в этом приложении, а где-то еще, например на сайте при авторизации в этот же сервис.