Рубрики
Без рубрики

Как безопасно хранить пароль в Java

В предыдущей версии этой статьи процесс “хеширования” путался с п… Помеченный java, безопасность, шифрование, хэширование.

В предыдущей версии этой статьи процесс “хеширования” путался с процессом “шифрования”. Стоит отметить, что, хотя хеширование и шифрование похожи, это два разных процесса.

Хэширование включает преобразование “многие к одному”, при котором заданный входной сигнал сопоставляется выходу (обычно фиксированного размера, обычно короче), который не является уникальным для данного конкретного входного сигнала. Другими словами, столкновения вероятны при выполнении алгоритма хеширования в огромном диапазоне входных значений (где разные входные данные сопоставляются с одним и тем же выходом). Хеширование является необратимым процессом из-за природы алгоритмов хеширования. Этот ответ SO дает хороший обзор различий между хешированием и шифрованием и дает хороший пример того, почему алгоритм хеширования может быть практически необратимым.

В качестве математического примера рассмотрим модуль (он же модуль, он же. mod) функция ( выраженная здесь, как определено Дональдом Кнутом ):

a % n = mod(a, n) = a - n * floor(a/n)

Простая интерпретация функции mod заключается в том, что это остаток от целочисленного деления:

7 % 2 = 1
4 % 3 = 1
33 % 16 = 1

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

Алгоритмы хеширования используют преимущества таких необратимых функций, как эта (а также битовые сдвиги и т.д.), И часто повторяются много раз, астрономически увеличивая количество требуемых догадок. Цель хеширования состоит в том, чтобы сделать расшифровку исходной информации чрезвычайно дорогостоящей с вычислительной точки зрения. Часто проще применить грубую силу алгоритм хэширования (используя как можно больше возможных входных данных, как можно быстрее), чем пытаться “перевернуть” алгоритм хэширования для декодирования хэшированной информации.

Распространенный метод сдерживания даже этих атак с применением грубой силы заключается в добавлении второго случайного фрагмента информации в качестве “соли”. Это предотвращает хакеров от выполнения атак по словарю , где список распространенных паролей сопоставляется с их хэшированными выводами – если хакер знает используемый алгоритм хэширования и может получить доступ к базе данных, в которой хранятся хэши, они могут использовать свой “словарь” для сопоставления с исходным паролем и получить доступ к этим учетным записям. Засаливание данных означает, что хакеры не только должны запустить определенный пароль через алгоритм хеширования и убедиться, что он соответствует хэшированным выводам, но и должны повторно запустить этот процесс для каждого возможного значения соли (обычно строка от десятков до сотен байт). Случайная 100-байтовая соль означает (100*2^8 или), что для каждого возможного пароля необходимо попробовать 256 000 общих комбинаций пароль-соль.

Другой метод предотвращения атак методом грубой силы состоит в том, чтобы просто потребовать, чтобы ваш алгоритм выполнялся некоторое время. Если ваш алгоритм запускается всего за 2 секунды и использует 100-байтовую соль, упомянутую выше, потребуется почти 6 дней, чтобы попробовать все возможные строки соли только для одного потенциального пароля. Отчасти поэтому алгоритмы хеширования часто повторяются тысячи раз. Чтобы создать безопасный хэш, убедитесь, что вы повторно вводите соль при каждой итерации, в противном случае вы настраиваете себя на большее количество столкновений хэшей, чем необходимо (см. Ссылку SO выше).

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

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

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

Шифрование с симметричным ключом означает, что для шифрования и дешифрования данных используется один и тот же криптографический ключ. Распространенной аналогией, используемой для объяснения шифрования, является отправка заблокированных пакетов по почте. Если вы повесите висячий замок на коробку и пришлете ее мне, а у меня есть копия ключа, который открывает висячий замок, то я легко смогу отпереть его и прочитать ваше сообщение внутри. Я могу отправить вам ответное сообщение, и вы сможете использовать тот же ключ, чтобы открыть коробку с другой стороны.

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

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

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

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

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

Оригинальная статья (обновлена для ясности):

Процесс хэширования пароля на Java может быть трудным для понимания поначалу, но на самом деле вам нужно всего три вещи:

  1. пароль
  2. алгоритм хеширования
  3. немного вкусного соль

