Хеширование Java с использованием MD5, SHA, PBKDF2, Bcrypt и Scrypt

Изучите алгоритмы хеширования Java подробно для хеширования паролей. Безопасный хеш пароля — это зашифрованная последовательность символов, полученная после применения определенных алгоритмов и манипуляций с предоставленными пользователем паролями, которые, как правило, очень слабы и их легко угадать.

Многие подобные алгоритмы хеширования в Java могут оказаться эффективными для обеспечения безопасности паролей.

Важный

Пожалуйста, помните, что после генерации хеша пароля мы не сможем преобразовать хеш обратно в исходный пароль.

Каждый раз, когда пользователь входит в приложение, нам приходится заново генерировать хеш пароля и сопоставлять его с хешем, хранящимся в базе данных.

Итак, если пользователь забудет свой пароль, нам придется отправить ему временный пароль; или попросить его сбросить пароль. Это сейчас обычное дело, не так ли?

1. Простейший хеш пароля с алгоритмом MD5

Алгоритм MD5 Message-Digest — это широко используемая криптографическая хэш-функция, которая выдает 128-битное(16-байтовое) хэш-значение. Он очень прост и понятен; основная идея заключается в отображении наборов данных переменной длины в наборы данных фиксированного размера.

Для этого входное сообщение разбивается на куски по 512-битных блоков. В конец добавляется заполнение, чтобы его длину можно было разделить на 512.

Эти блоки обрабатываются алгоритмом MD5, который работает в 128-битном состоянии, и результатом будет 128-битное значение хэша. После применения MD5 сгенерированный хэш обычно представляет собой 32-значное шестнадцатеричное число.

Здесь пароль, который необходимо закодировать, часто называют «сообщением», а сгенерированное значение хэша называют дайджестом сообщения или просто «дайджестом».

1.1 Пример хеширования Java MD5

