Як я можу хеш-пароль на Java?


176

Мені потрібно хеш-паролі для зберігання в базі даних. Як я можу це зробити на Java?

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

Тоді, коли користувач захотів увійти, я міг взяти поданий пароль, додати випадкову сіль із інформації свого облікового запису, хеш і перевірити, чи прирівнюється він до збереженого пароля хешу з інформацією про його рахунок.


11
@YGL це насправді не є рекомбінацією, коли атаки GPU є настільки дешевими, сім'я SHA насправді дуже поганий вибір для хешування паролів (занадто швидко) навіть з сіллю. Використовуйте bcrypt, scrypt або PBKDF2
Eran Medan

11
Чому це питання було закрито? Це питання для реальної інженерної проблеми, і відповіді неоцінені. ОП не запитує бібліотеку, він запитує, як вирішити інженерну проблему.
stackoverflowuser2010

12
Просто неймовірно. Це питання має 52 відгуки, і хтось вирішує закрити його як "поза темою".
stackoverflowuser2010

1
Так, я раніше писав на Meta про цю проблему закриття, хоч і дуже побили.
Кріс Датроу

8
Це питання слід знову відкрити. Це питання про те, як написати програму для вирішення описаної проблеми (автентифікація пароля) з коротким рішенням коду. Побачення тригерного слова "бібліотека" не виправдовує рефлекторного закриття питання; він не запитує рекомендації щодо бібліотеки, він запитує, як хеш-паролі. Редагувати: там виправлено.
erickson

Відповіді:


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 підкреслює, чому нам потрібна бібліотека , а не блок коду, який майже є. Страшно, що прийнята відповідь не відповідає на питання на таку важливу тему.
Nilzor

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 Так, час для неправильних паролів буде більшим ; точніше, неправильні паролі займуть той самий час, що і правильні паролі. Це запобігає тимчасовим атакам, хоча, зізнаюся, я не можу придумати практичного способу використання такої вразливості в даному випадку. Але ви не ріжете кути. Тільки тому, що я цього не бачу, це ще не означає, що розумніший розум не стане.
erickson

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


7

Ви можете використовувати Shiro бібліотека (раніше JSecurity ) здійснення того , що описується OWASP .

Також схоже, що бібліотека JASYPT має подібну корисну програму .


Це власне те, що я використовував. Але оскільки ми вирішили не використовувати Shiro, то виникло певне занепокоєння щодо неефективності необхідності включити всю бібліотеку Shiro лише для одного пакету.
Кріс Датроу

Я не знаю про бібліотеку, що складається лише з утиліти хешування паролів. Вам, мабуть, краще прокрутити власні, якщо залежність викликає занепокоєння. Відповідь Erickson мені виглядає досить добре. Або просто скопіюйте код із того посилання на OWASP, на яке я посилався, якщо ви хочете використовувати SHA безпечним способом.
лаз

7

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

Ви повинні використовувати інший алгоритм, такий як bcrypt, PBKDF2 та scrypt, щоб зберігати ваші паролі. Дивіться тут .


3
Як би ви хеш-пароль при вході в систему, не зберігаючи сіль у базі даних?
ZZ Coder

9
Використання імені користувача як солі не є фатальним недоліком, але це ніде не так добре, як використання солі з криптографічного RNG. І з зберіганням солі в базі даних абсолютно немає проблем. Сіль не є секретом.
erickson

1
Чи не буде також ім’я користувача та електронна пошта зберігатися в базі даних?
Кріс Датроу

@ZZ Coder, @erickson правильний, я якось припустив, що це буде одна сіль для всіх паролів, що призведе до легко обчислюваної таблиці веселки.
Божо

13
Одна з проблем використання імені користувача (або іншого ідентифікатора, наприклад електронної пошти) в якості солі, полягає в тому, що ви не можете змінити ідентифікатор, не встановивши користувачеві також новий пароль.
Лоуренс Дол

6

Окрім bcrypt та PBKDF2, згаданих в інших відповідях, я б рекомендував переглянути scrypt

MD5 і SHA-1 не рекомендуються, оскільки вони відносно швидкі, тому використовуючи розподілені обчислення "оренду за годину" (наприклад, EC2) або сучасний GPU високого класу, можна "зламати" паролі, використовуючи атаки грубої сили / словника при порівняно низьких витратах і розумних час.

