# Rust DjangoHashers A Rust port of the password primitives used in [Django Project](https://www.djangoproject.com). Django's `django.contrib.auth.models.User` class has a few methods to deal with passwords, like `set_password()` and `check_password()`; **DjangoHashers** implements the primitive functions behind those methods. All Django's built-in hashers are supported. This library was conceived for Django integration, but is not limited to it; you can use the password hash algorithm in any Rust project (or FFI integration), since its security model is already battle-tested. ## TL;DR Content of `examples/tldr.rs`: ```rust extern crate djangohashers; use djangohashers::*; fn main() { let encoded = make_password("K2jitmJ3CBfo"); println!("Hash: {:?}", encoded); let is_valid = check_password("K2jitmJ3CBfo", &encoded).unwrap(); println!("Is valid: {:?}", is_valid); } ``` Output: ``` $ cargo run --quiet --example tldr Hash: "pbkdf2_sha256$390000$7HRd1YJBZvYj$Rc3BW6f7ss3CShWkULiXI9Rxj7CDdstBeoyCgFFQaK0=" Is valid: true ``` ## Installation Add the dependency to your `Cargo.toml`: ```toml [dependencies] djangohashers = "^1.7" ``` Reference and import: ```rust extern crate djangohashers; // Everything (it's not much): use djangohashers::*; // Or, just what you need: use djangohashers::{check_password, make_password, Algorithm}; ``` ## Compiling Features By default all the hashers are enabled, but you can pick only the hashers that you need to avoid unneeded dependencies. * `default`: all hashers. * `with_pbkdf2`: only **PBKDF2** and **PBKDF2SHA1**. * `with_argon2`: only **Argon2**. * `with_scrypt`: only **Scrypt**. * `with_bcrypt`: only **BCrypt** and **BCryptSHA256**. * `with_legacy`: only **SHA1**, **MD5**, **UnsaltedSHA1**, **UnsaltedMD5** and **Crypt**. * `fpbkdf2`: enables **Fast PBKDF2** (requires OpenSSL, see below). * `fuzzy_tests`: only for development, enables fuzzy tests. ## Fast PBKDF2 Version Depending on your platform, OS and version of libraries, it is possible that DjangoHashers can be slower than Python/Django's reference implementation. If performance is critical for your case, there is an [alternatice implementation](https://www.cryptologie.net/article/281/pbkdf2-performance-matters/): the package [fastpbkdf2](https://github.com/ctz/rust-fastpbkdf2) uses a C-binding of a [library](https://github.com/ctz/fastpbkdf2) that requires OpenSSL. If **ring**'s implementation of PBKDF2 reaches this level of optiomization, the **fastpbkdf2** version will be deprecated. ### Installation Add the dependency to your `Cargo.toml` declaring the feature: ```toml [dependencies.djangohashers] version = "^1.7" features = ["fpbkdf2"] ``` You need to install OpenSSL and set the environment variable to make it visible to the compiler; this changes depending on the operation system and package manager, for example, in macOS you may need to do something like this: ``` $ brew install openssl $ export LIBRARY_PATH="$(brew --prefix openssl)/lib" $ export CFLAGS="-I$(brew --prefix openssl)/include" $ cargo ... ``` For other OSs and package managers, [follow the guide](https://cryptography.io/en/latest/installation/) of how to install Python’s **Cryptography** dependencies, that also links against OpenSSL. ### Performance On a Quad-Core Intel Core i7: Method | Encode or Check | Performance ------- | --------------- | ------- Django 4.1.5 on Python 3.11.1 | 189ms | 100% (baseline) djangohashers with ring::pbkdf2 (default) | 145ms | 76.7% πŸ‡ djangohashers with fastpbkdf2 | 119ms | 62.9 πŸ‡ On a Apple M1: Method | Encode or Check | Performance ------- | --------------- | ------- Django 4.1.5 on Python 3.11.1 | 65ms | 100% (baseline) djangohashers with ring::pbkdf2 (default) | 38ms | 58.5% πŸ‡ djangohashers with fastpbkdf2 | 26ms | 40.0% πŸ‡ Replicate test above with Docker: ``` $ docker build -t rs-dj-hashers-profile . ... $ docker run -t rs-dj-hashers-profile Hashing time: 65ms (Python 3.11.1, Django 4.1.5). Hashing time: 38ms (Vanilla PBKDF2). Hashing time: 26ms (Fast PBKDF2). ``` ## Compatibility DjangoHashers passes all relevant unit tests from Django 1.4 to 5.1 (and beta of 5.2), there is even a [line-by-line translation](https://github.com/Racum/rust-djangohashers/blob/master/tests/django.rs) of [tests/auth_tests/test_hashers.py](https://github.com/django/django/blob/e403f22/tests/auth_tests/test_hashers.py). What is **not** covered: * Upgrade/Downgrade callbacks. * Any 3rd-party hasher outside Django's code. * Some tests that makes no sense in idiomatic Rust. ## Usage [API Documentation](https://docs.rs/djangohashers/), thanks to **docs.rs** project! ### Verifying a Hashed Password Function signatures: ```rust pub fn check_password(password: &str, encoded: &str) -> Result {} pub fn check_password_tolerant(password: &str, encoded: &str) -> bool {} ``` Complete version: ```rust let password = "KRONOS"; // Sent by the user. let encoded = "pbkdf2_sha256$24000$..."; // Fetched from DB. match check_password(password, encoded) { Ok(valid) => { if valid { // Log the user in. } else { // Ask the user to try again. } } Err(error) => { // Deal with the error. } } ``` Possible Errors: * `HasherError::UnknownAlgorithm`: anything not recognizable as an algorithm. * `HasherError::BadHash`: Hash string is corrupted. * `HasherError::InvalidIterations`: number of iterations is not a positive integer. * `HasherError::EmptyHash`: hash string is empty. * `HasherError::InvalidArgon2Salt`: Argon2 salt should be Base64 encoded. If you want to automatically assume all errors as *"invalid password"*, there is a shortcut for that: ```rust if check_password_tolerant(password, encoded) { // Log the user in. } else { // Ask the user to try again. } ``` ### Generating a Hashed Password Function signatures: ```rust pub fn make_password(password: &str) -> String {} pub fn make_password_with_algorithm(password: &str, algorithm: Algorithm) -> String {} pub fn make_password_with_settings(password: &str, salt: &str, algorithm: Algorithm) -> String {} ``` Available algorithms: * `Algorithm::PBKDF2` (default) * `Algorithm::PBKDF2SHA1` * `Algorithm::Argon2` * `Algorithm::Scrypt` * `Algorithm::BCryptSHA256` * `Algorithm::BCrypt` * `Algorithm::SHA1` * `Algorithm::MD5` * `Algorithm::UnsaltedSHA1` * `Algorithm::UnsaltedMD5` * `Algorithm::Crypt` The algorithms follow the same Django naming model, minus the `PasswordHasher` suffix. Using default settings (PBKDF2 algorithm, random salt): ```rust let encoded = make_password("KRONOS"); // Returns something like: // pbkdf2_sha256$24000$go9s3b1y1BTe$Pksk4EptJ84KDnI7ciocmhzFAb5lFoFwd6qlPOwwW4Q= ``` Using a defined algorithm (random salt): ```rust let encoded = make_password_with_algorithm("KRONOS", Algorithm::BCryptSHA256); // Returns something like: // bcrypt_sha256$$2b$12$e5C3zfswn.CowOBbbb7ngeYbxKzJePCDHwo8AMr/SZeZCoGrk7oue ``` Using a defined algorithm and salt (not recommended, use it only for debug): ```rust let encoded = make_password_with_settings("KRONOS", "seasalt", Algorithm::PBKDF2SHA1); // Returns exactly this (remember, the salt is fixed!): // pbkdf2_sha1$24000$seasalt$F+kiWNHXbMBcwgxsvSKFCWHnZZ0= ``` **Warning**: `make_password_with_settings` and `make_password_core` will both panic if salt is not only letters and numbers (`^[A-Za-z0-9]*$`). ### Generating a Hashed Password based on a Django version Django versions can have different number of iterations for hashers based on PBKDF2 and BCrypt algorithms; this abstraction makes possible to generate a password with the same number of iterations used in that versions. ```rust use djangohashers::{Django, DjangoVersion}; let django = Django {version: DjangoVersion::V1_8}; // Django 1.8. let encoded = django.make_password("KRONOS"); // Returns something like: // pbkdf2_sha256$20000$u0C1E8jrnAYx$7KIo/fAuBJpswQyL7pTxO06ccrSjGdIe7iSqzdVub1w= // ||||| // ...notice the 20000 iterations, used in Django 1.8. ``` Available versions: * `DjangoVersion::CURRENT` Current Django version (`5.1` for DjangoHashers `1.7.4`). * `DjangoVersion::V1_4` Django 1.4 * `DjangoVersion::V1_5` Django 1.5 * `DjangoVersion::V1_6` Django 1.6 * `DjangoVersion::V1_7` Django 1.7 * `DjangoVersion::V1_8` Django 1.8 * `DjangoVersion::V1_9` Django 1.9 * `DjangoVersion::V1_10` Django 1.10 * `DjangoVersion::V1_11` Django 1.11 * `DjangoVersion::V2_0` Django 2.0 * `DjangoVersion::V2_1` Django 2.1 * `DjangoVersion::V2_2` Django 2.2 * `DjangoVersion::V3_0` Django 3.0 * `DjangoVersion::V3_1` Django 3.1 * `DjangoVersion::V3_2` Django 3.2 * `DjangoVersion::V4_0` Django 4.0 * `DjangoVersion::V4_1` Django 4.1 * `DjangoVersion::V4_2` Django 4.2 * `DjangoVersion::V5_0` Django 5.0 * `DjangoVersion::V5_1` Django 5.1 * `DjangoVersion::V5_2` Django 5.2 ### Verifying a Hash Format (pre-crypto) Function signature: ```rust pub fn is_password_usable(encoded: &str) -> bool {} ``` You can check if the password hash is properly formatted before running the expensive cryto stuff: ```rust let encoded = "pbkdf2_sha256$24000$..."; // Fetched from DB. if is_password_usable(encoded) { // Go ahead. } else { // Check your database or report an issue. } ``` ## Contributing * Be patient with me, I’m new to Rust and this is my first project. * Don't go nuts with your *mad-rust-skillz*, legibility is a priority. * Please use [rustfmt](https://github.com/rust-lang-nursery/rustfmt) in your code. * Always include some test case. ## License Rust DjangoHashers is released under the **3-Clause BSD License**. **tl;dr**: *"free to use as long as you credit me"*.