Как я могу хэшировать пароль в Java?

176

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

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

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

Крис Датроу
источник
11
@ YGL это на самом деле не рекомбинация, поскольку атаки на GPU настолько дешевы, что семейство SHA - очень плохой выбор для хеширования пароля (слишком быстрого) даже с солью. Используйте bcrypt, scrypt или PBKDF2
Eran Medan
11
Почему этот вопрос был закрыт? Это вопрос к реальной инженерной проблеме, и ответы на нее неоценимы. ОП не просит библиотеку, он спрашивает, как решить инженерную проблему.
stackoverflowuser2010
12
Просто великолепно. Этот вопрос имеет 52 голосов, и кто-то решает закрыть его как "не по теме".
stackoverflowuser2010
1
Да, я уже писал в Meta об этой проблеме закрытия, но меня довольно сильно избили.
Крис Датроу
8
Этот вопрос должен быть вновь открыт. Это вопрос о том, как написать программу для решения описанной проблемы (аутентификация по паролю) с помощью краткого кода. Наблюдение за триггерным словом «библиотека» не оправдывает рефлексивное закрытие вопроса; он не просит рекомендации библиотеки, он спрашивает, как хэшировать пароли. Редактировать: Там, это исправлено.
Эриксон

Ответы:

157

Вы можете использовать средство, встроенное в среду выполнения Java, для этого. SunJCE в Java 6 поддерживает PBKDF2, который является хорошим алгоритмом для хеширования паролей.

byte[] salt = new byte[16];
random.nextBytes(salt);
KeySpec spec = new PBEKeySpec("password".toCharArray(), salt, 65536, 128);
SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
byte[] hash = f.generateSecret(spec).getEncoded();
Base64.Encoder enc = Base64.getEncoder();
System.out.printf("salt: %s%n", enc.encodeToString(salt));
System.out.printf("hash: %s%n", enc.encodeToString(hash));

Вот служебный класс, который вы можете использовать для аутентификации по паролю PBKDF2:

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

/**
 * Hash passwords for storage, and test passwords against password tokens.
 * 
 * Instances of this class can be used concurrently by multiple threads.
 *  
 * @author erickson
 * @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a>
 */
public final class PasswordAuthentication
{

  /**
   * Each token produced by this class uses this identifier as a prefix.
   */
  public static final String ID = "$31$";

  /**
   * The minimum recommended cost, used by default
   */
  public static final int DEFAULT_COST = 16;

  private static final String ALGORITHM = "PBKDF2WithHmacSHA1";

  private static final int SIZE = 128;

  private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");

  private final SecureRandom random;

  private final int cost;

  public PasswordAuthentication()
  {
    this(DEFAULT_COST);
  }

  /**
   * Create a password manager with a specified cost
   * 
   * @param cost the exponential computational cost of hashing a password, 0 to 30
   */
  public PasswordAuthentication(int cost)
  {
    iterations(cost); /* Validate cost */
    this.cost = cost;
    this.random = new SecureRandom();
  }

  private static int iterations(int cost)
  {
    if ((cost < 0) || (cost > 30))
      throw new IllegalArgumentException("cost: " + cost);
    return 1 << cost;
  }

  /**
   * Hash a password for storage.
   * 
   * @return a secure authentication token to be stored for later authentication 
   */
  public String hash(char[] password)
  {
    byte[] salt = new byte[SIZE / 8];
    random.nextBytes(salt);
    byte[] dk = pbkdf2(password, salt, 1 << cost);
    byte[] hash = new byte[salt.length + dk.length];
    System.arraycopy(salt, 0, hash, 0, salt.length);
    System.arraycopy(dk, 0, hash, salt.length, dk.length);
    Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
    return ID + cost + '$' + enc.encodeToString(hash);
  }

  /**
   * Authenticate with a password and a stored password token.
   * 
   * @return true if the password and token match
   */
  public boolean authenticate(char[] password, String token)
  {
    Matcher m = layout.matcher(token);
    if (!m.matches())
      throw new IllegalArgumentException("Invalid token format");
    int iterations = iterations(Integer.parseInt(m.group(1)));
    byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
    byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
    byte[] check = pbkdf2(password, salt, iterations);
    int zero = 0;
    for (int idx = 0; idx < check.length; ++idx)
      zero |= hash[salt.length + idx] ^ check[idx];
    return zero == 0;
  }