Якщо ви повинні їх використовувати, то принаймні повторіть алгоритм заздалегідь задану значну кількість разів (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 і Argon 2 шифрування паролів.

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);

4

Незважаючи на те, що рекомендація NIST PBKDF2 вже згадувалася, я хотів би зазначити, що існувала публічна конкуренція за тестування паролів, яка проходила з 2013 по 2015 рік. Зрештою, Argon2 був обраний як рекомендована функція переміщення паролів.

Існує досить добре прийнята Java-прив’язка для оригінальної (рідної C) бібліотеки, яку ви можете використовувати.

У середньому випадку використання, я не думаю, що це має значення з точки зору безпеки, якщо ви вибираєте PBKDF2 над Argon2 або навпаки. Якщо у вас є чіткі вимоги до безпеки, рекомендую врахувати Argon2 у вашій оцінці.

Детальну інформацію про безпеку функцій хешування паролів див. У безпеці .


@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

У мене склалося враження, що багаторазові ітерації алгоритму хешування якості створюють приблизно таку ж безпеку, що і одна ітерація, оскільки довжина байтів все одно буде однаковою?
Кріс Дутроу

@erickson Краще явно сповільнити нападників.
Діамон

6
Про ключове посилення: солі існують, щоб зробити попередньо обчислені хеші непридатними. Але зловмисникам не доводиться брати участь в обчисленні. Зловмисники можуть просто хеш-струни + сіль "на льоту", поки не знайдуть потрібну. Але якщо ви повторите тисячі разів для своїх хешей, вони повинні будуть робити те саме. Ітерації на 10K не вплинуть на ваш сервер, оскільки це трапляється не так часто. Зловмисникам знадобиться обчислювальна потужність у 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, щоб зробити хеш.

Потрібно зберігати сіль у вашій базі даних, як ви запропонували.


1
Оскільки SSHA не повторює хеш-функцію, це занадто швидко. Це дозволяє зловмисникам швидше спробувати паролі. Кращі алгоритми, такі як Bcrypt, PBBKDF1 та PBKDF2, використовують методи "підсилення ключів", щоб уповільнити зловмисників до того моменту, коли повинен спливати пароль, перш ніж вони зможуть примусити навіть 8-літерний простір паролів.
еріксон

Проблема всіх цих механізмів полягає в тому, що ви не отримуєте підтримки клієнтів. Проблема з хешованим паролем полягає в тому, що ви не можете підтримувати хеширование паролів за допомогою інших алгоритмів. Що стосується ssha, принаймні всі клієнти LDAP підтримують його.
ZZ Coder

Це не "найбезпечніше", а просто "досить сумісне". bcrypt / scrypt - набагато більш інтенсивні ресурси.
eckes

1

Станом на 2020 рік, найбільш надійний і гнучкий алгоритм у використанні,

той, який найімовірніше оптимізує свою міцність, враховуючи будь-яке обладнання,

є Argon2id або Argon2i .

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

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

Жадібне хешування пам'яті допоможе проти використання GPU для розтріскування.

Весняна безпека / Реалізація замкового підстрибу не оптимізована та відносно тиждень враховує те, що може використати нападник. cf: Весняна документація

В даний час реалізація використовує замок Bouncy, який не використовує паралелізм / оптимізацію, яку будуть мати взломники паролів, тому між нападником та захисником існує непотрібна асиметрія.

Найбільш надійна реалізація, яка використовується для Java, - це mkammerer 's,

баночка / бібліотека офіційної натурної реалізації, написана в Rust.

Він добре написаний і простий у використанні.

Вбудована версія забезпечує вбудовані версії для Linux, Windows та OSX.

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

Ось приклад коду від tessera.

Калібрування може проводитися за допомогою de.mkammerer.argon2.Argon2Helper # findIterations

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


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);
}






}

Я готовий виправлятись, але, думаю, вам не слід використовувати випадкові числа при хешуванні. Це так, що ваша хеш-функція залишається детермінованою; тобто якщо ви хешуєте рядок кілька разів, ви завжди отримаєте одне й те саме значення хешу для цієї рядка.
дулді
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.