Статьи

Безопасное хранение паролей — что нельзя, что нужно и пример на Java

Важность безопасного хранения паролей
Как разработчики программного обеспечения, одной из наших самых важных обязанностей является защита личной информации наших пользователей. Без технических знаний о наших приложениях у пользователей нет другого выбора, кроме как полагать, что мы выполняем эту ответственность. К сожалению, когда дело доходит до паролей, сообщество разработчиков программного обеспечения имеет точный послужной список.
Хотя невозможно создать 100% безопасную систему, к счастью, есть несколько простых шагов, которые мы можем предпринять, чтобы сделать пароли наших пользователей достаточно безопасными для отправки потенциальных хакеров в поисках более легкой добычи.
Если вам не нужен весь фон, не стесняйтесь переходить к примеру Java SE ниже .
Не надо
Во-первых, давайте быстро обсудим некоторые вещи, которые вы не должны делать при создании приложения, требующего аутентификации:
  • Не храните данные аутентификации, если вам не нужно. Это может показаться отговоркой, но прежде чем вы начнете создавать базу данных с учетными данными пользователя, подумайте о том, чтобы позволить кому-то другому справиться с этим. Если вы создаете общедоступное приложение, рассмотрите возможность использования поставщиков OAuth , таких как Google или Facebook . Если вы создаете внутреннее корпоративное приложение, рассмотрите возможность использования любых существующих служб внутренней аутентификации, таких как корпоративная служба LDAP или Kerberos. Будь то общедоступное или внутреннее приложение, ваши пользователи оценят, что не нужно запоминать другой идентификатор пользователя и пароль, и для атак хакеров это на одну базу данных меньше.
  • Если вы должны хранить данные аутентификации, ради Гослинга не храните пароли в виде открытого текста . Это должно быть очевидно, но стоит упомянуть. Давайте хотя бы заставим хакеров вспотеть.
  • Не используйте двустороннее шифрование, если вам действительно не нужно восстановить пароль в виде открытого текста. Вам нужно знать их открытый текстовый пароль, только если вы используете их учетные данные для взаимодействия с внешней системой от их имени. Даже тогда вам лучше иметь аутентификацию пользователя в этой системе напрямую. Чтобы быть понятным, вам не нужно использовать исходный открытый текстовый пароль пользователя для выполнения аутентификации в вашем приложении . Я подробнее расскажу об этом позже, но при выполнении аутентификации вы будете применять алгоритм шифрования к паролю, введенному пользователем, и сравнивать его с зашифрованным паролем, который вы сохранили.
  • Не используйте устаревшие алгоритмы хеширования, такие как MD5 . Честно говоря, хеширование пароля с помощью MD5 практически бесполезно. Вот хешированный MD5 пароль: 569a70c2ccd0ac41c9d1637afe8cd932 . Зайдите на http://www.md5hacker.com/ и вы сможете расшифровать его за считанные секунды.
  • Не придумывайте свою собственную схему шифрования. В мире есть несколько блестящих экспертов по шифрованию, которые способны перехитрить хакеров и разработать новый алгоритм шифрования. Я не один из них, и, скорее всего, вы тоже. Если хакер получит доступ к вашей пользовательской базе данных, он, вероятно, сможет получить и ваш код. Если вы не изобрели следующего великого преемника PBKDF2 или bcrypt , они будут маниакально хихикать, поскольку они быстро взломают пароли всех ваших пользователей и опубликуют их в даркнете .
Дос
Хорошо, достаточно лекций о том, что не нужно делать. Вот вещи, на которых вы должны сосредоточиться:
  • Выберите алгоритм одностороннего шифрования. Как я уже упоминал выше, после того как вы зашифровали и сохранили пароль пользователя, вам больше не нужно знать реальное значение. Когда пользователь пытается аутентифицироваться, вы просто применяете тот же алгоритм к введенному им паролю и сравниваете его с зашифрованным паролем, который вы сохранили.
  • Сделайте шифрование настолько медленным, насколько ваше приложение может выдержать . Любой современный алгоритм шифрования пароля должен позволять вам предоставлять параметры, которые увеличивают время, необходимое для шифрования пароля (например, в PBKDF2, указывая количество итераций). Почему медленно хорошо? Ваши пользователи не заметят, если для шифрования их пароля потребуются дополнительные 100 мс, но хакер, пытающийся атаковать грубой силой, заметит разницу, запустив алгоритм миллиарды раз.
  • Выберите известный алгоритм . Национальный институт стандартов и технологий (NIST) рекомендует PBKDF2 для паролей. bcrypt является популярной и устоявшейся альтернативой, а scrypt является относительно новым алгоритмом, который был хорошо принят. Все это популярно по причине: они хороши.