  private static byte[] pbkdf2(char[] password, byte[] salt, int iterations)
  {
    KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
    try {
      SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
      return f.generateSecret(spec).getEncoded();
    }
    catch (NoSuchAlgorithmException ex) {
      throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
    }
    catch (InvalidKeySpecException ex) {
      throw new IllegalStateException("Invalid SecretKeyFactory", ex);
    }
  }

  /**
   * Hash a password in an immutable {@code String}. 
   * 
   * <p>Passwords should be stored in a {@code char[]} so that it can be filled 
   * with zeros after use instead of lingering on the heap and elsewhere.
   * 
   * @deprecated Use {@link #hash(char[])} instead
   */
  @Deprecated
  public String hash(String password)
  {
    return hash(password.toCharArray());
  }

  /**
   * Authenticate with a password in an immutable {@code String} and a stored 
   * password token. 
   * 
   * @deprecated Use {@link #authenticate(char[],String)} instead.
   * @see #hash(String)
   */
  @Deprecated
  public boolean authenticate(String password, String token)
  {
    return authenticate(password.toCharArray(), token);
  }

}
Эриксон
источник
11
Вы можете быть осторожны при преобразовании байтов в шестнадцатеричные символы BigInteger: ведущие нули удаляются. Это нормально для быстрой отладки, но я видел ошибки в рабочем коде из-за этого эффекта.
Томас Порнин
24
@ thomas-pornin подчеркивает, почему нам нужна библиотека , а не блок кода, который почти у цели . Страшно, что принятый ответ не отвечает на вопрос по столь важной теме.
Нильзор
9
Используйте алгоритм PBKDF2WithHmacSHA512, начиная с Java 8. Он немного сильнее.
iwan.z
1
Обратите внимание, что существующие ALGS не удаляется в более поздних версиях: java_4: PBEWithMD5AndDES, DESede, DES java_5 / 6/7: PBKDF2WithHmacSHA1, РОВ (только в Java 5), PBEWithSHA1AndRC2_40, PBEWithSHA1And, PBEWithMD5AndTriple java_8: PBEWithHmacSHA224AndAES_128, PBEWithHmacSHA384AndAES_128, PBEWithHmacSHA512AndAES_128, RC4_40, PBKDF2WithHmacSHA256 , PBEWithHmacSHA1AndAES_128, RC4_128, PBKDF2WithHmacSHA224, PBEWithHmacSHA256AndAES_256, RC2_128, PBEWithHmacSHA224AndAES_256, PBEWithHmacSHA384AndAES_256, PBEWithHmacSHA512AndAES_256, PBKDF2WithHmacSHA512, PBEWithHmacSHA256AndAES_128, PBKDF2WithHmacSHA384, PBEWithHmacSHA1AndAES_256
iwan.z
4
@TheTosters Да, время выполнения будет больше для неправильных паролей; более конкретно, неправильные пароли будут занимать то же время, что и правильные пароли. Это предотвращает временные атаки, хотя, признаюсь, я не могу придумать практического способа использования такой уязвимости в этом случае. Но вы не режете углы. То, что я не вижу этого, не означает, что более коварный ум не увидит.
Эриксон
97

Вот полная реализация с двумя методами, делающими именно то, что вы хотите:

String getSaltedHash(String password)
boolean checkPassword(String password, String stored)

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

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base64;

public class Password {
    // The higher the number of iterations the more 
    // expensive computing the hash is for us and
    // also for an attacker.
    private static final int iterations = 20*1000;
    private static final int saltLen = 32;
    private static final int desiredKeyLen = 256;

    /** Computes a salted PBKDF2 hash of given plaintext password
        suitable for storing in a database. 
        Empty passwords are not supported. */
    public static String getSaltedHash(String password) throws Exception {
        byte[] salt = SecureRandom.getInstance("SHA1PRNG").generateSeed(saltLen);
        // store the salt with the password
        return Base64.encodeBase64String(salt) + "$" + hash(password, salt);
    }

    /** Checks whether given plaintext password corresponds 
        to a stored salted hash of the password. */
    public static boolean check(String password, String stored) throws Exception{
        String[] saltAndHash = stored.split("\\$");
        if (saltAndHash.length != 2) {
            throw new IllegalStateException(
                "The stored password must have the form 'salt$hash'");
        }
        String hashOfInput = hash(password, Base64.decodeBase64(saltAndHash[0]));
        return hashOfInput.equals(saltAndHash[1]);
    }

