A Rust port of the password primitives used in Django Project.
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.
Content of examples/tldr.rs:
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$1000000$pQE1Pfr1CUpS$gDLIrbspb7isluj1zxcItegXxrE1BJP3sdg61S+72rw="
Is valid: true
Add the dependency to your Cargo.toml:
[dependencies]
djangohashers = "^1.8"Reference and import:
extern crate djangohashers;
// Everything (it's not much):
use djangohashers::*;
// Or, just what you need:
use djangohashers::{check_password, make_password, Algorithm};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.
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: the package fastpbkdf2 uses a C-binding of a library that requires OpenSSL. If ring's implementation of PBKDF2 reaches this level of optiomization, the fastpbkdf2 version will be deprecated.
Add the dependency to your Cargo.toml declaring the feature:
[dependencies.djangohashers]
version = "^1.8"
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 of how to install Python’s Cryptography dependencies, that also links against OpenSSL.
On a Apple M4 Pro:
| Method | Encode or Check | Performance |
|---|---|---|
| Django 5.2.0 on Python 3.13.2 | 136ms | 100% (baseline) |
| djangohashers with ring::pbkdf2 (default) | 77ms | 56.6% 🐇 |
| djangohashers with fastpbkdf2 | 49ms | 36.0% 🐇 |
Replicate test above with Docker:
$ docker build -t rs-dj-hashers-profile .
...
$ docker run -t rs-dj-hashers-profile
Hashing time: 136ms (Python 3.13.2, Django 5.2.0).
Hashing time: 77ms (Vanilla PBKDF2).
Hashing time: 49ms (Fast PBKDF2).
DjangoHashers passes all relevant unit tests from Django 1.4 to 5.2 (and betas up to 6.1), there is even a line-by-line translation of 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.
API Documentation, thanks to docs.rs project!
Function signatures:
pub fn check_password(password: &str, encoded: &str) -> Result<bool, HasherError> {}
pub fn check_password_tolerant(password: &str, encoded: &str) -> bool {}Complete version:
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:
if check_password_tolerant(password, encoded) {
// Log the user in.
} else {
// Ask the user to try again.
}Function signatures:
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::PBKDF2SHA1Algorithm::Argon2Algorithm::ScryptAlgorithm::BCryptSHA256Algorithm::BCryptAlgorithm::SHA1Algorithm::MD5Algorithm::UnsaltedSHA1Algorithm::UnsaltedMD5Algorithm::Crypt
The algorithms follow the same Django naming model, minus the PasswordHasher suffix.
Using default settings (PBKDF2 algorithm, random salt):
let encoded = make_password("KRONOS");
// Returns something like:
// pbkdf2_sha256$24000$go9s3b1y1BTe$Pksk4EptJ84KDnI7ciocmhzFAb5lFoFwd6qlPOwwW4Q=Using a defined algorithm (random salt):
let encoded = make_password_with_algorithm("KRONOS", Algorithm::BCryptSHA256);
// Returns something like:
// bcrypt_sha256$$2b$12$e5C3zfswn.CowOBbbb7ngeYbxKzJePCDHwo8AMr/SZeZCoGrk7oueUsing a defined algorithm and salt (not recommended, use it only for debug):
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]*$).
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.
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::CURRENTCurrent Django version (5.2for DjangoHashers1.8.3).DjangoVersion::V1_4Django 1.4DjangoVersion::V1_5Django 1.5DjangoVersion::V1_6Django 1.6DjangoVersion::V1_7Django 1.7DjangoVersion::V1_8Django 1.8DjangoVersion::V1_9Django 1.9DjangoVersion::V1_10Django 1.10DjangoVersion::V1_11Django 1.11DjangoVersion::V2_0Django 2.0DjangoVersion::V2_1Django 2.1DjangoVersion::V2_2Django 2.2DjangoVersion::V3_0Django 3.0DjangoVersion::V3_1Django 3.1DjangoVersion::V3_2Django 3.2DjangoVersion::V4_0Django 4.0DjangoVersion::V4_1Django 4.1DjangoVersion::V4_2Django 4.2DjangoVersion::V5_0Django 5.0DjangoVersion::V5_1Django 5.1DjangoVersion::V5_2Django 5.2DjangoVersion::V6_0Django 6.0DjangoVersion::V6_1Django 6.1
Function signature:
pub fn is_password_usable(encoded: &str) -> bool {}You can check if the password hash is properly formatted before running the expensive cryto stuff:
let encoded = "pbkdf2_sha256$24000$..."; // Fetched from DB.
if is_password_usable(encoded) {
// Go ahead.
} else {
// Check your database or report an issue.
}- 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 in your code.
- Always include some test case.
Rust DjangoHashers is released under the 3-Clause BSD License.
tl;dr: "free to use as long as you credit me".