На самом деле, первое правило безопасного хранения паролей — никогда не хранить пароли. Пусть за безопасность паролей отвечает сервер, например. Если это клиент-серверное приложение (каких очень много, и мы практически не замечаем, что ими пользуемся).
Но бывают ситуации, когда пароли (не хэши паролей, не контрольные суммы) все-таки надо хранить локально, как это делают, например, браузеры. И вот тут их надежно хранить не получается, или получается, но с диким геморроем. Для всяких крутых бизнес-систем, типа подписей на вашем контракте с Роскомпозором, это дело берут на себя криптопровайдеры, с их якобы крутыми разработчиками и стандартами.
А обычные клиентские приложения, типа браузера, могут предложить вам задать «пароль для паролей» (от базы хранящей пароли, запароленной главным паролем, который хранится в хранилище главных п… увлекся я). То есть, попросту предложить вам задать некий мастер-пароль, который вы будете держать в голове, и без этого пароля, браузер сам не расшифрует все остальные пароли. Так делают не только браузеры, но и всякие хранилища паролей типа keepass, и даже практически промышленные системы шифрования, типа Truecrypt. Они могут вам предложить сохранить мастер-пароль на «электронный ключ» или флешку и сгенерировать, за приемлемое время, неподбираемый пароль, размером, например 1Мб случайных символов.
Не всегда такая серьезная секретность нужна, некоторые пароли все-таки можно хранить, не заморачиваясь с параноидальной безопасностью. Например, пароль от условного прокси-сервера, который надо передать нашей программе, чтобы она через этот прокси соединилась с Интернетом. Или базой данных на сервере. Но, в любом случае, если вы разрабатываете приложение, использующее хранение паролей, в первую очередь, надо позволить пользователю его не сохранять, а вводить по требованию. Ну, так как же быть, если пароль не очень важный, и не хочется морочить голову пользователю постоянным его введением? Попробую ответить на этот вопрос.
А давайте, нагенерируем какой-нибудь мастер-пароль сами? Зависящий, например, от серийного номера материнской платы, потом, зашифруем хранимый пароль этим паролем с помощью любимого алгоритма шифрования.
А давайте!
Например, вот так
Минусы:
Практически выяснено, оказывается, запросы к WMI иногда глючат, и не всегда работают корректно. Иногда создается неотлаживаемый глюк на ровном месте.
Называется это дело DPAPI (ссылки на источники смотрите в конце заметки).
Итак, шифрование, примем для простоты, что нам нужно сохранить пароль для прокси-сервера:
1. У нас есть пароль в виде строки, который надо зашифровать и сохранить потом где-нибудь в конфиге программы. Преобразуем строку в массив байт:
byte[] pass = Encoding.UTF8.GetBytes(ProxyPassword);
2. Далее, нам нужно вычислить энтропию, хотя она опциональна, и не всегда нужна, но все-таки не будем отступать от рекомендаций, и вычислим ее. Как и соль, должна быть одинакова при шифровании и дешифровании.
Для объекта ProtectedData
, который в .NET Framework и занимается нашей задачей, энтропия должна быть передана ему в качестве массива байт (опционально).
Раз мы занялись энтропией, напишем функцию для ее получения. Пусть энтропией будет MD5-хэш от заданной строки.
private byte[] GetEntropy(string EntropyString)
{
MD5 md5 = MD5.Create();
return md5.ComputeHash(Encoding.UTF8.GetBytes(EntropyString));
}
В качестве строки для энтропии, используем что-нибудь не меняющееся, например адрес прокси-сервера и имя пользователя:
byte[] entropy = GetEntropy(ProxyAddress + ProxyUser);
3. Итак, пароль есть, энтропия тоже, осталось зашифровать:
byte[] crypted=ProtectedData.Protect(pass, entropy, DataProtectionScope.LocalMachine)
;
DataProtectionScope
, если объяснить простым языком, то это параметр, который позволяет DPAPI привязать шифрование либо к пользователю данной системы (надо использовать DataProtectionScope.CurrentUser
).
Либо к компьютеру (в смысле установленной ОС), тогда используется DataProtectionScope.LocalMachine
. В последнем случае, сохраненный пароль могут расшифровать все пользователи данного компьютера, тогда как в предыдущем — только пользователь, сохранивший пароль.
4. Далее преобразуем зашифрованный массив байтов в что-нибудь, что можно хранить, например в конфиге формата XML. В данном примере в строку BASE64
:
ProxyPassword = Convert.ToBase64String(crypted);
Расшифровка производится также, только в обратном порядке
-Раскодируем зашифрованную строку из BASE64
-Генерируем энтропию по тому же алгоритму, что использовался для шифрования
-Расшифровываем зашифрованный текст
-Используем расшифрованное где надо
Генерация энтропии:
private byte[] GetEntropy(string EntropyString) { MD5 md5 = MD5.Create(); return md5.ComputeHash(Encoding.UTF8.GetBytes(EntropyString)); }
Шифрованине:
private bool EncryptPassword() { if (string.IsNullOrEmpty(ProxyPassword)) return true; if (string.IsNullOrEmpty(ProxyAddress) || string.IsNullOrEmpty(ProxyUser)) return false; byte[] entropy = GetEntropy(ProxyAddress + ProxyUser); byte[] pass = Encoding.UTF8.GetBytes(ProxyPassword); byte[] crypted=ProtectedData.Protect(pass, entropy, DataProtectionScope.LocalMachine); ProxyPassword = Convert.ToBase64String(crypted); return true; }
Дешифрование:
private bool DecryptPassword() { if (string.IsNullOrEmpty(ProxyPassword)) return true; if (string.IsNullOrEmpty(ProxyAddress) || string.IsNullOrEmpty(ProxyUser)) return false; byte[] pass = null; try { pass = Convert.FromBase64String(ProxyPassword); } catch (Exception ex) { ConfigError = ex.Message; return false; } byte[] entropy = GetEntropy(ProxyAddress + ProxyUser); try { pass = ProtectedData.Unprotect(pass, entropy, DataProtectionScope.LocalMachine); } catch (Exception ex) { ConfigError = ex.Message; return false; } ProxyPassword = Encoding.UTF8.GetString(pass); return true; }
Пример на PasteBin (шифрование, дешифрование, генерация энтропии)
2 Responses to C# хранение паролей локально. На примере класса, хранящего настройки прокси.