    // using PBKDF2 from Sun, an alternative is https://github.com/wg/scrypt
    // cf. http://www.unlimitednovelty.com/2012/03/dont-use-bcrypt.html
    private static String hash(String password, byte[] salt) throws Exception {
        if (password == null || password.length() == 0)
            throw new IllegalArgumentException("Empty passwords are not supported.");
        SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        SecretKey key = f.generateSecret(new PBEKeySpec(
            password.toCharArray(), salt, iterations, desiredKeyLen));
        return Base64.encodeBase64String(key.getEncoded());
    }
}

Мы храним 'salt$iterated_hash(password, salt)'. Соль состоит из 32 случайных байтов, и ее цель в том, что если два разных человека выберут один и тот же пароль, сохраненные пароли будут выглядеть по-разному.

Это iterated_hash, в основном, hash(hash(hash(... hash(password, salt) ...)))делает потенциальным злоумышленником, имеющим доступ к вашей базе данных, очень дорого угадывать пароли, хэшировать их и искать хэши в базе данных. Вы должны вычислять это iterated_hashкаждый раз, когда пользователь входит в систему, но это не будет стоить вам слишком много по сравнению с злоумышленником, который тратит почти 100% своего времени на вычисления хэшей.

Мартин Коничек
источник
14
Извините, но теперь почему я должен выбрать это из существующей библиотеки? У библиотеки, вероятно, больше шансов на тщательное изучение. Я сомневаюсь, что каждый из 14 голосов за анализировал код на наличие проблем.
Иоахим Зауэр
2
@JoachimSauer По сути это просто использование библиотеки (javax.crypto), но вы правы - пустые пароли не поддерживаются. Добавлено исключение, чтобы сделать его явным. Спасибо!
Мартин Коничек
3
Вы, вероятно, должны изменить сигнатуры методов char[] passwordвместо String password.
assylias
2
Хотя, похоже, ссылка не получает единодушного согласия. См. Также: security.stackexchange.com/a/20369/12614
assylias,
3
Вы уверены, что .equals () в строках не закорачивает (то есть: останавливает зацикливание, когда находит два неравных байта)? Если это произойдет, существует риск утечки информации о хэше пароля.
bobpoekert
28

BCrypt - очень хорошая библиотека, и у нее есть порт Java .

Майкл Боргвардт
источник
7

Вы можете использовать Широ (ранее библиотеки JSecurity ) осуществление того , что описывается OWASP .

Также похоже, что библиотека JASYPT имеет аналогичную утилиту .

Лаз
источник
Это на самом деле то, что я использовал. Но поскольку мы решили не использовать Shiro, возникла некоторая обеспокоенность по поводу неэффективности необходимости включать всю библиотеку Shiro только для одного этого пакета.
Крис Датроу
Я не знаю библиотеки, состоящей только из утилиты хеширования паролей. Вам, вероятно, лучше откатывать свои собственные, если зависимости являются проблемой. Ответ Эриксона выглядит довольно хорошо для меня. Или просто скопируйте код из той ссылки OWASP, на которую я ссылался, если вы предпочитаете использовать SHA безопасным способом.
лаз
7

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

Вы должны использовать другой алгоритм, такой как bcrypt, PBKDF2 и scrypt, чтобы хранить ваши пароли. Смотрите здесь .

Bozho
источник
3
Как бы вы хэшировали пароль при входе в систему без сохранения соли в базе данных?
ZZ Coder
9
Использование имени пользователя в качестве соли не является фатальным недостатком, но это далеко не так хорошо, как использование соли из криптографического ГСЧ. И нет абсолютно никаких проблем с хранением соли в базе данных. Соль не секрет.
Эриксон
1
Разве имя пользователя и электронная почта также не будут храниться в базе данных?
Крис Датроу
@ZZ Coder, @erickson правильно, я почему-то предположил, что это будет одна соль для всех паролей, что приведет к легко вычислимой радужной таблице.
Божо
13
Одна проблема с использованием имени пользователя (или другого идентификатора, такого как электронная почта) в качестве соли заключается в том, что вы не можете затем изменить идентификатор, не заставив пользователя также установить новый пароль.
Лоуренс Дол
6