PBKDF2
Прежде чем я покажу вам конкретный код, давайте немного поговорим о том, почему PBKDF2 является хорошим выбором для шифрования паролей:
  • Рекомендовано NIST. Раздел 5.3 Специальной публикации 800-132 рекомендует PBKDF2 для шифрования паролей. Сотрудники службы безопасности будут любить это.
  • Регулируемый ключ растяжения, чтобы победить атаки грубой силы . Основная идея растяжения ключа заключается в том, что после применения алгоритма хеширования к паролю вы продолжаете много раз применять один и тот же алгоритм к результату (счетчик итераций). Если хакеры пытаются взломать ваши пароли, это значительно увеличивает время, необходимое для попытки миллиардов возможных паролей. Как упоминалось ранее, чем медленнее, тем лучше. PBKDF2 позволяет вам указать количество итераций, которые вы хотите применить, позволяя сделать это так медленно, как вам нравится.
  • Необходимая соль для победы над атаками радужного стола и предотвращения столкновений с другими пользователями. Соль — это случайно сгенерированная последовательность битов, которая уникальна для каждого пользователя и добавляется к паролю пользователя как часть хеширования. Это предотвращает атаки радужных таблиц , делая невозможным использование предварительно вычисленного списка результатов. И поскольку каждый пользователь получает свою собственную соль, даже если два пользователя имеют один и тот же пароль, зашифрованные значения будут разными. Существует много противоречивой информации о том, должны ли соли храниться где-то отдельно от зашифрованных паролей. Поскольку растяжение ключа в PBKDF2 уже защищает нас от атак грубой силы, я чувствую, что нет необходимости пытаться скрыть соль. Раздел 3.1 NIST SP 800-132 также определяет соль как «не секретное двоичное значение», так что это то, с чем я согласен.
  • Часть Java SE 6 . Никаких дополнительных библиотек не требуется. Это особенно привлекательно для тех, кто работает в средах с ограничительными политиками с открытым исходным кодом.
Хорошо, вот код для шифрования паролей с использованием PBKDF2. Требуется только Java SE 6.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
 
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
 
public class PasswordEncryptionService {
 
 public boolean authenticate(String attemptedPassword, byte[] encryptedPassword, byte[] salt)
   throws NoSuchAlgorithmException, InvalidKeySpecException {
  // Encrypt the clear-text password using the same salt that was used to
  // encrypt the original password
  byte[] encryptedAttemptedPassword = getEncryptedPassword(attemptedPassword, salt);
 
  // Authentication succeeds if encrypted password that the user entered
  // is equal to the stored hash
  return Arrays.equals(encryptedPassword, encryptedAttemptedPassword);
 }
 
 public byte[] getEncryptedPassword(String password, byte[] salt)
   throws NoSuchAlgorithmException, InvalidKeySpecException {
  // PBKDF2 with SHA-1 as the hashing algorithm. Note that the NIST
  // specifically names SHA-1 as an acceptable hashing algorithm for PBKDF2
  String algorithm = "PBKDF2WithHmacSHA1";
  // SHA-1 generates 160 bit hashes, so that's what makes sense here
  int derivedKeyLength = 160;
  // Pick an iteration count that works for you. The NIST recommends at
  // least 1,000 iterations:
  // iOS 4.x reportedly uses 10,000:
  int iterations = 20000;
 
  KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, iterations, derivedKeyLength);
 
  SecretKeyFactory f = SecretKeyFactory.getInstance(algorithm);
 
  return f.generateSecret(spec).getEncoded();
 }
 
 public byte[] generateSalt() throws NoSuchAlgorithmException {
  // VERY important to use SecureRandom instead of just Random
  SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
 
  // Generate a 8 byte (64 bit) salt as recommended by RSA PKCS5
  byte[] salt = new byte[8];
  random.nextBytes(salt);
 
  return salt;
 }
}
Поток идет примерно так:
  1. При добавлении нового пользователя вызовите generateSalt () , затем getEncryptedPassword () и сохраните зашифрованный пароль и соль. Не храните открытый текстовый пароль. Не беспокойтесь о хранении соли в отдельной таблице или месте из зашифрованного пароля; как обсуждалось выше, соль не является секретом.
  2. При аутентификации пользователя, извлеките ранее зашифрованный пароль и соль из базы данных, затем отправьте их и пароль в виде открытого текста, который они ввели для аутентификации () . Если он возвращает true, аутентификация прошла успешно.
  3. Когда пользователь меняет свой пароль, безопасно повторно использовать его старую соль; Вы можете просто вызвать getEncryptedPassword () со старой солью.
Достаточно просто, правда? Если вы создаете или поддерживаете приложение, которое нарушает любое из перечисленных выше «запретов», пожалуйста, сделайте одолжение своим пользователям и используйте что-то вроде PBKDF2 или bcrypt. Помоги им, разработчик Оби-Вана, ты их единственная надежда.