(Обратите внимание, что шифрование и дешифрование также возможно в Java, но обычно не используется для паролей, потому что сам пароль не нуждается в восстановлении. Нам просто нужно проверить, что пароль, который вводит пользователь, воссоздает хэш, который мы сохранили в базе данных.)

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

Мы можем генерировать соль просто с помощью класса Java SecureRandom :

import java.security.SecureRandom;
import java.util.Base64;
import java.util.Optional;

  private static final SecureRandom RAND = new SecureRandom();

  public static Optional generateSalt (final int length) {

    if (length < 1) {
      System.err.println("error in generateSalt: length must be > 0");
      return Optional.empty();
    }

    byte[] salt = new byte[length];
    RAND.nextBytes(salt);

    return Optional.of(Base64.getEncoder().encodeToString(salt));
  }

(Примечание: вы также можете получить a Безопасность и безопасность пример с SecureRandom.getinstancestrong() , хотя это бросает Исключение NoSuchAlgorithmException и поэтому должен быть завернут в a попробуйте{} поймать(){} блок.)

Далее нам понадобится сам код хэширования:

import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;

  private static final int ITERATIONS = 65536;
  private static final int KEY_LENGTH = 512;
  private static final String ALGORITHM = "PBKDF2WithHmacSHA512";

  public static Optional hashPassword (String password, String salt) {

    char[] chars = password.toCharArray();
    byte[] bytes = salt.getBytes();

    PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH);

    Arrays.fill(chars, Character.MIN_VALUE);

    try {
      SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM);
      byte[] securePassword = fac.generateSecret(spec).getEncoded();
      return Optional.of(Base64.getEncoder().encodeToString(securePassword));

    } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
      System.err.println("Exception encountered in hashPassword()");
      return Optional.empty();

    } finally {
      spec.clearPassword();
    }
  }

…здесь многое происходит, поэтому позвольте мне объяснить шаг за шагом:

  public static Optional hashPassword (String password, String salt) {

    char[] chars = password.toCharArray();
    byte[] bytes = salt.getBytes();

Во-первых, нам в конечном итоге нужен пароль в виде char[] , но у нас есть пользователь, передающий его как Строка — (как еще мы могли бы получить пароль от пользователя?) — поэтому мы должны преобразовать его в символ[] с самого начала. Соль также передается в виде строки и должна быть преобразована в байт[] . Здесь предполагается, что хэшированный пароль и соль будут записаны в базу данных в виде строк символов, поэтому мы хотим сгенерировать соль вне этого алгоритма в виде Строки и передать ее как Строка , а также.

Сохранение пароля пользователя в Строка опасна, потому что Java String s неизменяемы – как только они созданы, их нельзя перезаписать, чтобы скрыть пароль пользователя. Поэтому лучше всего собрать пароль, сделать с ним то, что нам нужно, и сразу же выбросить ссылку на исходный пароль Строка так что это может быть собранный мусор. (Вы можете предложить , чтобы мусор JVM собирал мертвую ссылку с помощью System.gc() , но сбор мусора происходит с непредсказуемыми интервалами и не может быть принудительно выполнен.) Аналогично, если мы преобразуем пароль Строку в символ[] , мы должны очистить массив, когда закончим с ним (подробнее об этом позже).

    PBEKeySpec spec = new PBEKeySpec(chars, bytes, ITERATIONS, KEY_LENGTH);

Здесь мы указываем, как мы будем генерировать хэшированный пароль. символы – это пароль открытого текста в виде символа[] , байты это соль Строка преобразована в байт[] , ИТЕРАЦИИ это то, сколько раз мы должны выполнить алгоритм хеширования, и ДЛИНА КЛЮЧА – желаемая длина результирующего криптографического ключа в битах.

Когда мы выполняем алгоритм хеширования, мы берем пароль открытого текста и соль и генерируем некоторую псевдослучайную выходную строку. Итерированные алгоритмы хэширования затем повторят этот процесс некоторое количество раз, используя выходные данные первого хэша в качестве входных данных для второго хэша. Это также известно как растяжение ключа и это значительно увеличивает время, необходимое для выполнения атак грубой силы (наличие большой соляной струны также усложняет такие атаки). Обратите внимание, что, хотя увеличение числа итераций увеличивает время, необходимое для хэширования пароля, и делает вашу базу данных менее уязвимой для атак методом перебора, это также может сделать вас более уязвимым для DOS-атак из-за дополнительного времени обработки.

Длина окончательного хэшированного пароля ограничена используемым алгоритмом. Например, “PBKDF2WithHmacSHA1” допускает хэши до 160 бит, в то время как “pbkdf2withhmacsha512” хэши могут быть длиной до 512 бит. Указание KEY_LENGTH больше максимальной длины ключа выбранного вами алгоритма не приведет к удлинению ключа сверх указанного максимального значения и может фактически замедлить алгоритм .

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

    Arrays.fill(chars, Character.MIN_VALUE);

Теперь мы закончили с массивом символов , так что мы можем его очистить. Здесь мы устанавливаем все элементы массива в \000 (нулевой символ).

    try {
      SecretKeyFactory fac = SecretKeyFactory.getInstance(ALGORITHM);
      byte[] securePassword = fac.generateSecret(spec).getEncoded();
      return Optional.of(Base64.getEncoder().encodeToString(securePassword));

    } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
      System.err.println("Exception encountered in hashPassword()");
      return Optional.empty();

    } finally {
      spec.clearPassword();
    }