В дополнение к bcrypt и PBKDF2, упомянутым в других ответах, я бы порекомендовал посмотреть на scrypt

MD5 и SHA-1 не рекомендуются, так как они относительно быстрые, поэтому при использовании распределенных вычислений «плата за час» (например, EC2) или современного высокопроизводительного графического процессора можно «взломать» пароли, используя атаки методом «грубой силы» / по словарю, при относительно низких затратах и ​​разумных время.

Если вы должны использовать их, то, по крайней мере, повторите алгоритм заранее определенное значительное количество раз (1000+).

Эран Медан
источник
6

Полностью согласен с Эриксоном, что PBKDF2 является ответом.

Если у вас нет этой опции или вам нужно использовать только хеш, Apache Commons DigestUtils намного проще, чем правильно делать код JCE: https://commons.apache.org/proper/commons-codec/apidocs/org/apache /commons/codec/digest/DigestUtils.html

Если вы используете хеш, используйте sha256 или sha512. На этой странице есть хорошие рекомендации по обработке паролей и хэшированию (обратите внимание, что для обработки паролей хеширование не рекомендуется): http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html

Дэвид Карбони
источник
Стоит отметить, что SHA512 не лучше, чем SHA256 (для этой цели) только потому, что число больше.
Azsgy
5

Вы можете использовать Spring Security Crypto (имеет только 2 необязательные зависимости компиляции ), который поддерживает шифрование паролей PBKDF2 , BCrypt , SCrypt и Argon2 .

Argon2PasswordEncoder argon2PasswordEncoder = new Argon2PasswordEncoder();
String aCryptedPassword = argon2PasswordEncoder.encode("password");
boolean passwordIsValid = argon2PasswordEncoder.matches("password", aCryptedPassword);
SCryptPasswordEncoder sCryptPasswordEncoder = new SCryptPasswordEncoder();
String sCryptedPassword = sCryptPasswordEncoder.encode("password");
boolean passwordIsValid = sCryptPasswordEncoder.matches("password", sCryptedPassword);
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String bCryptedPassword = bCryptPasswordEncoder.encode("password");
boolean passwordIsValid = bCryptPasswordEncoder.matches("password", bCryptedPassword);
Pbkdf2PasswordEncoder pbkdf2PasswordEncoder = new Pbkdf2PasswordEncoder();
String pbkdf2CryptedPassword = pbkdf2PasswordEncoder.encode("password");
boolean passwordIsValid = pbkdf2PasswordEncoder.matches("password", pbkdf2CryptedPassword);
Базз-DEE
источник
4

Хотя рекомендация NIST PBKDF2 уже упоминалась, я хотел бы отметить, что в период с 2013 по 2015 год проводился открытый конкурс на хеширование паролей . В конце концов, в качестве рекомендуемой функции хеширования паролей был выбран Argon2 .

Существует довольно хорошо принятая Java-привязка для исходной библиотеки (native C), которую вы можете использовать.

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

Для получения дополнительной информации о безопасности функций хеширования паролей см. Security.se .

Qw3ry
источник
@zaph Я отредактировал ответ, чтобы быть более объективным. Пожалуйста, имейте в виду, что рекомендация NIST не всегда может быть лучшим выбором (см. Здесь пример) - конечно, это верно для всего, что рекомендуется также где-то еще. Поэтому я думаю, что этот ответ дает ценность для этого вопроса.
Qw3ry
2

Здесь у вас есть две ссылки для хеширования MD5 и других методов хеширования:

API Javadoc: https://docs.oracle.com/javase/1.5.0/docs/api/java/security/MessageDigest.html

Учебное пособие: http://www.twmacinta.com/myjava/fast_md5.php

