bcrypt для Go: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

bcrypt для Go: полное руководство по хешированию паролей

Безопасность пользовательских данных — одна из ключевых задач современной веб-разработки. Хранение паролей в открытом виде недопустимо, а простое хеширование через MD5 или SHA-1 давно признано небезопасным из-за уязвимости к атакам перебором. Библиотека bcrypt для Go предоставляет надёжный и проверенный временем алгоритм адаптивного хеширования паролей, который устойчив как к атакам по словарю, так и к атакам с использованием радужных таблиц.

Алгоритм bcrypt был разработан в 1999 году Нилом Провосом и Дэвидом Мазьером на основе шифра Blowfish. Его ключевая особенность — адаптивная сложность: вы можете увеличивать количество итераций хеширования, делая перебор паролей всё более затратным по мере роста вычислительных мощностей. В Go-экосистеме стандартная библиотека golang.org/x/crypto/bcrypt является официальным расширением и поддерживается командой разработчиков Go.

В этой статье мы подробно разберём все аспекты работы с bcrypt в Go: от установки и базового использования до продвинутых техник и типичных ошибок. Вы узнаете, как правильно хешировать пароли, сравнивать их с хешами, выбирать оптимальную стоимость (cost) и интегрировать bcrypt в реальные приложения.

Установка

Библиотека bcrypt для Go не входит в стандартную библиотеку, но является частью официального подмодуля x/crypto. Установка выполняется одной командой:

go get golang.org/x/crypto/bcrypt

После выполнения этой команды в вашем go.mod появится зависимость, и вы сможете импортировать пакет в свой код:

import "golang.org/x/crypto/bcrypt"

Убедитесь, что у вас установлен Go версии 1.13 или выше (рекомендуется 1.21+). Если вы используете модули (а в 2024 году это стандарт), зависимость подтянется автоматически.

Быстрый старт: минимальный рабочий пример

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

package main

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

func main() {
    // Исходный пароль пользователя
    password := []byte("mySecretPassword123!")

    // Хешируем пароль со стоимостью по умолчанию (10)
    hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Хеш пароля: %s\n", hashedPassword)

    // Проверяем правильный пароль
    err = bcrypt.CompareHashAndPassword(hashedPassword, password)
    if err != nil {
        fmt.Println("Пароль не совпадает!")
    } else {
        fmt.Println("✓ Пароль совпадает!")
    }

    // Проверяем неправильный пароль
    wrongPassword := []byte("wrongPassword")
    err = bcrypt.CompareHashAndPassword(hashedPassword, wrongPassword)
    if err != nil {
        fmt.Println("✗ Пароль не совпадает (как и ожидалось)")
    }
}

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

Основные методы и функции

Пакет bcrypt предоставляет небольшой, но очень продуманный API. Рассмотрим все основные функции с примерами.

1. GenerateFromPassword

Сигнатура: func GenerateFromPassword(password []byte, cost int) ([]byte, error)

Описание: Основная функция для хеширования пароля. Принимает пароль в виде среза байтов и стоимость (cost) — логарифмический показатель количества итераций. Возвращает хеш в стандартном формате bcrypt (содержит соль, стоимость и сам хеш).

package main

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

func main() {
    password := []byte("userPassword123")
    
    // Используем стоимость 12 (более безопасно, но медленнее)
    hash, err := bcrypt.GenerateFromPassword(password, 12)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Длина хеша: %d байт\n", len(hash))
    fmt.Printf("Хеш: %s\n", hash)
    // Вывод: $2a$12$... (начинается с $2a$ или $2b$)
}

2. CompareHashAndPassword

Сигнатура: func CompareHashAndPassword(hashedPassword, password []byte) error

Описание: Сравнивает хеш с паролем. Возвращает nil, если пароль соответствует хешу, или ошибку bcrypt.ErrMismatchedHashAndPassword в противном случае. Важно: функция использует constant-time comparison для защиты от атак по времени.

func checkPassword(hashedPassword, password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
    return err == nil
}

func main() {
    hash := "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy"
    
    fmt.Println(checkPassword(hash, "correctPassword"))  // true
    fmt.Println(checkPassword(hash, "wrongPassword"))    // false
}

3. Cost

Сигнатура: func Cost(hashedPassword []byte) (int, error)

Описание: Извлекает значение стоимости (cost) из существующего хеша. Полезно, если вы хотите проверить, не устарел ли хеш, и перехешировать пароль с более высокой стоимостью.

func main() {
    hash := []byte("$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy")
    
    cost, err := bcrypt.Cost(hash)
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Printf("Текущая стоимость хеша: %d\n", cost)
    
    // Если стоимость меньше 12, рекомендуем обновить
    if cost < 12 {
        fmt.Println("Рекомендуется обновить хеш с более высокой стоимостью")
    }
}

4. DefaultCost

Сигнатура: const DefaultCost = 10

Описание: Константа, определяющая стоимость по умолчанию (10). Это хороший баланс между безопасностью и производительностью для большинства приложений. В 2024 году рекомендуется использовать стоимость 12-14 для новых проектов.

