sha2 / bcrypt для Rust: полное руководство с примерами
В современной разработке безопасность данных является критическим аспектом любого приложения. Библиотеки sha2 и bcrypt для языка Rust предоставляют мощные и безопасные инструменты для криптографического хеширования. sha2 реализует семейство алгоритмов SHA-2 (Secure Hash Algorithm 2), включая SHA-224, SHA-256, SHA-384, SHA-512, а также их варианты с усечением (SHA-512/224, SHA-512/256). bcrypt — это адаптивная криптографическая хеш-функция, специально разработанная для безопасного хранения паролей. Она использует "соль" (salt) и настраиваемое количество раундов (cost factor), что делает её устойчивой к атакам с использованием радужных таблиц и перебору.
Зачем это нужно? SHA-2 используется для проверки целостности данных, создания цифровых подписей, в системах контроля версий (Git), блокчейне и многих других областях, где требуется быстрый и надёжный хеш. Bcrypt, в свою очередь, является стандартом де-факто для хеширования паролей в веб-приложениях, базах данных и системах аутентификации. Его ключевое преимущество — настраиваемая "медлительность": вы можете увеличить количество раундов, чтобы компенсировать рост вычислительной мощности злоумышленников.
В этой статье мы подробно рассмотрим обе библиотеки: установку, основные методы, реальные примеры использования и типичные ошибки. Вы узнаете, как правильно хешировать данные, проверять пароли и интегрировать эти функции в ваши Rust-проекты.
Установка
Для использования sha2 и bcrypt в Rust необходимо добавить их в файл Cargo.toml вашего проекта. Откройте терминал в корневой директории проекта и выполните:
cargo add sha2 bcrypt
Эта команда добавит последние версии библиотек в раздел [dependencies]. Альтернативно, вы можете вручную отредактировать Cargo.toml:
[dependencies]
sha2 = "0.10"
bcrypt = "0.15"
После этого выполните cargo build, чтобы загрузить зависимости и убедиться, что всё установлено корректно.
Быстрый старт: минимальный рабочий пример
Давайте начнём с простого примера: хеширование строки с помощью SHA-256 и проверка пароля с помощью bcrypt.
use sha2::{Sha256, Digest};
use bcrypt::{hash, verify, DEFAULT_COST};
fn main() {
// SHA-256 хеширование
let data = b"Hello, world!";
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
println!("SHA-256 хеш: {:x}", result);
// Bcrypt хеширование пароля
let password = "my_secure_password";
let hashed = hash(password, DEFAULT_COST).unwrap();
println!("Bcrypt хеш: {}", hashed);
// Проверка пароля
let is_valid = verify(password, &hashed).unwrap();
println!("Пароль верен: {}", is_valid);
}
Этот код демонстрирует основные возможности обеих библиотек. Вывод будет содержать 64-символьный hex-хеш SHA-256 и длинную строку bcrypt (начинающуюся с $2b$).
Основные методы и функции
Рассмотрим ключевые функции и методы из sha2 и bcrypt. Для каждой приведём сигнатуру, описание и рабочий пример.
1. sha2::Sha256::new()
Сигнатура: pub fn new() -> Sha256
Описание: Создаёт новый экземпляр хешера SHA-256. Это отправная точка для хеширования данных.
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(b"test data");
let hash = hasher.finalize();
println!("{:x}", hash);
2. sha2::Sha256::update()
Сигнатура: pub fn update(&mut self, data: impl AsRef<[u8]>)
Описание: Добавляет данные к текущему состоянию хешера. Можно вызывать несколько раз для потоковой обработки.
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(b"part1");
hasher.update(b"part2");
let hash = hasher.finalize();
println!("{:x}", hash);
3. sha2::Sha256::finalize()
Сигнатура: pub fn finalize(self) -> GenericArray<u8, U32>
Описание: Завершает процесс хеширования и возвращает итоговый хеш в виде массива байтов (32 байта для SHA-256).
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(b"data");
let hash = hasher.finalize();
// hash - это GenericArray, можно преобразовать в hex
println!("{:x}", hash);
4. sha2::Sha512::new()
Сигнатура: pub fn new() -> Sha512
Описание: Создаёт хешер SHA-512, который возвращает 64-байтовый хеш. Аналогичен Sha256, но с большей длиной.
use sha2::{Sha512, Digest};
let mut hasher = Sha512::new();
hasher.update(b"secure data");
let hash = hasher.finalize();
println!("SHA-512: {:x}", hash);
5. sha2::Sha256::digest() (утилита)
Сигнатура: pub fn digest(data: impl AsRef<[u8]>) -> GenericArray<u8, U32>
Описание: Удобная функция для одноразового хеширования. Создаёт хешер, обновляет его данными и завершает — всё в одном вызове.
use sha2::{Sha256, Digest};
let hash = Sha256::digest(b"quick hash");
println!("{:x}", hash);
6. bcrypt::hash()
Сигнатура: pub fn hash<S: AsRef<[u8]>>(password: S, cost: u32) -> Result<String, BcryptError>
Описание: Хеширует пароль с использованием bcrypt. Параметр cost определяет количество раундов (от 4 до 31). Возвращает строку, содержащую соль и хеш.
use bcrypt::{hash, DEFAULT_COST};
let hashed = hash("my_password", DEFAULT_COST).unwrap();
println!("Хеш: {}", hashed);
7. bcrypt::verify()
Сигнатура: pub fn verify<S: AsRef<[u8]>>(password: S, hashed_password: &str) -> Result<bool, BcryptError>
Описание: Проверяет, соответствует ли пароль указанному хешу bcrypt. Возвращает true или false.
use bcrypt::{hash, verify, DEFAULT_COST};
let password = "secret";
let hashed = hash(password, DEFAULT_COST).unwrap();
let is_valid = verify(password, &hashed).unwrap();
println!("Пароль корректен: {}", is_valid);
8. bcrypt::hash_with_result()
Сигнатура: pub fn hash_with_result<S: AsRef<[u8]>>(password: S, cost: u32) -> Result<BcryptHash, BcryptError>
Описание: Возвращает структуру BcryptHash, содержащую отдельно соль, хеш и параметры. Полезно для кастомной сериализации.
use bcrypt::{hash_with_result, DEFAULT_COST};
let result = hash_with_result("password", DEFAULT_COST).unwrap();
println!("Соль: {:?}", result.salt);
println!("Хеш: {:?}", result.hash);
println!("Параметры: {:?}", result.params);
9. bcrypt::bcrypt() (низкоуровневая)
Сигнатура: pub fn bcrypt(password: &[u8], salt: &[u8], cost: u32) -> Result<Vec<u8>, BcryptError>
Описание: Низкоуровневая функция, выполняющая сам алгоритм bcrypt. Требует явной передачи соли (16 байт). Используется редко, но даёт полный контроль.
use bcrypt::bcrypt;
let password = b"password";
let salt = b"$2b$12$abcdefghijklmnopqrstuv"; // 16 байт после префикса
let cost = 12;
let hash = bcrypt(password, salt, cost).unwrap();
println!("{:?}", hash);
10. bcrypt::format::format_hash()
Сигнатура: pub fn format_hash(salt: &[u8], hash: &[u8], cost: u32) -> String
Описание: Форматирует соль и хеш в стандартную строку bcrypt (например, $2b$12$...). Полезно при ручном формировании хеша.
use bcrypt::format::format_hash;
let salt = b"abcdefghijklmnop"; // 16 байт
let hash = b"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; // 32 байта
let formatted = format_hash(salt, hash, 12);
println!("{}", formatted);
Реальный практический пример
Создадим полноценный пример: простую систему управления пользователями с регистрацией и аутентификацией. Этот код демонстрирует, как использовать sha2 для хеширования email (для быстрого поиска) и bcrypt для безопасного хранения паролей.
use sha2::{Sha256, Digest};
use bcrypt::{hash, verify, DEFAULT_COST};
use std::collections::HashMap;
// Структура пользователя
struct User {
email_hash: String, // SHA-256 хеш email для быстрого поиска
password_hash: String, // Bcrypt хеш пароля
}
// Система управления пользователями
struct UserSystem {
users: HashMap, // ключ - хеш email, значение - пользователь
}
impl UserSystem {
fn new() -> Self {
UserSystem {
users: HashMap::new(),
}
}
// Хеширование email с помощью SHA-256
fn hash_email(email: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(email.as_bytes());
format!("{:x}", hasher.finalize())
}
// Регистрация нового пользователя
fn register(&mut self, email: &str, password: &str) -> Result<(), String> {
let email_hash = Self::hash_email(email);
if self.users.contains_key(&email_hash) {
return Err("Пользователь с таким email уже существует".to_string());
}
// Хешируем пароль с bcrypt
let password_hash = hash(password, DEFAULT_COST)
.map_err(|e| format!("Ошибка хеширования пароля: {}", e))?;
let user = User {
email_hash: email_hash.clone(),
password_hash,
};
self.users.insert(email_hash, user);
Ok(())
}
// Аутентификация пользователя
fn authenticate(&self, email: &str, password: &str) -> Result {
let email_hash = Self::hash_email(email);
let user = self.users.get(&email_hash)
.ok_or_else(|| "Пользователь не найден".to_string())?;
// Проверяем пароль
verify(password, &user.password_hash)
.map_err(|e| format!("Ошибка проверки пароля: {}", e))
}
// Изменение пароля
fn change_password(&mut self, email: &str, old_password: &str, new_password: &str) -> Result<(), String> {
// Сначала аутентифицируем
if !self.authenticate(email, old_password)? {
return Err("Неверный текущий пароль".to_string());
}
let email_hash = Self::hash_email(email);
let user = self.users.get_mut(&email_hash).unwrap();
// Хешируем новый пароль
user.password_hash = hash(new_password, DEFAULT_COST)
.map_err(|e| format!("Ошибка хеширования пароля: {}", e))?;
Ok(())
}
// Удаление пользователя
fn delete_user(&mut self, email: &str, password: &str) -> Result<(), String> {
if !self.authenticate(email, password)? {
return Err("Неверный пароль".to_string());
}
let email_hash = Self::hash_email(email);
self.users.remove(&email_hash);
Ok(())
}
}
fn main() {
let mut system = UserSystem::new();
// Регистрируем пользователей
println!("Регистрация пользователей...");
system.register("alice@example.com", "AlicePass123!").unwrap();
system.register("bob@example.com", "BobPass456!").unwrap();
// Пытаемся зарегистрировать дубликат
match system.register("alice@example.com", "AnotherPass") {
Ok(_) => println!("Успешно (не должно было произойти)"),
Err(e) => println!("Ошибка (ожидаемо): {}", e),
}
// Аутентификация
println!("\nАутентификация:");
let auth_result = system.authenticate("alice@example.com", "AlicePass123!");