Безопасное хеширование паролей
В этом разделе разъясняются причины, стоящие за хешированием паролей в целях безопасности, а также эффективные методы хеширования.
- Почему я должен хешировать пароли пользователей в моем приложении?
- Почему популярные хеширующие функции, такие как md5 и sha1 не подходят для паролей?
- Если популярные хеширующие функции не подходят, как же я тогда должен хешировать свои пароли?
- Что такое соль?
- Как я должен хранить свою соль?
Почему я должен хешировать пароли пользователей в моем приложении?
Хеширование паролей является одним из самых основных соображений безопасности, которые необходимо сделать, при разработке приложения, принимающего пароли от пользователей. Без хеширования, пароли, хранящиеся в базе вашего приложения, могут быть украдены, например, если ваша база данных была скомпрометирована, а затем немедленно могут быть применены для компрометации не только вашего приложения, но и аккаунтов ваших пользователей на других сервисах, если они не используют уникальных паролей.
Применяя хеширующий алгоритм к пользовательским паролям перед сохранением их в своей базе данных, вы делаете невозможным разгадывание оригинального пароля для атакующего вашу базу данных, в то же время сохраняя возможность сравнения полученного хеша с оригинальным паролем.
Важно заметить, однако, что хеширование паролей защищает их только от компрометирования в вашем хранилище, но не обязательно от вмешательства вредоносного кода в вашем приложении.
Почему популярные хеширующие функции, такие как md5() и sha1() не подходят для паролей?
Такие хеширующие алгоритмы как MD5, SHA1 и SHA256 были спроектированы очень быстрыми и эффективными. При наличии современных технологий и оборудования, стало довольно просто выяснить результат этих алгоритмов методом «грубой силы» для определения оригинальных вводимых данных.
Из-за той скорости, с которой современные компьютеры могут «обратить» эти хеширующие алгоритмы, многие профессионалы компьютерной безопасности строго не рекомендуют использовать их для хеширования паролей.
Если популярные хеширующие функции не подходят, как же я тогда должен хешировать свои пароли?
При хешировании паролей существует два важных соображения: это стоимость вычисления и соль. Чем выше стоимость вычисления хеширующего алгоритма, тем больше времени требуется для взлома его вывода методом «грубой силы».
Другой возможностью является функция crypt() , которая поддерживает несколько алгоритмов хеширования. При использовании этой функции вы можете быть уверенным, что выбранный вами алгоритм доступен, так как PHP содержит собственную реализацию каждого поддерживаемого алгоритма, даже в случае, если какие-то из них не поддерживаются вашей системой.
При хешировании паролей рекомендуется применять алгоритм Blowfish, который также используется по умолчанию в API хеширования паролей, так как он значительно большей вычислительной сложности, чем MD5 или SHA1, при этом по-прежнему гибок.
Учтите, что, если вы используете функцию crypt() для проверки пароля, то вам нужно предостеречь себя от атак по времени, применяя сравнение строк, которое занимает постоянное время. Ни операторы PHP == и ===, ни функция strcmp() не являются таковыми. Функция же password_verify() как раз делает то, что нужно. Настоятельно рекомендуется использовать встроенное API хеширования паролей, если есть такая возможность.
Что такое соль?
Криптографическая соль представляет собой данные, которые применяются в процессе хеширования для предотвращения возможности разгадать оригинальный ввод с помощью поиска результата хеширования в списке заранее вычисленных пар ввод-хеш, известном также как «радужная» таблица.
Более простыми словами, соль — это кусочек дополнительных данных, которые делают ваши хеши намного более устойчивыми к взлому. Существует много онлайн-сервисов, предоставляющих обширные списки заранее вычисленных хешей вместе с их оригинальным вводом. Использование соли делает поиск результирующего хеша в таком списке маловероятным или даже невозможным.
password_hash() создаёт случайную соль в случае, если она не была передана, и чаще всего это наилучший и безопасный выбор.
Как я должен хранить свою соль?
При использовании функции password_hash() или crypt() , возвращаемое значение уже содержит соль как часть созданного хеша. Это значение нужно хранить как есть в вашей базе данных, так как оно содержит также информацию о хеширующей функции, которая использовалась, и может быть непосредственно передано в функции password_verify() или crypt() при проверке пароля.
Следующая диаграмма показывает формат возвращаемого значения функциями crypt() или password_hash() . Как можно видеть, они содержат полную информацию об алгоритме и соли, требуемых для будущей проверки пароля.
Шифрование передаваемых паролей
Настройка ключа шифрования драйвера и шифрования пароля для аутентификации IPP обеспечивает связь с использованием шифрованных паролей, а также способствует повышению уровня устойчивости паролей к подбору. Для дополнительного повышения уровня безопасности рекомендуется совместно использовать протоколы IPsec, SNMPv3 и SSL/TLS. Кроме того, шифруйте пароли входа для аутентификации администратора и пользователя.
Ключ шифрования драйвера
Этот ключ представляет собой символьную строку, используемую для шифрования паролей входа в систему или паролей документов, передаваемых от каждого драйвера при включенной аутентификации пользователей. Для шифрования пароля входа укажите ключ шифрования драйвера на аппарате и драйвере принтера, установленном на компьютере пользователя.
Пароль для функции «IPP аутентификация»
Чтобы зашифровать пароль аутентификации IPP в Web Image Monitor, задайте для параметра «Аутентификация» значение [DIGEST] , а затем укажите пароль аутентификации IPP, установленный на аппарате. С помощью telnet или FTP вы можете управлять паролями для аутентификации IPP, хотя это и не рекомендуется.
- Для получения подробной информации о шифровании регистрационных паролей, которые используются для аутентификации администратора, см. Регистрация и замена администраторов.
Как зашифровать пароли?
На сайте есть регистрация. После регистрации все данные о пользователе попадают в таблицу Users. Так же туда кладётся пароль в виде обычной строки, что неправильно. Как мне его зашифровать?
Отслеживать
задан 18 июл 2016 в 13:14
Andrew Tarasevich Andrew Tarasevich
41 1 1 золотой знак 1 1 серебряный знак 5 5 бронзовых знаков
Какой уровень защиты вы хотите? Что потенциально может украсть «хакер» ?
18 июл 2016 в 13:19
MD5 пароля передавайте, это самый простой вариант
18 июл 2016 в 13:19
@ДмитрийЧистик автор же пишет, что хранение пароля не устраивает. При чем тут «передавайте».
18 июл 2016 в 13:21
@ДмитрийЧистик и в базе хранить md5 всмысле? Так толку то, тогда злоумышленник перехватывает и передает md5, не зная пароля, толку от защиты ноль.
18 июл 2016 в 13:24
md5 — не для шифров. Лучше бы сказали, что может украсть хакер, от которого вы защищаетесь.
18 июл 2016 в 13:25
4 ответа 4
Сортировка: Сброс на вариант по умолчанию
Хэширование vs Шифрование.
Для начала надо понять, что хэширование — это не шифрование. Хэшрование пароля на клиенте никак не заменяет шифрование, и никак не позволяет защититься от перехвата траффика. При передаче и пароля/его хэша (если очень хочется написать велосипед на JS) никак не заменяет шифрование. Вам стоит использовать HTTPS. Все остальные способы, включая самописное ассиметричное шифрование — ненадежны.
Хэширование пароля
Хэширование защищает только от утечки данных из базы. Основная проблема при такой утечке — возможность восстановить пароли по хэшу и использовать их для password reuse attack — попытаться зайти с тем же email/username/password на популярные сервисы. Например, на Github. Т.е. вы вынудите пользователей не только сменить пароль на свой сервис, но и поменять пароль на все остальные сервисы (да, нехорошо использовать везде один и тот же пароль, но все так делают!)
Из этого прямо следуют требования к хэшированию:
- Злодей не должен иметь возможность быстро восстановить популярные пароли по хэшу. Использование популярного MD5/SHA1 позволяет быстро восстановить пароли. 5f4dcc3b5aa765d61d8327deb882cf99 расшифрует с первого раза любой онлайновый сервис.
- Злодей не должен иметь возможность восстановить перебором пароли для всех пользователей сразу. Один и тот же пароль, использованный разными пользователеми, должен дать разный хэш. Это увеличит время на расшифровку пропорционально количеству пользователей.
Есть надежный стандартный способ решить сразу обе проблемы — Key Derivation Function (Функция формирования ключа).
- При первоначальном хэшировании сгенерировать рандомное значение — соль, и посчитать хэш от «соль + пароль». Использованную соль хранить как часть результата.
- При проверке пароля — хэшировать «соль + проверяемый пароль», и сравнивать со значением, полученным в (1).
Такой подход дает разные значения даже в том случае, если один пользователь дважды использует один и тот же пароль.
В .NET есть готовые резализации KDF, например Rfc2898DeriveBytes. Используется примерно так:
public static string HashPassword(string password) < byte[] salt; byte[] buffer2; if (password == null) < throw new ArgumentNullException("password"); >using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, 0x10, 0x3e8)) < salt = bytes.Salt; buffer2 = bytes.GetBytes(0x20); >byte[] dst = new byte[0x31]; Buffer.BlockCopy(salt, 0, dst, 1, 0x10); Buffer.BlockCopy(buffer2, 0, dst, 0x11, 0x20); return Convert.ToBase64String(dst); >
public static bool VerifyHashedPassword(string hashedPassword, string password) < byte[] buffer4; if (hashedPassword == null) < return false; >if (password == null) < throw new ArgumentNullException("password"); >byte[] src = Convert.FromBase64String(hashedPassword); if ((src.Length != 0x31) || (src[0] != 0)) < return false; >byte[] dst = new byte[0x10]; Buffer.BlockCopy(src, 1, dst, 0, 0x10); byte[] buffer3 = new byte[0x20]; Buffer.BlockCopy(src, 0x11, buffer3, 0, 0x20); using (Rfc2898DeriveBytes bytes = new Rfc2898DeriveBytes(password, dst, 0x3e8)) < buffer4 = bytes.GetBytes(0x20); >return ByteArraysEqual(buffer3, buffer4); >
Отслеживать
ответ дан 18 июл 2016 в 16:50
user177221 user177221
Вот, пользоваться пользовался, но расписывать стало лень, да и названия не знал конкретных алгоритмов. Автору то может и лишнее, а в целом — наиболее безопасный вариант.
18 июл 2016 в 18:19
Ну в одном из комментариев я написал, что один из самых действенных вариантов SSL, но не везде это реально, чтобы «скрыть» сам пароль можно воспользоваться хешом, Кстати подскажите ссылку на сервис вскрытия хешов, очень стало интересно. А вот система открытого ключа, или формирование общего ключа может решить проблему, однако что первый что второй вариант ломается промежуточным слушателем. У нас на предприятии сертификат подменяется собственным, дабы «контролировать» весь трафик.
19 июл 2016 в 5:35
@ДмитрийЧистик нет, не ломается. У вас на предприятии подмена сертификата работает только потому, что на все рабочие машины установлен свой certificate authority, скорее всего через политики домена. Т.е. подмена работает только потому, что администратор принудительно настроил каждую машину доверять промежуточному слушателю. Это не man in the middle, а просто подтверждение факта, что админ имеет полный контроль над машиной. В интернете такое не проходит, по крайней мере пока. Из сервисов — например, md5online.org.
– user177221
19 июл 2016 в 6:38
@PashaPash первый же хеш No result found in our database, как и подозревал поиск уже в созданных базах хеша, декрипта как такового не существует. Максимум что Вы получите дак это аналог пароля но не его самого.
19 июл 2016 в 7:01
@ДмитрийЧистик ну да, декрипта нет — это же хэш. Поиск по базам позволяет восстановить популярные пароли. Подбор через радужные таблицы позволяет восстановить остальные пароли (буквы+цифры). Радужные таблицы дают перебор 10^13 паролей в секунду — т.е. любой c00lpa55 подбирается мгновенно. Да, с некоторой вероятностью перебор выдаст не пароль, а коллизию — но для коротких (
– user177221
19 июл 2016 в 8:01
На шарпе хеш можно посчитать как-нибудь так:
public static string GetHash(string password) < using (var hash = SHA1.Create()) < return string.Concat(hash.ComputeHash(Encoding.UTF8.GetBytes(password)).Select(x =>x.ToString("X2"))); > >
Вместо SHA1 подойдет любой алгоритм, в том числе и MD5.
Вы должны понимать, что для злоумышленника это не задержка вообще, особенно если вы разрешаете доступ по самому хешу — тогда никакой разницы, утек пароль или его хеш.
Отслеживать
ответ дан 18 июл 2016 в 13:45
4,448 2 2 золотых знака 18 18 серебряных знаков 41 41 бронзовый знак
Пожалуй самый защищенный метод это либо Криптосистема с открытым ключом, либо Электронная подпись. Но и они в отдельных случаях не спасают. Хотя подпись пожалуй самая надёжная, только дорогая.
18 июл 2016 в 13:52
@Monk можете пояснить, что значит никакой разницы, утек пароль или его хэш?. При вводе в строку пароля хэша, от него же тоже посчитается хэш. И как пароль не подойдет. А вообще, хэш-функции нужны же для того, чтобы при потере БД у злоумышленника не было доступа ко всем учетным записям.
18 июл 2016 в 13:57
Тут автор прав, по сути пост запросом Вы отправляете MD5, что мешает злоумышленнику подсунуть уже созданный MD5 вместо пароля, который по сути переводится в MD5 JS. Главное получить пару логинизации на сервере, ну это уже определяется криптозащитой необходимой лично Вам.
18 июл 2016 в 14:01
@Trymount это только пока хакер ограничен формой ввода. Но вот если хакер имеет доступ к базе, то может он и ещё к чему имеет доступ? Фактически, при доступе к базе, я уже расчитываю на худший сценарий, когда и к серверу доступ тоже есть. А значит ввести напрямую хеш, а не пароль куда надо он точно сможет.
18 июл 2016 в 14:03
@ДмитрийЧистик асимметричное шифрование действительно выглядит лучше всего, в плане дешевых и стабильных реализаций.
18 июл 2016 в 14:04
Сделал это с помощью этого метода:
public string CalculateMD5Hash(string input) < // step 1, calculate MD5 hash from input MD5 md5 = System.Security.Cryptography.MD5.Create(); byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input); byte[] hash = md5.ComputeHash(inputBytes); // step 2, convert byte array to hex string StringBuilder sb = new StringBuilder(); for (int i = 0; i < hash.Length; i++) < sb.Append(hash[i].ToString("X2")); >return sb.ToString(); >
Отслеживать
ответ дан 20 июл 2016 в 9:49
Andrew Tarasevich Andrew Tarasevich
41 1 1 золотой знак 1 1 серебряный знак 5 5 бронзовых знаков
Это вот прямо очень плохое решение. MD5 для нынешнего времени очень слаб для хеширования паролей (и по меркам 2016 года тоже)
27 мая 2019 в 11:10
var MD5 = function (string) < function RotateLeft(lValue, iShiftBits) < return (lValue<>>(32-iShiftBits)); > function AddUnsigned(lX,lY) < var lX4,lY4,lX8,lY8,lResult; lX8 = (lX & 0x80000000); lY8 = (lY & 0x80000000); lX4 = (lX & 0x40000000); lY4 = (lY & 0x40000000); lResult = (lX & 0x3FFFFFFF)+(lY & 0x3FFFFFFF); if (lX4 & lY4) < return (lResult ^ 0x80000000 ^ lX8 ^ lY8); >if (lX4 | lY4) < if (lResult & 0x40000000) < return (lResult ^ 0xC0000000 ^ lX8 ^ lY8); >else < return (lResult ^ 0x40000000 ^ lX8 ^ lY8); >> else < return (lResult ^ lX8 ^ lY8); >> function F(x,y,z) < return (x & y) | ((~x) & z); >function G(x,y,z) < return (x & z) | (y & (~z)); >function H(x,y,z) < return (x ^ y ^ z); >function I(x,y,z) < return (y ^ (x | (~z))); >function FF(a,b,c,d,x,s,ac) < a = AddUnsigned(a, AddUnsigned(AddUnsigned(F(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); >; function GG(a,b,c,d,x,s,ac) < a = AddUnsigned(a, AddUnsigned(AddUnsigned(G(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); >; function HH(a,b,c,d,x,s,ac) < a = AddUnsigned(a, AddUnsigned(AddUnsigned(H(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); >; function II(a,b,c,d,x,s,ac) < a = AddUnsigned(a, AddUnsigned(AddUnsigned(I(b, c, d), x), ac)); return AddUnsigned(RotateLeft(a, s), b); >; function ConvertToWordArray(string) < var lWordCount; var lMessageLength = string.length; var lNumberOfWords_temp1=lMessageLength + 8; var lNumberOfWords_temp2=(lNumberOfWords_temp1-(lNumberOfWords_temp1 % 64))/64; var lNumberOfWords = (lNumberOfWords_temp2+1)*16; var lWordArray=Array(lNumberOfWords-1); var lBytePosition = 0; var lByteCount = 0; while ( lByteCount < lMessageLength ) < lWordCount = (lByteCount-(lByteCount % 4))/4; lBytePosition = (lByteCount % 4)*8; lWordArray[lWordCount] = (lWordArray[lWordCount] | (string.charCodeAt(lByteCount)<lWordCount = (lByteCount-(lByteCount % 4))/4; lBytePosition = (lByteCount % 4)*8; lWordArray[lWordCount] = lWordArray[lWordCount] | (0x80<>>29; return lWordArray; >; function WordToHex(lValue) < var WordToHexValue="",WordToHexValue_temp="",lByte,lCount; for (lCount = 0;lCount<=3;lCount++) < lByte = (lValue>>>(lCount*8)) & 255; WordToHexValue_temp = "0" + lByte.toString(16); WordToHexValue = WordToHexValue + WordToHexValue_temp.substr(WordToHexValue_temp.length-2,2); > return WordToHexValue; >; function Utf8Encode(string) < string = string.replace(/\r\n/g,"\n"); var utftext = ""; for (var n = 0; n < string.length; n++) < var c = string.charCodeAt(n); if (c < 128) < utftext += String.fromCharCode(c); >else if((c > 127) && (c < 2048)) < utftext += String.fromCharCode((c >> 6) | 192); utftext += String.fromCharCode((c & 63) | 128); > else < utftext += String.fromCharCode((c >> 12) | 224); utftext += String.fromCharCode(((c >> 6) & 63) | 128); utftext += String.fromCharCode((c & 63) | 128); > > return utftext; >; var x=Array(); var k,AA,BB,CC,DD,a,b,c,d; var S11=7, S12=12, S13=17, S14=22; var S21=5, S22=9 , S23=14, S24=20; var S31=4, S32=11, S33=16, S34=23; var S41=6, S42=10, S43=15, S44=21; string = Utf8Encode(string); x = ConvertToWordArray(string); a = 0x67452301; b = 0xEFCDAB89; c = 0x98BADCFE; d = 0x10325476; for (k=0;k var temp = WordToHex(a)+WordToHex(b)+WordToHex(c)+WordToHex(d); return temp.toLowerCase(); >
MD5("whatever");
Можно воспользоваться протоколом Диффи-Хеллмана. Но тут придётся налаживать общий ключ между клиентом и сервером. В принципе нет особо защищенного метода, разве что ssh, Но и он может подделываться промежуточным прокси.
Как зашифровать пароль вручную
По историческим причинам, Symfony использует термин «шифрование паролей», в то время, как на самом деле стоит использовать «хеширование паролей». «Кодировщики» на самом деле являются криптографическими хеш-функциями.
Если, к примеру, вы храните пользователей в DB, вам понадобится шифровать пароли пользователей до того, как вставлять их. Независимо от того, какой алгоритм вы сконфигурируете для вашего объекта пользователя, хешированный пароль всегда может быть определён из контроллера следующим образом:
1 2 3 4 5 6 7 8 9 10 11
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; public function registerAction(UserPasswordEncoderInterface $encoder) < // любой *ваш* объект пользователя $user = new AppBundle\Entity\User(); $plainPassword = 'ryanpass'; $encoded = $encoder->encodePassword($user, $plainPassword); $user->setPassword($encoded); >
Для того, чтобы это работало, просто убедитесь в том, что у вас есть кодировщик для вашего класса пользователя (например, AppBundle\Entity\User ), сконфигурированный под ключом encoders в app/config/security.yml .
Объект $encoder также имеет метод isPasswordValid() , который берёт объект User в качестве первого аргумента и нешифрованный пароль для проверки в качестве второго аргумента.
Когда вы позволяете пользователю отправлять нешифрованный пароль (например, в регистрационной форме или форме смены пароля), вы должны иметь валидацию, которая гарантирует, что пароль состоит из 4096 символов или меньше. Прочтите больше деталей об этом: Как реализовать простую форму регистрации .
Symfony is a trademark of Symfony SAS. Переклад — Playtini. UA RU RU EN