Саймон
источник
3
Просто имейте в виду, что для хэширования пароля чем медленнее, тем лучше. Вы должны использовать тысячи итераций хеш-функции в качестве техники «усиления ключа». Кроме того, соль является обязательным условием.
Эриксон
У меня сложилось впечатление, что несколько итераций качественного алгоритма хеширования обеспечат примерно ту же безопасность, что и одна итерация, поскольку длина байтов все равно будет одинаковой?
Крис Датроу
@erickson Было бы лучше явно замедлить нападавших.
Деймон
6
О ключевом усилении: существуют соли, чтобы сделать предварительно вычисленные хэши непригодными. Но злоумышленникам не нужно заранее вычислять. Атакующие могут просто хэшировать строки + соль «на лету», пока не найдут нужную. Но если вы повторяете тысячи раз для своих хэшей, им придется делать то же самое. На ваш сервер не будут сильно влиять итерации по 10 тыс., Так как это случается не так часто. Злоумышленникам потребуется в 10 тысяч раз больше вычислительной мощности.
zockman
2
@Simon сегодня MD5 считается бесполезным для хеширования паролей, поскольку он может быть взломан за считанные секунды, используя атаки GPU грубой силы / словаря. Смотрите здесь: codahale.com/how-to-safely-store-a-password
Эран Медан
1

Среди всех стандартных схем хеширования LDAP ssha является наиболее безопасным для использования,

http://www.openldap.org/faq/data/cache/347.html

Я бы просто следовал указанным там алгоритмам и использовал MessageDigest для хеширования.

Вы должны хранить соль в своей базе данных, как вы предложили.

ZZ Coder
источник
1
Поскольку SSHA не выполняет итерацию хеш-функции, она слишком быстрая. Это позволяет злоумышленникам быстрее использовать пароли. Лучшие алгоритмы, такие как Bcrypt, PBBKDF1 и PBKDF2, используют методы «усиления ключа», чтобы замедлить действия злоумышленников до того момента, когда срок действия пароля истечет, прежде чем они смогут перебить даже 8-буквенное пространство для паролей.
Эриксон
Проблема всех этих механизмов заключается в том, что вы не получаете поддержку клиентов. Проблема с хешированным паролем заключается в том, что вы не можете поддерживать хеширование пароля с помощью других алгоритмов. С ssha, по крайней мере, все клиенты LDAP поддерживают это.
ZZ Coder
Он не «самый безопасный», он просто «довольно совместимый». bcrypt / scrypt более интенсивно используют ресурсы.
Eckes
1

По состоянию на 2020 год наиболее надежный и гибкий алгоритм в использовании,

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

является Argon2id или Argon2i .

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

  • Argon2i специализируется на жадном хешировании памяти
  • Argon2d специализируется на жадном хешировании процессора
  • Argon2id использует оба метода.

Жадное хеширование памяти поможет против использования GPU для взлома.

Внедрение Spring Security / Bouncy Castle не оптимизировано и относительно недолго, учитывая, что может использовать злоумышленник. cf: Spring документация

В настоящее время реализация использует Bouncy Castle, который не использует параллелизм / оптимизацию, как взломщики паролей, поэтому между злоумышленником и защитником существует ненужная асимметрия.

Наиболее вероятная реализация, используемая для Java, это mkammerer 's,

jar / библиотека-оболочка официальной нативной реализации, написанной на Rust.

Это хорошо написано и просто в использовании.

Встроенная версия предоставляет встроенные сборки для Linux, Windows и OSX.

В качестве примера, он используется JPMorganChase в тессера проекте безопасности , используемого для обеспечения кворума , его Эфириума реализации cryptocurency.

Вот пример кода от Тессера.

Калибровка может быть выполнена с использованием de.mkammerer.argon2.Argon2Helper # findIterations

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

user1767316
источник
0

я опирался на это видео из udemy и отредактировал, чтобы он был сильнее случайного пароля

}

private String pass() {
        String passswet="1234567890zxcvbbnmasdfghjklop[iuytrtewq@#$%^&*" ;

        char icon1;
        char[] t=new char[20];

         int rand1=(int)(Math.random()*6)+38;//to make a random within the range of special characters

            icon1=passswet.charAt(rand1);//will produce char with a special character

        int i=0;
        while( i <11) {

             int rand=(int)(Math.random()*passswet.length());
             //notice (int) as the original value of Math>random() is double

             t[i] =passswet.charAt(rand);

             i++;
                t[10]=icon1;
//to replace the specified item with icon1
         }
        return new String(t);
}






}
سحنون المالكى
источник
Я открыт для исправления, но я думаю, что вы не должны использовать случайные числа при хешировании. Это так, что ваша хеш-функция остается детерминированной; то есть если вы хешируете строку несколько раз, вы всегда получите одно и то же значение хеша для этой строки.
Дульди