func main() {
    fmt.Printf("Стоимость по умолчанию: %d\n", bcrypt.DefaultCost)
    
    // Эквивалентные вызовы:
    hash1, _ := bcrypt.GenerateFromPassword([]byte("pass"), bcrypt.DefaultCost)
    hash2, _ := bcrypt.GenerateFromPassword([]byte("pass"), 10)
    
    // hash1 и hash2 будут иметь одинаковую стоимость, но разные хеши (из-за соли)
    fmt.Printf("hash1: %s\nhash2: %s\n", hash1, hash2)
}

5. MaxCost

Сигнатура: const MaxCost = 31

Описание: Максимально допустимое значение стоимости. На практике значения выше 20 делают хеширование чрезвычайно медленным (минуты на современном CPU). Используется для валидации входных параметров.

func validateCost(cost int) error {
    if cost < bcrypt.MinCost || cost > bcrypt.MaxCost {
        return fmt.Errorf("cost must be between %d and %d", bcrypt.MinCost, bcrypt.MaxCost)
    }
    return nil
}

func main() {
    fmt.Printf("Максимальная стоимость: %d\n", bcrypt.MaxCost)
    
    // Попытка использовать слишком высокую стоимость
    _, err := bcrypt.GenerateFromPassword([]byte("test"), 32)
    if err != nil {
        fmt.Printf("Ошибка: %v\n", err) // bcrypt: cost out of range
    }
}

6. MinCost

Сигнатура: const MinCost = 4

Описание: Минимально допустимое значение стоимости. Использование стоимости ниже 4 считается небезопасным. Обычно не используется на практике, но может пригодиться для тестов.

func main() {
    // Быстрое хеширование для тестов (НЕ ИСПОЛЬЗОВАТЬ В ПРОДАКШЕНЕ!)
    hash, _ := bcrypt.GenerateFromPassword([]byte("test"), bcrypt.MinCost)
    fmt.Printf("Минимальная стоимость: %d\n", bcrypt.MinCost)
    fmt.Printf("Хеш с минимальной стоимостью: %s\n", hash)
}

7. HashVersion

Сигнатура: const HashVersion = '$2a$' (также есть '$2b$' и '$2x$')

Описание: Префикс, определяющий версию алгоритма bcrypt. $2a$ — оригинальная версия, $2b$ — исправленная версия, которая корректно обрабатывает пароли длиннее 72 символов. Современная библиотека Go использует $2a$, но корректно работает со всеми версиями.

func main() {
    hash, _ := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost)
    hashStr := string(hash)
    
    fmt.Printf("Версия хеша: %s\n", hashStr[:4]) // $2a$
    
    // Проверка, что хеш начинается с корректного префикса
    if len(hashStr) > 4 && (hashStr[:4] == "$2a$" || hashStr[:4] == "$2b$") {
        fmt.Println("Корректный формат bcrypt")
    }
}

8. ErrHashTooShort

Сигнатура: var ErrHashTooShort = errors.New("bcrypt: hash is too short")

Описание: Ошибка, возвращаемая, если переданный хеш имеет недостаточную длину. Полезна для валидации входных данных.

func validateHash(hash string) error {
    _, err := bcrypt.Cost([]byte(hash))
    if errors.Is(err, bcrypt.ErrHashTooShort) {
        return fmt.Errorf("некорректный хеш: слишком короткий")
    }
    return err
}

func main() {
    err := validateHash("short")
    if err != nil {
        fmt.Printf("Ошибка валидации: %v\n", err)
    }
    
    err = validateHash("$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy")
    if err == nil {
        fmt.Println("Хеш корректен")
    }
}

9. ErrMismatchedHashAndPassword

Сигнатура: var ErrMismatchedHashAndPassword = errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password")

Описание: Ошибка, возвращаемая функцией CompareHashAndPassword, когда пароль не соответствует хешу. Важно: не раскрывайте детали ошибки пользователю — просто сообщайте "неверный пароль".

func login(hashedPassword, providedPassword string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(providedPassword))
    if err != nil {
        // Безопасное логирование без раскрытия деталей
        log.Printf("Ошибка аутентификации: %v", err)
        return false
    }
    return true
}

Реальный практический пример: сервис аутентификации пользователей

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

package main

import (
    "errors"
    "fmt"
    "log"
    "sync"
    "time"
    "golang.org/x/crypto/bcrypt"
)

// User представляет модель пользователя
type User struct {
    ID           int
    Username     string
    PasswordHash string
    CreatedAt    time.Time
}

// UserStore — потокобезопасное хранилище пользователей в памяти
type UserStore struct {
    mu       sync.RWMutex
    users    map[string]*User
    nextID   int
}

// NewUserStore создаёт новое хранилище
func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[string]*User),
        nextID: 1,
    }
}

// RegisterUser регистрирует нового пользователя
func (s *UserStore) RegisterUser(username, password string) (*User, error) {
    // Валидация входных данных
    if len(username) < 3 || len(username) > 50 {
        return nil, errors.New("username must be between 3 and 50 characters")
    }
    if len(password) < 8 {
        return nil, errors.New("password must be at least 8 characters")
    }
    if len(password) > 72 {
        return nil, errors.New("password must not exceed 72 characters (bcrypt limitation)")
    }

    s.mu.Lock()
    defer s.mu.Unlock()

    // Проверка, что пользователь не существует
    if _, exists := s.users[username]; exists {
        return nil, errors.New("username already exists")
    }

    // Хеширование пароля с адаптивной стоимостью
    // В 2024 году рекомендуется cost = 12-14 для production
    cost := 12                    

Рекомендаций