Наш метод HashPassword() заканчивается на этом попробуйте{} поймать(){} блок. В нем мы сначала получаем алгоритм, который мы определили ранее (“PBKDF2WITHHMACSHA512”), и мы используем этот алгоритм для хэширования пароля открытого текста в соответствии со спецификациями, изложенными в спекуляция . generateSecret() возвращает объект SecretKey , который является “непрозрачным представлением криптографического ключа”, что означает, что он содержит только хэшированный пароль и никакой другой идентифицирующей информации. Мы используем getEncoded() чтобы получить хэшированный пароль в виде байта[] и сохранить его как безопасный пароль .

Если все это пройдет без сучка и задоринки, мы закодируем байты[] в base64 (поэтому он состоит только из печатаемых символов ASCII) и вернем его в виде Строки . Мы делаем это для того, чтобы хэшированный пароль можно было сохранить в базе данных в виде строки символов без каких-либо проблем с кодировкой.

Если таковые имеются Исключения во время процесса шифрования мы возвращаем пустое Необязательно . В противном случае мы завершаем метод, удаляя пароль из спецификации . Теперь в этом методе не осталось ссылок на исходный текстовый пароль. (Обратите внимание, что наконец блок выполняется независимо от того, есть ли Исключение и до того, как что-либо будет возвращено из любого предыдущего попробуйте или поймать блокирует, поэтому безопасно иметь его в конце вот так – пароль будет удален из спецификации .)

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

  public static boolean verifyPassword (String password, String key, String salt) {
    Optional optEncrypted = hashPassword(password, salt);
    if (!optEncrypted.isPresent()) return false;
    return optEncrypted.get().equals(key);
  }

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

Хорошо, теперь, когда у нас есть все это, давайте проверим это! (Примечание: У меня эти методы определены в служебном классе с именем PasswordUtils , в пакете с именем watson .)

jshell> import watson.*

jshell> String salt = PasswordUtils.generateSalt(512).get()
salt ==> "DARMFcJcJDeNMmNMLkZN4rSnHV2OQPDd27yi5fYQ77r2vKTa ... Wt9QZog0wtkx8DQYEAOOwQVs="

jshell> String password = "Of Salesmen!"
password ==> "Of Salesmen!"

jshell> String key = PasswordUtils.hashPassword(password, salt).get()
key ==> "djaaKTM/+X14XZ6rxjN68l3Zx4+5WGkJo3nAs7KzjISiT6aa ... sN5DcmOeMfhqMGCNxq6TIhg=="

jshell> PasswordUtils.verifyPassword("Of Salesmen!", key, salt)
$5 ==> true

jshell> PasswordUtils.verifyPassword("By-Tor! And the Snow Dog!", key, salt)
$6 ==> false

Работает как заклинание! Иди вперед и разбирайся!

Оригинал: “https://dev.to/awwsmm/how-to-encrypt-a-password-in-java-42dh”