Как следует безопасно хранить пароли пользователей в базе данных?

Ответ

Пароли пользователей никогда не должны храниться в открытом виде (plaintext). Единственный безопасный способ — хранить их хеши с использованием криптографически стойких, медленных алгоритмов.

Ключевые принципы:

  1. Используйте медленный алгоритм хеширования. Быстрые алгоритмы (MD5, SHA-256) не подходят, так как они уязвимы для атак перебором (brute-force). Рекомендуемые алгоритмы:

    • Bcrypt: Надежный и широко используемый стандарт.
    • Scrypt: Требует больше памяти, что делает его еще более устойчивым к аппаратным атакам.
    • Argon2: Победитель конкурса Password Hashing Competition, считается самым современным и надежным стандартом.
  2. Используйте соль (Salt). Соль — это случайная строка, которая добавляется к паролю перед хешированием. Это предотвращает атаки с использованием "радужных таблиц" (rainbow tables). Современные библиотеки, такие как bcrypt в Go, генерируют и встраивают соль в хеш автоматически.

  3. Настраивайте "стоимость" (Cost Factor). У медленных алгоритмов есть параметр "стоимости" или "количества раундов", который определяет, насколько ресурсоемким будет процесс хеширования. Его нужно подбирать так, чтобы хеширование занимало приемлемое время (например, 100-500 мс) на вашем оборудовании.

Пример реализации на Go с bcrypt:

package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

// HashPassword хеширует пароль с использованием bcrypt
func HashPassword(password string) (string, error) {
    // bcrypt.DefaultCost равен 10. Для продакшена рекомендуется 12-14.
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    return string(bytes), err
}

// CheckPasswordHash сравнивает пароль с хешем
func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func main() {
    password := "mySecurePa$$w0rd"
    hash, _ := HashPassword(password)

    fmt.Println("Password:", password)
    fmt.Println("Hash:    ", hash)

    match := CheckPasswordHash(password, hash)
    fmt.Println("Match:   ", match) // Выведет: true
}

Ответ 18+ 🔞

Так, слушай, вот сижу, думаю, как же народ до сих пор не въехал в простую, блядь, истину. Пароли в открытом виде хранить — это всё равно что на двери табличку повесить «ключ под ковриком, ёпта». Пиздец, а не безопасность.

Вот смотри, главные правила, без которых ты просто манда с ушами:

  1. Только медленные алгоритмы, нахуй. SHA-256, MD5 — это для проверки целостности файлов, а не для паролей. Их за секунду перебрать можно, овердохуища мощностей сейчас. Нужно такое, что бы каждый хеш считался как будто ты на велосипеде в горку едешь. Bcrypt, Scrypt, Argon2 — вот твои новые боги. Особенно Argon2 — это вообще хитрая жопа, победитель всех конкурсов.

  2. Соль, блядь, соль! Без соли — это как суп без картошки, пресно и уязвимо для радужных таблиц. Это случайная строка, которая мешает хакерам использовать готовые словари. Хорошие библиотеки её сами генерируют и в хеш зашивают, так что не парься.

  3. Крути стоимость. У этих алгоритмов есть настройка «тяжести». Выставляй так, чтобы хеширование занимало, ну, 100-500 миллисекунд. Быстро для тебя, но овердохуища медленно для того, кто миллиарды комбинаций перебирает.

Вот, смотри, как на Go с bcrypt это выглядит, проще простого:

package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

// HashPassword хеширует пароль с использованием bcrypt
func HashPassword(password string) (string, error) {
    // bcrypt.DefaultCost равен 10. Для продакшена рекомендуется 12-14.
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14)
    return string(bytes), err
}

// CheckPasswordHash сравнивает пароль с хешем
func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func main() {
    password := "mySecurePa$$w0rd"
    hash, _ := HashPassword(password)

    fmt.Println("Password:", password)
    fmt.Println("Hash:    ", hash)

    match := CheckPasswordHash(password, hash)
    fmt.Println("Match:   ", match) // Выведет: true
}

Видишь? Никакой магии. Сгенерировал хеш, сохранил его в базу. Когда пользователь логинится — берёшь введённый пароль, сравниваешь с этим хешом. Всё, пизда, безопасно. Библиотека сама и соль достанет, и проверит.

Главное — не выёбывайся и не пытайся свою криптографию написать. Возьми проверенное, и спи спокойно. А то потом будешь как тот Герасим, смотреть на утонувшую базу пользователей и мычать «Му-му...», а нихуя не исправишь.