import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class SimpleMD5Example{public static void main(String[] args){String passwordToHash = "password";String generatedPassword = null;try{// Create MessageDigest instance for MD5MessageDigest md = MessageDigest.getInstance("MD5");// Add password bytes to digestmd.update(passwordToHash.getBytes());// Get the hash's bytesbyte[] bytes = md.digest();// This bytes[] has bytes in decimal format. Convert it to hexadecimal formatStringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++) {sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}// Get complete hashed password in hex formatgeneratedPassword = sb.toString();} catch(NoSuchAlgorithmException e) {e.printStackTrace();}System.out.println(generatedPassword);}}
 5f4dcc3b5aa765d61d8327deb882cf99

1.2 Недостатки

  • Хотя MD5 является широко распространенным алгоритмом хеширования, он далек от безопасности, MD5 генерирует довольно слабые хэши. Его основные преимущества в том, что он быстрый и простой в реализации. Но это также означает, что он подвержен атакам методом перебора и атакам по словарю.
  • Радужные таблицы со словами и хэшами позволяют очень быстро находить известный хэш и угадывать исходный пароль.
  • MD5 не устойчив к коллизиям, что означает, что разные пароли в конечном итоге могут привести к одному и тому же хешу.

Если вы используете хеш MD5 в своем приложении, рассмотрите возможность добавления соли для обеспечения безопасности.

2. Повышение безопасности MD5 с помощью соли

Помните, что добавление соли не является специфичным для MD5. Мы можем добавлять соль и к любому другому алгоритму. Поэтому, пожалуйста, сосредоточьтесь на том, как она применяется, а не на ее связи с MD5.

Википедия определяет соль как случайные данные, которые используются в качестве дополнительных входных данных для односторонней функции, которая хеширует пароль или парольную фразу.

Проще говоря, соль — это некий случайно сгенерированный текст, который добавляется к паролю перед получением хеша.

Первоначальной целью соления была, прежде всего, защита от предварительно вычисленных атак с использованием радужных таблиц, которые в противном случае могли бы использоваться для значительного повышения эффективности взлома хэшированной базы данных паролей.

Более существенным преимуществом является замедление параллельных операций, которые сравнивают хэш предполагаемого пароля с несколькими хэшами паролей одновременно.

Важный

Нам всегда нужно использовать SecureRandom для создания хороших солей. Класс Java SecureRandom поддерживает алгоритм генератора псевдослучайных чисел «SHA1PRNG», и мы можем воспользоваться этим.

2.1 Как получить соль

Давайте посмотрим, как нам следует производить соль.

private static String getSalt()throws NoSuchAlgorithmException, NoSuchProviderException{// Always use a SecureRandom generatorSecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");// Create array for saltbyte[] salt = new byte[16];// Get a random saltsr.nextBytes(salt);// return saltreturn salt.toString();}

Алгоритм SHA1PRNG используется как криптографически стойкий генератор псевдослучайных чисел на основе алгоритма дайджеста сообщений SHA-1.

Обратите внимание, что если начальное число не указано, оно будет сгенерировано с помощью генератора случайных чисел(TRNG).

2.2. Генерация MD5 с солью

Теперь давайте рассмотрим пример модифицированного хеширования MD5:

import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.security.NoSuchProviderException;import java.security.SecureRandom;public class SaltedMD5Example{public static void main(String[] args)throws NoSuchAlgorithmException, NoSuchProviderException{String passwordToHash = "password";String salt = getSalt();String securePassword = getSecurePassword(passwordToHash, salt);System.out.println(securePassword);String regeneratedPassowrdToVerify =getSecurePassword(passwordToHash, salt);System.out.println(regeneratedPassowrdToVerify);}private static String getSecurePassword(String passwordToHash,String salt) {String generatedPassword = null;try {// Create MessageDigest instance for MD5MessageDigest md = MessageDigest.getInstance("MD5");// Add password bytes to digestmd.update(salt.getBytes());// Get the hash's bytesbyte[] bytes = md.digest(passwordToHash.getBytes());// This bytes[] has bytes in decimal format;// Convert it to hexadecimal formatStringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++) {sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}// Get complete hashed password in hex formatgeneratedPassword = sb.toString();} catch(NoSuchAlgorithmException e) {e.printStackTrace();}return generatedPassword;}// Add saltprivate static String getSalt()throws NoSuchAlgorithmException, NoSuchProviderException{// Always use a SecureRandom generatorSecureRandom sr = SecureRandom.getInstance("SHA1PRNG", "SUN");// Create array for saltbyte[] salt = new byte[16];// Get a random saltsr.nextBytes(salt);// return saltreturn salt.toString();}}

Обратите внимание, что теперь вы должны хранить это значение соли для каждого хэшируемого вами пароля. Поскольку при повторном входе пользователя в систему мы должны использовать только изначально сгенерированную соль для повторного создания хеша, соответствующего сохраненному хешу. Если используется другая соль(мы генерируем случайную соль), то сгенерированный хеш будет другим.

Также вы могли слышать термины crazy hashing и salting. Это обычно относится к созданию пользовательских комбинаций.

alt + пароль + соль => Сгенерированный хэш

Не практикуйте эти безумные вещи. Они в любом случае не помогают сделать хэши более безопасными. Если вы хотите большей безопасности, выберите лучший алгоритм.

3. Лучшая безопасность паролей с использованием алгоритмов SHA

SHA(Secure Hash Algorithm) — это семейство криптографических хеш-функций. Он очень похож на MD5, за исключением того, что генерирует более сильные хеши.

Однако хеши SHA не всегда уникальны, и это означает, что у нас могут быть одинаковые хеши для двух разных входов. Когда это происходит, это называется «коллизией». Вероятность коллизии в SHA меньше, чем в MD5. Но не беспокойтесь об этих коллизиях, поскольку они очень редки.

В Java есть четыре реализации алгоритма SHA. Они генерируют следующие хэши длины по сравнению с MD5(128-битный хэш):

  • SHA-1(самый простой – 160-битный хэш)
  • SHA-256(Надежнее, чем SHA-1 – 256-битный хэш)
  • SHA-384(сильнее, чем SHA-256 – хэш 384 бит)
  • SHA-512(сильнее, чем SHA-384 – хэш 512 бит)

Длинный хеш сложнее взломать. В этом и заключается основная идея.

Чтобы получить любую реализацию алгоритма, передайте ее в качестве параметра в MessageDigest. Например:

MessageDigest md = MessageDigest.getInstance("SHA-512");//ORMessageDigest md = MessageDigest.getInstance("SHA-256");

3.1 Пример хеширования Java SHA

Давайте создадим тестовую программу для демонстрации генерации хеша SHA:

import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.security.SecureRandom;public class SHAExample {public static void main(String[] args) throws NoSuchAlgorithmException {String passwordToHash = "password";String salt = getSalt();String securePassword = get_SHA_1_SecurePassword(passwordToHash, salt);System.out.println(securePassword);securePassword = get_SHA_256_SecurePassword(passwordToHash, salt);System.out.println(securePassword);securePassword = get_SHA_384_SecurePassword(passwordToHash, salt);System.out.println(securePassword);securePassword = get_SHA_512_SecurePassword(passwordToHash, salt);System.out.println(securePassword);}private static String get_SHA_1_SecurePassword(String passwordToHash,String salt) {String generatedPassword = null;try {MessageDigest md = MessageDigest.getInstance("SHA-1");md.update(salt.getBytes());byte[] bytes = md.digest(passwordToHash.getBytes());StringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++) {sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}generatedPassword = sb.toString();} catch(NoSuchAlgorithmException e) {e.printStackTrace();}return generatedPassword;}private static String get_SHA_256_SecurePassword(String passwordToHash,String salt) {String generatedPassword = null;try {MessageDigest md = MessageDigest.getInstance("SHA-256");md.update(salt.getBytes());byte[] bytes = md.digest(passwordToHash.getBytes());StringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++) {sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}generatedPassword = sb.toString();} catch(NoSuchAlgorithmException e) {e.printStackTrace();}return generatedPassword;}private static String get_SHA_384_SecurePassword(String passwordToHash,String salt) {String generatedPassword = null;try {MessageDigest md = MessageDigest.getInstance("SHA-384");md.update(salt.getBytes());byte[] bytes = md.digest(passwordToHash.getBytes());StringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++) {sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}generatedPassword = sb.toString();} catch(NoSuchAlgorithmException e) {e.printStackTrace();}return generatedPassword;}private static String get_SHA_512_SecurePassword(String passwordToHash,String salt) {String generatedPassword = null;try {MessageDigest md = MessageDigest.getInstance("SHA-512");md.update(salt.getBytes());byte[] bytes = md.digest(passwordToHash.getBytes());StringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++) {sb.append(Integer.toString((bytes[i] & 0xff) + 0x100, 16).substring(1));}generatedPassword = sb.toString();} catch(NoSuchAlgorithmException e) {e.printStackTrace();}return generatedPassword;}// Add saltprivate static String getSalt() throws NoSuchAlgorithmException {SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");byte[] salt = new byte[16];sr.nextBytes(salt);return salt.toString();}}
 e4c53afeaa7a08b1f27022abd443688c37981bc487adfd14a7a89b201bf6d99105b417287db6581d8aee989076bb7f86154e8f32bc5914fe3896ae8a2c43a4513f2a0d716974cc305733847e3d49e1ea52d1ca50e2a9d0ac192acd43facfb422bb5ace88529211542985b8f7af61994670d03d25d55cc9cd1cff8d57bb799c4b586891e1 12b197530c76744bcd7ef135b58d47d65a0bec221eb5d77793956cf2709dd012

Очень быстро можно сказать, что SHA-512 генерирует самый надежный хэш.

4. Более сильные хэши с использованием алгоритма PBKDF2WithHmacSHA1

До сих пор мы узнали о создании безопасных хэшей для паролей и использовании соли, чтобы сделать их еще более безопасными. Но сегодня проблема в том, что аппаратные средства стали настолько быстрыми, что любая атака методом подбора с использованием словаря и радужных таблиц становится настолько быстрой, что злоумышленник может взломать любой пароль за меньшее или большее время.

Чтобы решить эту проблему, общая идея заключается в том, чтобы сделать атаки грубой силы медленнее, чтобы минимизировать ущерб. Наш следующий алгоритм работает именно на этой концепции.

Цель состоит в том, чтобы сделать хеш-функцию достаточно медленной, чтобы препятствовать атакам, но все же достаточно быстрой, чтобы не вызывать заметной задержки для пользователя.

Эта функция по сути реализована с использованием некоторых ресурсоемких алгоритмов, таких как PBKDF2, Bcrypt или Scrypt. Эти алгоритмы принимают в качестве аргумента фактор работы(также известный как фактор безопасности) или количество итераций.

Количество итераций определяет, насколько медленной будет хэш-функция. Когда в следующем году компьютеры станут быстрее, мы сможем увеличить фактор работы, чтобы сбалансировать его.

Java реализовала алгоритм « PBKDF2 » как « PBKDF2WithHmacSHA1 ».

4.1 Пример хэша Java PBKDF2WithHmacSHA1

Давайте рассмотрим пример использования алгоритма PBKDF2WithHmacSHA1.

public static void main(String[] args)throws NoSuchAlgorithmException, InvalidKeySpecException{String originalPassword = "password";String generatedSecuredPasswordHash= generateStorngPasswordHash(originalPassword);System.out.println(generatedSecuredPasswordHash);}private static String generateStorngPasswordHash(String password)throws NoSuchAlgorithmException, InvalidKeySpecException{int iterations = 1000;char[] chars = password.toCharArray();byte[] salt = getSalt();PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, 64 * 8);SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");byte[] hash = skf.generateSecret(spec).getEncoded();return iterations + ":" + toHex(salt) + ":" + toHex(hash);}private static byte[] getSalt() throws NoSuchAlgorithmException{SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");byte[] salt = new byte[16];sr.nextBytes(salt);return salt;}private static String toHex(byte[] array) throws NoSuchAlgorithmException{BigInteger bi = new BigInteger(1, array);String hex = bi.toString(16);int paddingLength =(array.length * 2) - hex.length();if(paddingLength > 0){return String.format("%0" +paddingLength + "d", 0) + hex;}else{return hex;}}
 1000:5b4240333032306164:f38d165fce8ce42f59d366139ef5d9e1ca1247f0e06e503ee1a6 11dd9ec40876bb5edb8409f5abe5504aab6628e70cfb3d3a18e99d70357d295002c3d0a308a0

4.2 Проверка паролей

Следующим шагом будет создание функции, которую мы сможем использовать для повторной проверки пароля, когда пользователь вернется и войдет в систему.

public static void main(String[] args)throws NoSuchAlgorithmException, InvalidKeySpecException{String originalPassword = "password";String generatedSecuredPasswordHash= generateStorngPasswordHash(originalPassword);System.out.println(generatedSecuredPasswordHash);boolean matched = validatePassword("password", generatedSecuredPasswordHash);System.out.println(matched);matched = validatePassword("password1", generatedSecuredPasswordHash);System.out.println(matched);}private static boolean validatePassword(String originalPassword, String storedPassword)throws NoSuchAlgorithmException, InvalidKeySpecException{String[] parts = storedPassword.split(":");int iterations = Integer.parseInt(parts[0]);byte[] salt = fromHex(parts[1]);byte[] hash = fromHex(parts[2]);PBEKeySpec spec = new PBEKeySpec(originalPassword.toCharArray(),salt, iterations, hash.length * 8);SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");byte[] testHash = skf.generateSecret(spec).getEncoded();int diff = hash.length ^ testHash.length;for(int i = 0; i < hash.length && i < testHash.length; i++){diff |= hash[i] ^ testHash[i];}return diff == 0;}private static byte[] fromHex(String hex) throws NoSuchAlgorithmException{byte[] bytes = new byte[hex.length() / 2];for(int i = 0; i < bytes.length ;i++){bytes[i] =(byte)Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);}return bytes;}

Пожалуйста, обратитесь к функциям из приведенных выше примеров кода. Если возникнут какие-либо трудности, загрузите исходный код, прикрепленный в конце руководства.

5. Хэши с использованием Bcrypt и Scrypt

Концепции, лежащие в основе bcrypt, похожи на предыдущую концепцию PBKDF2. Просто так получилось, что Java не имеет встроенной поддержки алгоритма bcrypt, чтобы замедлить атаку, но все же вы можете найти одну такую реализацию в приложенном исходном коде.

5.1 Создание хеша с использованием Bcrypt с солью

Давайте рассмотрим пример кода использования(BCrypt.java доступен в исходном коде).

public class BcryptHashingExample{public static void main(String[] args) throws NoSuchAlgorithmException{String originalPassword = "password";String generatedSecuredPasswordHash = BCrypt.hashpw(originalPassword, BCrypt.gensalt(12));System.out.println(generatedSecuredPasswordHash);boolean matched = BCrypt.checkpw(originalPassword, generatedSecuredPasswordHash);System.out.println(matched);}}
 $2a$12$WXItscQ/FDbLKU4mO58jxu3Tx/mueaS8En3M6QOVZIZLaGdWrS.pKистинный

5.2 Создание хеша с помощью Scrypt с солью

Как и в случае с bcrypt, я скачал scrypt с github и добавил исходный код алгоритма scrypt в исходный код.

Давайте посмотрим, как использовать реализацию:

public class ScryptPasswordHashingDemo{public static void main(String[] args) {String originalPassword = "password";String generatedSecuredPasswordHash = SCryptUtil.scrypt(originalPassword, 16, 16, 16);System.out.println(generatedSecuredPasswordHash);boolean matched = SCryptUtil.check("password", generatedSecuredPasswordHash);System.out.println(matched);matched = SCryptUtil.check("passwordno", generatedSecuredPasswordHash);System.out.println(matched);}}
 $s0$41010$Gxbn9LQ4I+fZ/kt0glnZgQ==$X+dRy9oLJz1JaNm1xscUl7EmUFHIILT1ktYB5DQ3fZs=истинныйЛОЖЬ

6. Заключение

  1. Хранение текстового пароля с хешированием — самое опасное на сегодняшний день для безопасности приложений.
  2. MD5 обеспечивает базовое хеширование для генерации безопасного хэша пароля. Добавление соли делает его еще более сильным.
  3. MD5 генерирует 128-битный хэш. Используйте алгоритм SHA, чтобы сделать его более безопасным, который создает хэши длиной от 160 до 512 бит. 512 бит — самый сильный.
  4. Даже защищенные пароли, захешированные SHA, можно взломать с помощью современных быстрых аппаратных средств. Чтобы преодолеть это, вам понадобятся алгоритмы, которые замедлят атаки методом подбора и минимизируют их воздействие. К таким алгоритмам относятся PBKDF2, BCrypt и SCrypt.
  5. Пожалуйста, хорошо подумайте, прежде чем применять соответствующий алгоритм безопасности.

Чтобы загрузить исходный код приведенных выше примеров алгоритмов, перейдите по ссылке ниже.

Загрузить исходный код

Прокрутить вверх