├── .dockerignore ├── tests ├── django61.rs ├── deny_of_service.rs ├── django14.rs ├── django15.rs ├── django18.rs ├── django20.rs ├── django21.rs ├── django22.rs ├── django30.rs ├── django31.rs ├── django41.rs ├── django42.rs ├── django111.rs ├── django50.rs ├── django51.rs ├── django52.rs ├── django60.rs ├── django16.rs ├── django17.rs ├── django110.rs ├── django32.rs ├── fuzzy_tests.rs ├── django40.rs ├── lib.rs └── django19.rs ├── .gitignore ├── examples ├── tldr.rs ├── simple.rs ├── profile.rs └── profile.py ├── Dockerfile ├── LICENSE ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── src ├── crypto_utils.rs ├── hashers.rs └── lib.rs ├── CHANGELOG.md └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | target -------------------------------------------------------------------------------- /tests/django61.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .DS_Store 4 | 5 | # Compiled files 6 | *.o 7 | *.so 8 | *.rlib 9 | *.dll 10 | -------------------------------------------------------------------------------- /examples/tldr.rs: -------------------------------------------------------------------------------- 1 | use djangohashers::*; 2 | 3 | fn main() { 4 | let encoded = make_password("K2jitmJ3CBfo"); 5 | println!("Hash: {:?}", encoded); 6 | let is_valid = check_password("K2jitmJ3CBfo", &encoded).unwrap(); 7 | println!("Is valid: {:?}", is_valid); 8 | } 9 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | use djangohashers::{check_password, make_password}; 2 | 3 | fn main() { 4 | let encoded = make_password("KRONOS"); 5 | if check_password("KRONOS", &encoded).unwrap() { 6 | println!("Yeap! your password is good!"); 7 | } else { 8 | println!("Maybe another time..."); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/profile.rs: -------------------------------------------------------------------------------- 1 | use djangohashers::make_password; 2 | use std::time::Instant; 3 | 4 | fn main() { 5 | let now = Instant::now(); 6 | for _ in 0..100 { 7 | let _ = make_password("lètmein"); 8 | } 9 | 10 | #[cfg(feature = "fpbkdf2")] 11 | println!( 12 | "Hashing time: {}ms (Fast PBKDF2).", 13 | now.elapsed().as_millis() / 100 14 | ); 15 | 16 | #[cfg(not(feature = "fpbkdf2"))] 17 | println!( 18 | "Hashing time: {}ms (Vanilla PBKDF2).", 19 | now.elapsed().as_millis() / 100 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim AS rust_builder 2 | RUN apt-get update && apt-get -y install libssl-dev && rm -rf /var/lib/apt/lists/* 3 | RUN mkdir /repo && mkdir /repo/bin 4 | ADD . /repo 5 | WORKDIR /repo 6 | RUN cargo build --example profile --release --no-default-features --features "with_pbkdf2" && \ 7 | mv target/release/examples/profile bin/vanilla_profile && \ 8 | cargo build --example profile --release --no-default-features --features "with_pbkdf2 fpbkdf2" && \ 9 | mv target/release/examples/profile bin/fastpbkdf2_profile && \ 10 | rm -rf target/release/examples 11 | 12 | FROM python:slim 13 | RUN mkdir /app 14 | WORKDIR /app 15 | COPY --from=rust_builder /repo/bin/* /app/ 16 | RUN pip install django 17 | ADD examples/profile.py . 18 | CMD python profile.py && ./vanilla_profile && ./fastpbkdf2_profile 19 | -------------------------------------------------------------------------------- /examples/profile.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | from sys import version_info as pyver 3 | 4 | from django import VERSION as djver 5 | from django.conf import settings 6 | from django.contrib.auth.hashers import make_password 7 | 8 | 9 | def test(): 10 | make_password('lètmein', hasher='pbkdf2_sha256') 11 | 12 | 13 | if __name__ == '__main__': 14 | settings.configure() 15 | hash_100_times = timeit.timeit(stmt='test()', 16 | setup='from __main__ import test', 17 | number=100) 18 | hash_average = int(hash_100_times * 10) 19 | python_version = '.'.join(map(str, pyver[:3])) 20 | django_version = '.'.join(map(str, djver[:3])) 21 | version_info = f'(Python {python_version}, Django {django_version})' 22 | print(f'Hashing time: {hash_average}ms {version_info}.') 23 | -------------------------------------------------------------------------------- /tests/deny_of_service.rs: -------------------------------------------------------------------------------- 1 | use djangohashers::*; 2 | 3 | #[test] 4 | #[cfg(feature = "with_pbkdf2")] 5 | fn test_pbkdf2_deny_of_servicez() { 6 | let encoded = format!("pbkdf2_sha256${}$salt$hash", std::u32::MAX); 7 | assert_eq!( 8 | check_password("pass", &encoded), 9 | Err(HasherError::InvalidIterations) 10 | ); 11 | } 12 | 13 | #[test] 14 | #[cfg(feature = "with_pbkdf2")] 15 | fn test_pbkdf2_sha1_deny_of_service() { 16 | let encoded = format!("pbkdf2_sha1${}$salt$hash", std::u32::MAX); 17 | assert_eq!( 18 | check_password("pass", &encoded), 19 | Err(HasherError::InvalidIterations) 20 | ); 21 | } 22 | 23 | #[test] 24 | #[cfg(feature = "with_bcrypt")] 25 | fn test_bcrypt_deny_of_service() { 26 | let encoded = format!("bcrypt$$2b${}$hash", 17); 27 | assert_eq!( 28 | check_password("pass", &encoded), 29 | Err(HasherError::InvalidIterations) 30 | ); 31 | } 32 | 33 | #[test] 34 | #[cfg(feature = "with_bcrypt")] 35 | fn test_bcrypt_sha256_deny_of_service() { 36 | let encoded = format!("bcrypt_sha256$$2b${}$hash", 17); 37 | assert_eq!( 38 | check_password("pass", &encoded), 39 | Err(HasherError::InvalidIterations) 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 3-Clause BSD License 2 | 3 | Copyright (c) 2015, Ronaldo "Racum" Ferreira 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | * Neither the name of Ronaldo "Racum" Ferreira nor the 14 | names of its contributors may be used to endorse or promote products 15 | derived from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL RONALDO "RACUM" FERREIRA BE LIABLE FOR ANY 21 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 24 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: ${{matrix.rust}} ${{matrix.features}} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | rust: 13 | - stable 14 | - nightly 15 | features: 16 | - "" 17 | - --features fpbkdf2 18 | - --features "fpbkdf2 fuzzy_tests" --release 19 | - --no-default-features --features "with_pbkdf2" --release 20 | - --no-default-features --features "with_bcrypt" --release 21 | - --no-default-features --features "with_argon2" --release 22 | - --no-default-features --features "with_scrypt" --release 23 | - --no-default-features --features "with_legacy" --release 24 | - --no-default-features --features "with_legacy with_bcrypt" --release 25 | - --no-default-features --features "with_legacy with_argon2" --release 26 | - --no-default-features --features "with_legacy with_legacy" --release 27 | - --no-default-features --features "with_bcrypt with_argon2" --release 28 | - --no-default-features --features "with_bcrypt with_legacy" --release 29 | - --no-default-features --features "with_argon2 with_legacy" --release 30 | - --no-default-features --features "with_scrypt with_pbkdf2" --release 31 | 32 | steps: 33 | - uses: actions/checkout@v1 34 | 35 | - uses: actions-rs/toolchain@v1 36 | with: 37 | toolchain: ${{matrix.rust}} 38 | override: true 39 | 40 | - name: Build 41 | run: cargo test --verbose --no-fail-fast ${{matrix.features}} 42 | env: 43 | RUST_BACKTRACE: 1 44 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "djangohashers" 3 | version = "1.8.3" 4 | authors = ["Ronaldo Racum "] 5 | documentation = "https://docs.rs/djangohashers/" 6 | license = "BSD-3-Clause" 7 | readme = "README.md" 8 | homepage = "https://github.com/racum/rust-djangohashers" 9 | repository = "https://github.com/racum/rust-djangohashers" 10 | keywords = ["hash", "password", "python", "django", "crypto"] 11 | categories = ["algorithms", "authentication", "cryptography"] 12 | description = "A Rust port of the password primitives used in Django project." 13 | edition = "2024" 14 | 15 | [lib] 16 | doc = true 17 | doctest = false 18 | bench = true 19 | 20 | [profile.dev.package."*"] 21 | opt-level = 1 22 | 23 | [features] 24 | default = ["with_pbkdf2", "with_argon2", "with_scrypt", "with_bcrypt", "with_legacy"] 25 | with_pbkdf2 = ["base64", "constant_time_eq", "ring"] 26 | with_argon2 = ["base64", "constant_time_eq", "rust-argon2"] 27 | with_scrypt = ["base64", "constant_time_eq", "scrypt"] 28 | with_bcrypt = ["bcrypt", "sha2"] 29 | with_legacy = ["pwhash", "constant_time_eq", "md-5", "sha-1", "hex_fmt"] 30 | fpbkdf2 = ["base64", "constant_time_eq", "fastpbkdf2"] 31 | fuzzy_tests = [] 32 | 33 | [dependencies] 34 | rand = "^0.9" 35 | regex = "^1.0" 36 | lazy_static = "^1.0" 37 | md-5 = { version = "^0.10", optional = true } 38 | sha-1 = { version = "^0.10", optional = true } 39 | sha2 = { version = "^0.10", optional = true } 40 | ring = { version = "^0.17", default-features = false, optional = true } 41 | bcrypt = { version = "^0.17", optional = true } 42 | base64 = { version = "^0.22", optional = true } 43 | pwhash = { version = "^1.0", default-features = false, optional = true } 44 | fastpbkdf2 = { version = "^0.1", optional = true } 45 | rust-argon2 = { version = "^3.0", optional = true } 46 | scrypt = { version = "^0.11", optional = true } 47 | constant_time_eq = { version = "^0.4", optional = true } 48 | hex_fmt = { version = "^0.3", optional = true } 49 | 50 | [dev-dependencies] 51 | quickcheck = "^1.0" 52 | -------------------------------------------------------------------------------- /tests/django14.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.4: 2 | //! https://github.com/django/django/blob/2591fb8/django/contrib/auth/tests/hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_4, 12 | }; 13 | let encoded = django.make_password_with_settings("letmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("letmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("letmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_4, 34 | }; 35 | let encoded = django.make_password_with_settings("letmein", "seasalt", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=" 39 | ); 40 | assert_eq!(check_password("letmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_4, 48 | }; 49 | let encoded = django.make_password_with_settings("letmein", "seasalt", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$10000$seasalt$91JiNKgwADC8j2j86Ije/cc4vfQ=" 53 | ); 54 | assert_eq!(check_password("letmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django15.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.5: 2 | //! https://github.com/django/django/blob/b170c07/django/contrib/auth/tests/hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_5, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$10000$seasalt$CWWFdHOWwPnki7HvkcqN9iA2T3KLW1cf2uZ5kvArtVY=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_5, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$10000$seasalt$CWWFdHOWwPnki7HvkcqN9iA2T3KLW1cf2uZ5kvArtVY=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_5, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$10000$seasalt$oAfF6vgs95ncksAhGXOWf4Okq7o=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django18.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.8: 2 | //! https://github.com/django/django/blob/feac4c3/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_8, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$20000$seasalt$oBSd886ysm3AqYun62DOdin8YcfbU1z9cksZSuLP9r0=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_8, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$20000$seasalt2$Flpve/uAcyo6+IFI6YAhjeABGPVbRQjzHDxRhqxewgw=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_8, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$20000$seasalt2$pJt86NmjAweBY1StBvxCu7l1o9o=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django20.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 2.0: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V2_0, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$100000$seasalt$BNZ6eyaNc8qFTJPjrAq99hSYb73EgAdytAtdBg2Sdcc=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V2_0, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$100000$seasalt2$Tl4GMr+Yt1zzO1sbKoUaDBdds5NkR3RxaDWuQsliFrI=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V2_0, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$100000$seasalt2$dK/dL+ySBZ5zoR0+Zk3SB/VsH0U=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django21.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 2.1: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V2_1, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$120000$seasalt$fsgWMpOXin7ZAmi4j+7XjKCZ4JCvxJTGiwwDrawRqSc=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V2_1, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$120000$seasalt2$FRWVLZaxRXtbVIkhYdTQc/tE7JF/s5tU/4O4VhB94ig=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V2_1, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$120000$seasalt2$6kIwMgg3rEEwDAQY/CB9VUVtEiI=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django22.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 2.2: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V2_2, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$150000$seasalt$71l36B3C2UesFoWz5oshQ1SSTtCLnDO5RMysCfljq5o=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V2_2, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$150000$seasalt2$5xGh/XsAm2L9fQXShAI1qf739n97YlTaaLY8/t6Ms7o=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V2_2, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$150000$seasalt2$lIjyT2rG1gVh5rdCmuAEoHwQtQE=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django30.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 3.0: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V3_0, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$180000$seasalt$gH56uAM9k5UGHuCzAYqLtJQ7AFgnXEZ4LMzt71ldHoc=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V3_0, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$180000$seasalt2$42TW7RGTT6FJUY+hv/VNLy7/3F0KbOcvoKmvB6TAnGU=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V3_0, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$180000$seasalt2$y3RFPd5ZY+yJ8pv4soGPYtg2tZo=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django31.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 3.1: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V3_1, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$216000$seasalt$youGZxOw6ZOcfrXv2i8/AhrnpZflJJ9EshS9XmUJTUg=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V3_1, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$216000$seasalt2$gHyszNJ9lwTG5y3MQUjZe+OJmYVTBPl/y7bYq9dtk8M=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V3_1, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$216000$seasalt2$E1KH89wMKuPXrrQzifVcG4cBtiA=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django41.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 4.1: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V4_1, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$390000$seasalt$8xBlGd3jVgvJ+92hWPxi5ww0uuAuAnKgC45eudxro7c=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V4_1, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$390000$seasalt2$geC/uZ92nRXDSjSxeoiBqYyRcrLzMm8xK3ro1QS1uo8=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V4_1, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$390000$seasalt2$aDapRanzW8aHTz97v2TcfHzWD+I=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django42.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 4.2: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V4_2, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$600000$seasalt$OAXyhAQ/4ZDA9V5RMExt3C1OwQdUpLZ99vm1McFlLRA=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V4_2, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$600000$seasalt2$OSllgFdJjYQjb0RfMzrx8u0XYl4Fkt+wKpI1yq4lZlo=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V4_2, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$600000$seasalt2$2CLsaL1MZhq6JOG6QOHtVbiopHE=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django111.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.11: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_11, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$36000$seasalt$mEUPPFJkT/xtwDU8rB7Q+puHRZnR07WRjerTkt/3HI0=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_11, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$36000$seasalt2$QkIBVCvGmTmyjPJ5yox2y/jQB8isvgUNK98FxOU1UYo=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_11, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$36000$seasalt2$GoU+9AubJ/xRkO0WD1Xf3WPxWfE=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django50.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 5.0: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V5_0, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$720000$seasalt$eDupbcisD1UuIiou3hMuMu8oe/XwnpDw45r6AA5iv0E=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V5_0, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$720000$seasalt2$e8hbsPnTo9qWhT3xYfKWoRth0h0J3360yb/tipPhPtY=", 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V5_0, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$720000$seasalt2$2DDbzziqCtfldrRSNAaF8oA9OMw=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django51.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 5.1: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V5_1, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$870000$seasalt$wJSpLMQRQz0Dhj/pFpbyjMj71B2gUYp6HJS5AU+32Ac=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V5_1, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$870000$seasalt2$nxgnNHRsZWSmi4hRSKq2MRigfaRmjDhH1NH4g2sQRbU=", 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V5_1, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$870000$seasalt2$iFPKnrkYfxxyxaeIqxq+c3nJ/j4=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django52.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 5.2: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V5_2, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$1000000$seasalt$r1uLUxoxpP2Ued/qxvmje7UH9PUJBkRrvf9gGPL7Cps=", 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V5_2, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$1000000$seasalt2$egbhFghgsJVDo5Tpg/k9ZnfbySKQ1UQnBYXhR97a7sk=", 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V5_2, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$1000000$seasalt2$3R9hvSAiAy5ARspAFy5GJ/2rjXo=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django60.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 6.0: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V6_0, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$1200000$seasalt$6sTlFi4QohxXLuZigqDIUNX8xG9NxrTmV8+flFQdBqE=", 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V6_0, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$1200000$seasalt2$hPlIUc6GqWsws6cZV1K8OuOARm1UrbZ3vLGFoHkH0ZI=", 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V6_0, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$1200000$seasalt2$RGU4BAy93u+JDPtuMamdllndh+c=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django16.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.8: 2 | //! https://github.com/django/django/blob/d92b085/django/contrib/auth/tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_6, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$12000$seasalt$Ybw8zsFxqja97tY/o6G+Fy1ksY4U/Hw3DRrGED6Up4s=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_6, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$12000$seasalt2$hlDLKsxgkgb1aeOppkM5atCYw5rPzAjCNQZ4NYyUROw=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_6, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$12000$seasalt2$JeMRVfjjgtWw3/HzlnlfqBnQ6CA=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django17.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.7: 2 | //! https://github.com/django/django/blob/d92b085/django/contrib/auth/tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_7, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$12000$seasalt$Ybw8zsFxqja97tY/o6G+Fy1ksY4U/Hw3DRrGED6Up4s=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_7, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$12000$seasalt2$hlDLKsxgkgb1aeOppkM5atCYw5rPzAjCNQZ4NYyUROw=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_7, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$12000$seasalt2$JeMRVfjjgtWw3/HzlnlfqBnQ6CA=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | -------------------------------------------------------------------------------- /tests/django110.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.10: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V1_10, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$30000$seasalt$VrX+V8drCGo68wlvy6rfu8i1d1pfkdeXA4LJkRGJodY=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V1_10, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$30000$seasalt2$a75qzbogeVhNFeMqhdgyyoqGKpIzYUo651sq57RERew=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V1_10, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$30000$seasalt2$pMzU1zNPcydf6wjnJFbiVKwgULc=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | 57 | #[test] 58 | #[cfg(feature = "with_argon2")] 59 | fn test_argon2() { 60 | let django = Django { 61 | version: DjangoVersion::V1_10, 62 | }; 63 | let encoded = django.make_password_with_algorithm("lètmein", Algorithm::Argon2); 64 | assert!(is_password_usable(&encoded)); 65 | assert!(encoded.starts_with("argon2$")); 66 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 67 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 68 | // Blank passwords 69 | let blank_encoded = django.make_password_with_algorithm("", Algorithm::Argon2); 70 | assert!(blank_encoded.starts_with("argon2$")); 71 | assert!(is_password_usable(&blank_encoded)); 72 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 73 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 74 | } 75 | -------------------------------------------------------------------------------- /tests/django32.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 3.2: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V3_2, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$260000$seasalt$YlZ2Vggtqdc61YjArZuoApoBh9JNGYoDRBUGu6tcJQo=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V3_2, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$260000$seasalt2$UCGMhrOoaq1ghQPArIBK5RkI6IZLRxlIwHWA1dMy7y8=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V3_2, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$260000$seasalt2$wAibXvW6jgvatCdONi6SMJ6q7mI=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | 57 | #[test] 58 | #[cfg(feature = "with_argon2")] 59 | fn test_argon2() { 60 | let django = Django { 61 | version: DjangoVersion::V3_2, 62 | }; 63 | let encoded = django.make_password_with_algorithm("lètmein", Algorithm::Argon2); 64 | assert!(is_password_usable(&encoded)); 65 | assert!(encoded.starts_with("argon2$argon2id$")); 66 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 67 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 68 | // Blank passwords 69 | let blank_encoded = django.make_password_with_algorithm("", Algorithm::Argon2); 70 | assert!(blank_encoded.starts_with("argon2$argon2id$")); 71 | assert!(is_password_usable(&blank_encoded)); 72 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 73 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 74 | // Old hashes without version attribute 75 | let encoded = "argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg"; 76 | assert_eq!(check_password("secret", encoded), Ok(true)); 77 | assert_eq!(check_password("wrong", encoded), Ok(false)); 78 | // Old hashes with version attribute. 79 | let encoded = "argon2$argon2i$v=19$m=8,t=1,p=1$c2FsdHNhbHQ$YC9+jJCrQhs5R6db7LlN8Q"; 80 | assert_eq!(check_password("secret", encoded), Ok(true)); 81 | assert_eq!(check_password("wrong", encoded), Ok(false)); 82 | } 83 | -------------------------------------------------------------------------------- /tests/fuzzy_tests.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "fuzzy_tests")] 2 | mod fuzzy_tests { 3 | use djangohashers::*; 4 | use quickcheck::{TestResult, quickcheck}; 5 | 6 | #[cfg(feature = "with_argon2")] 7 | use base64::engine::Engine as _; 8 | #[cfg(feature = "with_argon2")] 9 | use base64::engine::general_purpose; 10 | 11 | fn check_algorithm(pwd: String, salt: String, algorithm: Algorithm) -> TestResult { 12 | if !VALID_SALT_RE.is_match(&salt) { 13 | return TestResult::discard(); 14 | } 15 | 16 | TestResult::from_bool(check_password_tolerant( 17 | &pwd, 18 | &make_password_with_settings(&pwd, &salt, algorithm), 19 | )) 20 | } 21 | 22 | #[cfg(feature = "with_pbkdf2")] 23 | quickcheck! { 24 | fn test_fuzzy_pbkdf2(pwd: String, salt: String) -> TestResult { 25 | check_algorithm(pwd, salt, Algorithm::PBKDF2) 26 | } 27 | } 28 | 29 | #[cfg(feature = "with_pbkdf2")] 30 | quickcheck! { 31 | fn test_fuzzy_pbkdf2sha1(pwd: String, salt: String) -> TestResult { 32 | check_algorithm(pwd, salt, Algorithm::PBKDF2SHA1) 33 | } 34 | } 35 | 36 | #[cfg(feature = "with_argon2")] 37 | quickcheck! { 38 | fn test_fuzzy_argon2(pwd: String, salt: String) -> TestResult { 39 | if salt.len() < 8 { 40 | return TestResult::discard(); 41 | } 42 | check_algorithm(pwd, general_purpose::URL_SAFE_NO_PAD.encode(salt.as_bytes()), Algorithm::Argon2) 43 | } 44 | } 45 | 46 | #[cfg(feature = "with_bcrypt")] 47 | quickcheck! { 48 | fn test_fuzzy_bcryptsha256(pwd: String, salt: String) -> TestResult { 49 | check_algorithm(pwd, salt, Algorithm::BCryptSHA256) 50 | } 51 | } 52 | 53 | #[cfg(feature = "with_bcrypt")] 54 | quickcheck! { 55 | fn test_fuzzy_bcrypt(pwd: String, salt: String) -> TestResult { 56 | if pwd.contains('\0') || pwd.len() >= 72 { 57 | return TestResult::discard(); 58 | } 59 | 60 | check_algorithm(pwd, salt, Algorithm::BCrypt) 61 | } 62 | } 63 | 64 | #[cfg(feature = "with_legacy")] 65 | quickcheck! { 66 | fn test_fuzzy_sha1(pwd: String, salt: String) -> TestResult { 67 | check_algorithm(pwd, salt, Algorithm::SHA1) 68 | } 69 | } 70 | 71 | #[cfg(feature = "with_legacy")] 72 | quickcheck! { 73 | fn test_fuzzy_md5(pwd: String, salt: String) -> TestResult { 74 | check_algorithm(pwd, salt, Algorithm::MD5) 75 | } 76 | } 77 | 78 | #[cfg(feature = "with_legacy")] 79 | quickcheck! { 80 | fn test_fuzzy_unsaltedsha1(pwd: String, salt: String) -> TestResult { 81 | check_algorithm(pwd, salt, Algorithm::UnsaltedSHA1) 82 | } 83 | } 84 | 85 | #[cfg(feature = "with_legacy")] 86 | quickcheck! { 87 | fn test_fuzzy_unsaltedmd5(pwd: String, salt: String) -> TestResult { 88 | check_algorithm(pwd, salt, Algorithm::UnsaltedMD5) 89 | } 90 | } 91 | 92 | #[cfg(feature = "with_legacy")] 93 | quickcheck! { 94 | fn test_fuzzy_crypt(pwd: String, salt: String) -> TestResult { 95 | check_algorithm(pwd, salt, Algorithm::Crypt) 96 | } 97 | } 98 | 99 | #[cfg(feature = "with_scrypt")] 100 | quickcheck! { 101 | fn test_fuzzy_scrypt(pwd: String, salt: String) -> TestResult { 102 | check_algorithm(pwd, salt, Algorithm::Scrypt) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/django40.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 4.0: 2 | //! https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 3 | //! ...but only for the tests where the iterations differ from Django 1.9. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_pbkdf2() { 10 | let django = Django { 11 | version: DjangoVersion::V4_0, 12 | }; 13 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 14 | assert_eq!( 15 | encoded, 16 | "pbkdf2_sha256$320000$seasalt$Toj2II2rBvFiGQcPmUml1Nlni2UtvyRWwz/jz4q6q/4=" 17 | ); 18 | assert!(is_password_usable(&encoded)); 19 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 20 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 21 | // Blank passwords 22 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 23 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 24 | assert!(is_password_usable(&blank_encoded)); 25 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 26 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 27 | } 28 | 29 | #[test] 30 | #[cfg(feature = "with_pbkdf2")] 31 | fn test_low_level_pbkdf2() { 32 | let django = Django { 33 | version: DjangoVersion::V4_0, 34 | }; 35 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 36 | assert_eq!( 37 | encoded, 38 | "pbkdf2_sha256$320000$seasalt2$BRr4pYNIQDsLFP+u4dzjs7pFuWJEin4lFMMoO9wBYvo=" 39 | ); 40 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 41 | } 42 | 43 | #[test] 44 | #[cfg(feature = "with_pbkdf2")] 45 | fn test_low_level_pbkdf2_sha1() { 46 | let django = Django { 47 | version: DjangoVersion::V4_0, 48 | }; 49 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 50 | assert_eq!( 51 | encoded, 52 | "pbkdf2_sha1$320000$seasalt2$sDOkTvzV93jPWTRVxFGh50Jefo0=" 53 | ); 54 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 55 | } 56 | 57 | #[test] 58 | #[cfg(feature = "with_argon2")] 59 | fn test_argon2() { 60 | let django = Django { 61 | version: DjangoVersion::V4_0, 62 | }; 63 | let encoded = django.make_password_with_algorithm("lètmein", Algorithm::Argon2); 64 | assert!(is_password_usable(&encoded)); 65 | assert!(encoded.starts_with("argon2$")); 66 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 67 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 68 | // Blank passwords 69 | let blank_encoded = django.make_password_with_algorithm("", Algorithm::Argon2); 70 | assert!(blank_encoded.starts_with("argon2$")); 71 | assert!(is_password_usable(&blank_encoded)); 72 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 73 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 74 | } 75 | 76 | #[test] 77 | #[cfg(feature = "with_scrypt")] 78 | fn test_scrypt() { 79 | let django = Django { 80 | version: DjangoVersion::V4_0, 81 | }; 82 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::Scrypt); 83 | assert_eq!( 84 | encoded, 85 | "scrypt$16384$seasalt$8$1$Qj3+9PPyRjSJIebHnG81TMjsqtaIGxNQG/aEB/NYafTJ7tibgfYz71m0ldQESkXFRkdVCBhhY8mx7rQwite/Pw==" 86 | ); 87 | assert!(is_password_usable(&encoded)); 88 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 89 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 90 | // Blank passwords 91 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::Scrypt); 92 | assert!(blank_encoded.starts_with("scrypt$")); 93 | assert!(is_password_usable(&blank_encoded)); 94 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 95 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 96 | } 97 | -------------------------------------------------------------------------------- /src/crypto_utils.rs: -------------------------------------------------------------------------------- 1 | //! Set of cryptographic functions to simplify the Hashers. 2 | 3 | #[cfg(any( 4 | feature = "with_pbkdf2", 5 | feature = "with_argon2", 6 | feature = "with_scrypt" 7 | ))] 8 | use base64::engine::Engine as _; 9 | #[cfg(any( 10 | feature = "with_pbkdf2", 11 | feature = "with_argon2", 12 | feature = "with_scrypt" 13 | ))] 14 | use base64::engine::general_purpose; 15 | 16 | #[cfg(any( 17 | feature = "with_pbkdf2", 18 | feature = "with_argon2", 19 | feature = "with_legacy", 20 | feature = "with_scrypt" 21 | ))] 22 | pub fn safe_eq(a: &str, b: String) -> bool { 23 | constant_time_eq::constant_time_eq(a.as_bytes(), b.as_bytes()) 24 | } 25 | 26 | #[cfg(feature = "with_argon2")] 27 | use argon2::{self, Config, ThreadMode, Variant, Version}; 28 | 29 | #[cfg(all(feature = "with_pbkdf2", not(feature = "fpbkdf2")))] 30 | pub fn hash_pbkdf2_sha256(password: &str, salt: &str, iterations: u32) -> String { 31 | let mut result = [0u8; 32]; 32 | use core::num::NonZeroU32; 33 | ring::pbkdf2::derive( 34 | ring::pbkdf2::PBKDF2_HMAC_SHA256, 35 | NonZeroU32::new(iterations).unwrap(), 36 | salt.as_bytes(), 37 | password.as_bytes(), 38 | &mut result, 39 | ); 40 | general_purpose::STANDARD.encode(result) 41 | } 42 | 43 | #[cfg(feature = "with_pbkdf2")] 44 | #[cfg(feature = "fpbkdf2")] 45 | pub fn hash_pbkdf2_sha256(password: &str, salt: &str, iterations: u32) -> String { 46 | let mut result = [0u8; 32]; 47 | fastpbkdf2::pbkdf2_hmac_sha256( 48 | &password.as_bytes(), 49 | &salt.as_bytes(), 50 | iterations, 51 | &mut result, 52 | ); 53 | general_purpose::STANDARD.encode(&result) 54 | } 55 | 56 | #[cfg(feature = "with_pbkdf2")] 57 | #[cfg(not(feature = "fpbkdf2"))] 58 | pub fn hash_pbkdf2_sha1(password: &str, salt: &str, iterations: u32) -> String { 59 | let mut result = [0u8; 20]; 60 | use core::num::NonZeroU32; 61 | ring::pbkdf2::derive( 62 | ring::pbkdf2::PBKDF2_HMAC_SHA1, 63 | NonZeroU32::new(iterations).unwrap(), 64 | salt.as_bytes(), 65 | password.as_bytes(), 66 | &mut result, 67 | ); 68 | general_purpose::STANDARD.encode(result) 69 | } 70 | 71 | #[cfg(feature = "with_pbkdf2")] 72 | #[cfg(feature = "fpbkdf2")] 73 | pub fn hash_pbkdf2_sha1(password: &str, salt: &str, iterations: u32) -> String { 74 | let mut result = [0u8; 20]; 75 | fastpbkdf2::pbkdf2_hmac_sha1( 76 | &password.as_bytes(), 77 | &salt.as_bytes(), 78 | iterations, 79 | &mut result, 80 | ); 81 | general_purpose::STANDARD.encode(&result) 82 | } 83 | 84 | #[cfg(feature = "with_legacy")] 85 | pub fn hash_sha1(password: &str, salt: &str) -> String { 86 | use hex_fmt::HexFmt; 87 | use sha1::{Digest, Sha1}; 88 | let mut hasher = Sha1::new(); 89 | hasher.update(salt); 90 | hasher.update(password); 91 | let result = hasher.finalize(); 92 | format!("{}", HexFmt(&result[..])) 93 | } 94 | 95 | #[cfg(feature = "with_bcrypt")] 96 | pub fn hash_sha256(password: &str) -> String { 97 | use sha2::{Digest, Sha256}; 98 | format!("{:x}", Sha256::digest(password.as_bytes())) 99 | } 100 | 101 | #[cfg(feature = "with_legacy")] 102 | pub fn hash_md5(password: &str, salt: &str) -> String { 103 | use hex_fmt::HexFmt; 104 | use md5::{Digest, Md5}; 105 | let mut hasher = Md5::new(); 106 | hasher.update(salt); 107 | hasher.update(password); 108 | let result = hasher.finalize(); 109 | format!("{}", HexFmt(&result[..])) 110 | } 111 | 112 | #[cfg(feature = "with_legacy")] 113 | pub fn hash_unix_crypt(password: &str, salt: &str) -> String { 114 | #[allow(deprecated)] 115 | pwhash::unix_crypt::hash_with(salt, password).unwrap_or_default() 116 | } 117 | 118 | #[cfg(feature = "with_argon2")] 119 | pub fn hash_argon2( 120 | password: &str, 121 | salt: &str, 122 | time_cost: u32, 123 | memory_cost: u32, 124 | parallelism: u32, 125 | version: Version, 126 | hash_length: u32, 127 | ) -> String { 128 | let config = Config { 129 | variant: Variant::Argon2i, 130 | version, 131 | mem_cost: memory_cost, 132 | time_cost, 133 | lanes: parallelism, 134 | thread_mode: ThreadMode::Parallel, 135 | secret: &[], 136 | ad: &[], 137 | hash_length, 138 | }; 139 | let salt_bytes = general_purpose::URL_SAFE_NO_PAD.decode(salt).unwrap(); 140 | let result = argon2::hash_raw(password.as_bytes(), &salt_bytes, &config).unwrap(); 141 | general_purpose::URL_SAFE_NO_PAD.encode(result) 142 | } 143 | 144 | #[cfg(feature = "with_scrypt")] 145 | use scrypt::{Params, scrypt}; 146 | 147 | #[cfg(feature = "with_scrypt")] 148 | pub fn hash_scrypt( 149 | password: &str, 150 | salt: &str, 151 | work_factor: u8, 152 | block_size: u32, 153 | parallelism: u32, 154 | ) -> String { 155 | const KEY_SIZE: usize = 64; 156 | let mut buf = [0u8; KEY_SIZE]; 157 | let params = Params::new(work_factor, block_size, parallelism, KEY_SIZE).unwrap(); 158 | scrypt(password.as_bytes(), salt.as_bytes(), ¶ms, &mut buf).unwrap(); 159 | general_purpose::STANDARD.encode(buf) 160 | } 161 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | use djangohashers::*; 2 | 3 | static PASSWORD: &str = "ExjGmyUT73bFoT"; 4 | static SALT: &str = "KQ8zeK6wKRuR"; 5 | 6 | #[test] 7 | #[cfg(feature = "with_pbkdf2")] 8 | fn test_pbkdf2_sha256() { 9 | let encoded = make_password_core(PASSWORD, SALT, Algorithm::PBKDF2, DjangoVersion::V1_9); 10 | assert_eq!( 11 | encoded, 12 | "pbkdf2_sha256$24000$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY=" 13 | ); 14 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 15 | } 16 | 17 | #[test] 18 | #[cfg(feature = "with_pbkdf2")] 19 | fn test_pbkdf2_sha256_bad_hash() { 20 | assert!(is_password_usable("pbkdf2_sha256$")); 21 | assert_eq!( 22 | check_password(PASSWORD, "pbkdf2_sha256$"), 23 | Err(HasherError::InvalidIterations) 24 | ); 25 | } 26 | 27 | #[test] 28 | #[cfg(feature = "with_pbkdf2")] 29 | fn test_pbkdf2_sha1() { 30 | let encoded = make_password_core(PASSWORD, SALT, Algorithm::PBKDF2SHA1, DjangoVersion::V1_9); 31 | assert_eq!( 32 | encoded, 33 | "pbkdf2_sha1$24000$KQ8zeK6wKRuR$tSJh4xdxfMJotlxfkCGjTFpGYZU=" 34 | ); 35 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 36 | } 37 | 38 | #[test] 39 | #[cfg(feature = "with_pbkdf2")] 40 | fn test_pbkdf2_sha1_bad_hash() { 41 | assert!(is_password_usable("pbkdf2_sha1$")); 42 | assert_eq!( 43 | check_password(PASSWORD, "pbkdf2_sha1$"), 44 | Err(HasherError::InvalidIterations) 45 | ); 46 | } 47 | 48 | #[test] 49 | #[cfg(feature = "with_legacy")] 50 | fn test_sha1() { 51 | let encoded = make_password_core(PASSWORD, SALT, Algorithm::SHA1, DjangoVersion::V1_9); 52 | assert_eq!( 53 | encoded, 54 | "sha1$KQ8zeK6wKRuR$f83371bca01fa6089456e673ccfb17f42d810b00" 55 | ); 56 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 57 | } 58 | 59 | #[test] 60 | #[cfg(feature = "with_legacy")] 61 | fn test_sha1_bad_hash() { 62 | assert!(is_password_usable("sha1$")); 63 | assert_eq!(check_password(PASSWORD, "sha1$"), Err(HasherError::BadHash)); 64 | } 65 | 66 | #[test] 67 | #[cfg(feature = "with_legacy")] 68 | fn test_md5() { 69 | let encoded = make_password_core(PASSWORD, SALT, Algorithm::MD5, DjangoVersion::V1_9); 70 | assert_eq!(encoded, "md5$KQ8zeK6wKRuR$0137e4d74cb2d9ed9cb1a5f391f6175e"); 71 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 72 | } 73 | 74 | #[test] 75 | #[cfg(feature = "with_legacy")] 76 | fn test_md5_bad_hash() { 77 | assert!(is_password_usable("md5$")); 78 | assert_eq!(check_password(PASSWORD, "md5$"), Err(HasherError::BadHash)); 79 | } 80 | 81 | #[test] 82 | #[cfg(feature = "with_legacy")] 83 | fn test_unsalted_md5() { 84 | let encoded = make_password_core(PASSWORD, "", Algorithm::UnsaltedMD5, DjangoVersion::V1_9); 85 | assert_eq!(encoded, "7cf6409a82cd4c8b96a9ecf6ad679119"); 86 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 87 | } 88 | 89 | #[test] 90 | #[cfg(feature = "with_legacy")] 91 | fn test_unsalted_sha1() { 92 | let encoded = make_password_core(PASSWORD, "", Algorithm::UnsaltedSHA1, DjangoVersion::V1_9); 93 | assert_eq!(encoded, "sha1$$22e6217f026c7a395f0840c1ffbdb163072419e7"); 94 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 95 | } 96 | 97 | #[test] 98 | #[cfg(feature = "with_bcrypt")] 99 | fn test_bcrypt_sha256() { 100 | let encoded = make_password_core(PASSWORD, "", Algorithm::BCryptSHA256, DjangoVersion::V1_9); 101 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 102 | let h = "bcrypt_sha256$$2b$12$LZSJchsWG/DrBy1erNs4eeYo6tZNlLFQmONdxN9HPesa1EyXVcTXK"; 103 | assert_eq!(check_password(PASSWORD, h), Ok(true)); 104 | } 105 | 106 | #[test] 107 | #[cfg(feature = "with_bcrypt")] 108 | fn test_bcrypt() { 109 | let encoded = make_password_core(PASSWORD, "", Algorithm::BCrypt, DjangoVersion::V1_9); 110 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 111 | let h = "bcrypt$$2b$12$LZSJchsWG/DrBy1erNs4ee31eJ7DaWiuwhDOC7aqIyqGGggfu6Y/."; 112 | assert_eq!(check_password(PASSWORD, h), Ok(true)); 113 | } 114 | 115 | #[test] 116 | #[cfg(feature = "with_legacy")] 117 | fn test_crypt() { 118 | let encoded = make_password_core(PASSWORD, SALT, Algorithm::Crypt, DjangoVersion::V1_9); 119 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 120 | let h = "crypt$$KQW3RFkgPSuuA"; 121 | assert_eq!(check_password(PASSWORD, h), Ok(true)); 122 | } 123 | 124 | #[test] 125 | #[cfg(feature = "with_legacy")] 126 | fn test_crypt_bad_hash() { 127 | assert!(is_password_usable("crypt$")); 128 | assert_eq!( 129 | check_password(PASSWORD, "crypt$"), 130 | Err(HasherError::BadHash) 131 | ); 132 | } 133 | 134 | #[test] 135 | #[cfg(feature = "with_argon2")] 136 | fn test_argon2() { 137 | let encoded = make_password_core(PASSWORD, SALT, Algorithm::Argon2, DjangoVersion::V1_10); 138 | assert_eq!(check_password(PASSWORD, &encoded), Ok(true)); 139 | let h = "argon2$argon2i$v=19$m=512,t=2,p=2$S1E4emVLNndLUnVS$RUET3AC8iXvcVPD2TRjvVQ"; 140 | assert_eq!(check_password(PASSWORD, h), Ok(true)); 141 | } 142 | 143 | #[test] 144 | #[cfg(feature = "with_argon2")] 145 | fn test_argon2_old() { 146 | // From https://github.com/django/django/blob/master/tests/auth_tests/test_hashers.py 147 | let old_from_django = "argon2$argon2i$m=8,t=1,p=1$c29tZXNhbHQ$gwQOXSNhxiOxPOA0+PY10P9QFO4NAYysnqRt1GSQLE55m+2GYDt9FEjPMHhP2Cuf0nOEXXMocVrsJAtNSsKyfg"; 148 | assert_eq!(check_password("secret", old_from_django), Ok(true)); 149 | assert_eq!(check_password("wrong", old_from_django), Ok(false)); 150 | // From https://github.com/hynek/argon2_cffi/blob/master/tests/test_low_level.py 151 | // ...prefixed with "argon2$", emulating Django's format: 152 | let old_from_argon2_cffi = "argon2$argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY"; 153 | assert_eq!(check_password("password", old_from_argon2_cffi), Ok(true)); 154 | assert_eq!(check_password("wrong", old_from_argon2_cffi), Ok(false)); 155 | } 156 | 157 | #[test] 158 | #[cfg(feature = "with_argon2")] 159 | fn test_argon2_bad_hash() { 160 | assert!(is_password_usable("argon2$")); 161 | assert_eq!( 162 | check_password(PASSWORD, "argon2$"), 163 | Err(HasherError::BadHash) 164 | ); 165 | } 166 | 167 | #[test] 168 | fn test_is_password_usable() { 169 | // Good hashes: 170 | #[cfg(feature = "with_pbkdf2")] 171 | assert!(is_password_usable( 172 | "pbkdf2_sha1$24000$KQ8zeK6wKRuR$tSJh4xdxfMJotlxfkCGjTFpGYZU=" 173 | )); 174 | #[cfg(feature = "with_legacy")] 175 | assert!(is_password_usable("7cf6409a82cd4c8b96a9ecf6ad679119")); 176 | // Bad hashes: 177 | assert!(!is_password_usable("")); 178 | assert!(!is_password_usable("password")); 179 | assert!(!is_password_usable("!cf6409a82cd4c8b96a9ecf6ad679119")); 180 | } 181 | 182 | #[test] 183 | fn test_check_password_tolerant() { 184 | let negative = "pbkdf2_sha256$-24000$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY="; 185 | assert!(!check_password_tolerant(PASSWORD, negative)); 186 | let nan = "pbkdf2_sha256$NaN$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY="; 187 | assert!(!check_password_tolerant(PASSWORD, nan)); 188 | let rot13 = "rot13$1$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY="; 189 | assert!(!check_password_tolerant(PASSWORD, rot13)); 190 | assert!(!check_password_tolerant(PASSWORD, "")); 191 | } 192 | 193 | #[test] 194 | #[cfg(feature = "with_pbkdf2")] 195 | fn test_errors() { 196 | let negative = "pbkdf2_sha256$-24000$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY="; 197 | assert!(check_password(PASSWORD, negative) == Err(HasherError::InvalidIterations)); 198 | let nan = "pbkdf2_sha256$NaN$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY="; 199 | assert!(check_password(PASSWORD, nan) == Err(HasherError::InvalidIterations)); 200 | let rot13 = "rot13$1$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY="; 201 | assert!(check_password(PASSWORD, rot13) == Err(HasherError::UnknownAlgorithm)); 202 | assert!(check_password(PASSWORD, "") == Err(HasherError::EmptyHash)); 203 | } 204 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [1.8.3] - 2025-09-25 7 | 8 | ### Added 9 | 10 | - Support to Django 6.1. 11 | 12 | ### Changed 13 | 14 | - Renamed Django 5.3 to 6.0. 15 | 16 | ## [1.8.2] - 2025-08-10 17 | 18 | ### Changed 19 | 20 | - Updated rust-argon2 dependency. 21 | 22 | ## [1.8.1] - 2024-04-04 23 | 24 | ### Added 25 | 26 | - Support to Django 5.3. 27 | 28 | ### Changed 29 | 30 | - Set default Django version to 5.2. 31 | 32 | ## [1.8.0] - 2025-03-26 33 | 34 | ### Changed 35 | 36 | - Migrated to Rust Edition 2024. 37 | - Updated rand, bcrypt and constant_time_eq dependencies. 38 | 39 | ## [1.7.5] - 2025-01-05 40 | 41 | ### Changed 42 | 43 | - Updated bcrypt dependency. 44 | 45 | ## [1.7.4] - 2024-10-09 46 | 47 | ### Added 48 | 49 | - Support to Django 5.2. 50 | 51 | ### Changed 52 | 53 | - Set default Django version to 5.1. 54 | 55 | ## [1.7.3] - 2024-03-12 56 | 57 | ### Changed 58 | 59 | - Updated base64 dependency. 60 | 61 | ## [1.7.2] - 2023-12-04 62 | 63 | ### Added 64 | 65 | - Support to Django 5.1. 66 | 67 | ### Changed 68 | 69 | - Set default Django version to 5.0. 70 | 71 | ## [1.7.1] - 2023-10-03 72 | 73 | ### Changed 74 | 75 | - Updated ring dependency. 76 | 77 | ## [1.7.0] - 2023-08-18 78 | 79 | ### Changed 80 | 81 | - Migrated to Rust Edition 2021. 82 | 83 | ## [1.6.9] - 2023-08-18 84 | 85 | ### Changed 86 | 87 | - Updated rust-argon2 dependency. 88 | 89 | ## [1.6.8] - 2023-07-06 90 | 91 | ### Changed 92 | 93 | - Updated bcrypt dependency. 94 | 95 | ## [1.6.7] - 2023-06-21 96 | 97 | ### Changed 98 | 99 | - Updated constant_time_eq dependency. 100 | 101 | ## [1.6.6] - 2023-04-22 102 | 103 | ### Changed 104 | 105 | - No changes, just applied clippy fixes. 106 | 107 | ## [1.6.5] - 2023-04-03 108 | 109 | ### Changed 110 | 111 | - Set default Django version to 4.2. 112 | 113 | ## [1.6.3] - 2023-03-09 114 | 115 | ### Changed 116 | 117 | - Updated scrypt dependency. 118 | 119 | ## [1.6.2] - 2023-02-10 120 | 121 | ### Changed 122 | 123 | - Fix README. 124 | 125 | ## [1.6.1] - 2023-02-08 126 | 127 | ### Added 128 | 129 | - Support to Django 5.0 130 | 131 | ### Changed 132 | 133 | - Updated PBKDF2 iterations to 600000 for Django 4.2. 134 | - Updated bcrypt dependency. 135 | 136 | ## [1.6.0] - 2023-01-13 137 | 138 | ### Added 139 | 140 | - Support to Django 4.2. 141 | 142 | ### Changed 143 | 144 | - Set default Django version to 4.1. 145 | 146 | ## [1.5.10] - 2023-01-12 147 | 148 | ### Changed 149 | 150 | - Updated base64 dependency. 151 | 152 | ## [1.5.9] - 2022-08-21 153 | 154 | ### Changed 155 | 156 | - Compile requirements with opt-level = 1. 157 | 158 | ## [1.5.8] - 2022-05-29 159 | 160 | ### Changed 161 | 162 | - Updated bcrypt dependency. 163 | 164 | ## [1.5.7] - 2022-03-20 165 | 166 | ### Changed 167 | 168 | - Updated dependencies. 169 | 170 | ## [1.5.6] - 2022-02-28 171 | 172 | ### Changed 173 | 174 | - Updated bcrypt dependency. 175 | 176 | ## [1.5.5] - 2022-02-23 177 | 178 | ### Changed 179 | 180 | - Updated bcrypt dependency. 181 | 182 | ## [1.5.4] - 2022-02-18 183 | 184 | ### Changed 185 | 186 | - Updated scrypt dependency. 187 | 188 | ## [1.5.3] - 2022-01-08 189 | 190 | ### Changed 191 | 192 | - Updated rust-argon2 dependency. 193 | 194 | ## [1.5.2] - 2022-01-05 195 | 196 | ### Changed 197 | 198 | - Argon2 hasher now encodes as Argon2id variant. 199 | 200 | ## [1.5.1] - 2021-12-07 201 | 202 | ### Changed 203 | 204 | - Fixed PREFERRED_ALGORITHM resolution. 205 | 206 | ## [1.5.0] - 2021-12-07 207 | 208 | ### Added 209 | 210 | - Support to ScryptHasher (added on Django 4.0). 211 | - Support to Django 4.1. 212 | 213 | ### Changed 214 | 215 | - Set default Django version to 4.0. 216 | - Updated dependencies. 217 | 218 | ## [1.4.3] - 2021-06-18 219 | 220 | ### Changed 221 | 222 | - Updated bcrypt dependency. 223 | 224 | ## [1.4.2] - 2021-06-15 225 | 226 | ### Changed 227 | 228 | - Cleaner code (thank's @andy128k). 229 | - Build via GitHub CI (thank's @andy128k). 230 | 231 | ## [1.4.1] - 2021-04-07 232 | 233 | ### Changed 234 | 235 | - Set default Django version to 3.1. 236 | 237 | ### Added 238 | 239 | - Support to Django 4.0. 240 | 241 | ## [1.4.0] - 2021-01-10 242 | 243 | ### Changed 244 | 245 | - Changed pbkdf2 crate to ring for PBKDF2 algorithms. 246 | - Updated dependencies. 247 | 248 | ## [1.3.2] - 2021-01-02 249 | 250 | ### Changed 251 | 252 | - Updated dependencies. 253 | - Fix compatibility with rand 0.8. 254 | 255 | ## [1.3.1] - 2020-09-13 256 | 257 | ### Changed 258 | 259 | - Set default Django version to 3.1. 260 | - Updated dependencies. 261 | 262 | ### Added 263 | 264 | - Support to Django 3.2. 265 | 266 | ## [1.3.0] - 2020-06-20 267 | 268 | ### Changed 269 | 270 | - Pure-Rust implementation of Argon2 (cargon -> rust-argon2). 271 | - Updated dependencies. 272 | 273 | ### Added 274 | 275 | - Support for ARM 64-bit CPUs. 276 | 277 | ## [1.2.1] - 2020-06-06 278 | 279 | ### Changed 280 | 281 | - Updated dependencies. 282 | 283 | ## [1.2.0] - 2020-02-19 284 | 285 | ### Added 286 | 287 | - Support to Django 3.1. 288 | 289 | ### Changed 290 | 291 | - Cleaner code (thank's @andy128k). 292 | - Set default Django version to 3.0. 293 | - Updated dependencies. 294 | 295 | ## [1.1.1] - 2019-08-21 296 | 297 | ### Added 298 | 299 | - Speed comparison with Django via Docker. 300 | 301 | ## [1.1.0] - 2019-08-16 302 | 303 | ### Added 304 | 305 | - Support to Django 3.0. 306 | - Support to Django 2.2. 307 | 308 | ### Changed 309 | 310 | - Protection against Denial-of-Service for high iterations. 311 | - Set default Django version to 2.2. 312 | - Updated dependencies. 313 | 314 | ## [1.0.1] - 2019-01-27 315 | 316 | ### Changed 317 | 318 | - Ignored null-character password fuzzing for BCrypt (thank's @andy128k). 319 | 320 | ## [1.0.0] - 2019-01-19 321 | 322 | ### Changed 323 | 324 | - Update to Rust 2018 edition (thank's @andy128k). 325 | - Switch to RustCrypto implementations (thank's @andy128k). 326 | - Added error case for HasherError::BadHash (thank's @andy128k). 327 | - Updated dependencies. 328 | 329 | ## [0.3.2] - 2018-11-17 330 | 331 | ### Added 332 | 333 | - Support to Django 2.1. 334 | - Support to Django 2.2. 335 | 336 | ### Changed 337 | 338 | - Set default Django version to 2.1. 339 | - Updated dependencies. 340 | 341 | ## [0.3.1] - 2018-06-23 342 | 343 | ### Changed 344 | 345 | - Removed deprecated `rand::Rng::gen_ascii_chars()`. 346 | - Silence deprecation warning on `pwhash::unix_crypt`. 347 | - Updated dependencies. 348 | 349 | ## [0.3.0] - 2017-12-05 350 | 351 | ### Added 352 | 353 | - New compiling features to select that hashers to include. 354 | 355 | ### Changed 356 | 357 | - Renamed Django version enum and its items. 358 | 359 | ## [0.2.12] - 2017-12-03 360 | 361 | ### Changed 362 | 363 | - Set default Django version to 2.0 364 | - Updated dependencies. 365 | 366 | ## [0.2.11] - 2017-10-02 367 | 368 | ### Added 369 | 370 | - Added protection against time-attacks on string comparisons. 371 | 372 | ## [0.2.10] - 2017-09-02 373 | 374 | ### Added 375 | 376 | - Travis-CI badge. 377 | 378 | ## [0.2.9] - 2017-06-14 379 | 380 | ### Changed 381 | 382 | - Updated base64 to take advantage of new optimizations. 383 | 384 | ## [0.2.8] - 2017-06-07 385 | 386 | ### Changed 387 | 388 | - Replaced deprecated rustc-serialize with base64. 389 | 390 | ## [0.2.7] - 2017-04-05 391 | 392 | ### Changed 393 | 394 | - Set default Django version to 1.11. 395 | - Updated dependencies. 396 | 397 | ## [0.2.6] - 2017-02-04 398 | 399 | ### Added 400 | 401 | - Support to Argon2PasswordHasher. 402 | - Support to Django 1.11. 403 | 404 | ### Changed 405 | 406 | - Set default Django version to 1.10. 407 | - Updated dependencies. 408 | 409 | ## [0.2.5] - 2016-09-19 410 | 411 | ### Added 412 | 413 | - Fuzzy tests, via quickcheck (thank's @fbecart). 414 | 415 | ### Changed 416 | 417 | - Fixed MD5 check: "blank salt" doesn't mean "unsalted". 418 | - Function make_password_core now panics with Invalid salt. 419 | 420 | ## [0.2.2] - 2016-03-29 421 | 422 | ### Added 423 | 424 | - Support to CryptPasswordHasher, UNIX crypt(3) hash function. 425 | 426 | ## [0.2.1] - 2016-03-24 427 | 428 | ### Added 429 | 430 | - Option of choosing a Django version to generate the password. 431 | 432 | ## [0.2.0] - 2016-03-22 433 | 434 | ### Added 435 | 436 | - Option of using fastpbkdf2 (requires OpenSSL to build). 437 | 438 | ## [0.1.0] - 2016-01-01 439 | 440 | ### Added 441 | 442 | - Functional parity with actual password hashers from Django Project. 443 | - Line-by-line translation from Django’s tests. 444 | - Extra tests to guarantee compatibility. 445 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rust DjangoHashers 2 | 3 | A Rust port of the password primitives used in [Django Project](https://www.djangoproject.com). 4 | 5 | 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. 6 | 7 | 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. 8 | 9 | ## TL;DR 10 | 11 | Content of `examples/tldr.rs`: 12 | 13 | ```rust 14 | extern crate djangohashers; 15 | use djangohashers::*; 16 | 17 | fn main() { 18 | let encoded = make_password("K2jitmJ3CBfo"); 19 | println!("Hash: {:?}", encoded); 20 | let is_valid = check_password("K2jitmJ3CBfo", &encoded).unwrap(); 21 | println!("Is valid: {:?}", is_valid); 22 | } 23 | ``` 24 | 25 | Output: 26 | 27 | ``` 28 | $ cargo run --quiet --example tldr 29 | Hash: "pbkdf2_sha256$1000000$pQE1Pfr1CUpS$gDLIrbspb7isluj1zxcItegXxrE1BJP3sdg61S+72rw=" 30 | Is valid: true 31 | ``` 32 | 33 | 34 | ## Installation 35 | 36 | Add the dependency to your `Cargo.toml`: 37 | 38 | ```toml 39 | [dependencies] 40 | djangohashers = "^1.8" 41 | ``` 42 | 43 | Reference and import: 44 | 45 | ```rust 46 | extern crate djangohashers; 47 | 48 | // Everything (it's not much): 49 | use djangohashers::*; 50 | 51 | // Or, just what you need: 52 | use djangohashers::{check_password, make_password, Algorithm}; 53 | ``` 54 | 55 | ## Compiling Features 56 | 57 | By default all the hashers are enabled, but you can pick only the hashers that you need to avoid unneeded dependencies. 58 | 59 | * `default`: all hashers. 60 | * `with_pbkdf2`: only **PBKDF2** and **PBKDF2SHA1**. 61 | * `with_argon2`: only **Argon2**. 62 | * `with_scrypt`: only **Scrypt**. 63 | * `with_bcrypt`: only **BCrypt** and **BCryptSHA256**. 64 | * `with_legacy`: only **SHA1**, **MD5**, **UnsaltedSHA1**, **UnsaltedMD5** and **Crypt**. 65 | * `fpbkdf2`: enables **Fast PBKDF2** (requires OpenSSL, see below). 66 | * `fuzzy_tests`: only for development, enables fuzzy tests. 67 | 68 | ## Fast PBKDF2 Version 69 | 70 | 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. 71 | 72 | ### Installation 73 | 74 | Add the dependency to your `Cargo.toml` declaring the feature: 75 | 76 | ```toml 77 | [dependencies.djangohashers] 78 | version = "^1.8" 79 | features = ["fpbkdf2"] 80 | ``` 81 | 82 | 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: 83 | 84 | ``` 85 | $ brew install openssl 86 | $ export LIBRARY_PATH="$(brew --prefix openssl)/lib" 87 | $ export CFLAGS="-I$(brew --prefix openssl)/include" 88 | $ cargo ... 89 | ``` 90 | 91 | 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. 92 | 93 | ### Performance 94 | 95 | On a Apple M4 Pro: 96 | 97 | Method | Encode or Check | Performance 98 | ------- | --------------- | ------- 99 | Django 5.2.0 on Python 3.13.2 | 136ms | 100% (baseline) 100 | djangohashers with ring::pbkdf2 (default) | 77ms | 56.6% 🐇 101 | djangohashers with fastpbkdf2 | 49ms | 36.0% 🐇 102 | 103 | Replicate test above with Docker: 104 | 105 | ``` 106 | $ docker build -t rs-dj-hashers-profile . 107 | ... 108 | 109 | $ docker run -t rs-dj-hashers-profile 110 | Hashing time: 136ms (Python 3.13.2, Django 5.2.0). 111 | Hashing time: 77ms (Vanilla PBKDF2). 112 | Hashing time: 49ms (Fast PBKDF2). 113 | ``` 114 | 115 | ## Compatibility 116 | 117 | 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](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). 118 | 119 | What is **not** covered: 120 | 121 | * Upgrade/Downgrade callbacks. 122 | * Any 3rd-party hasher outside Django's code. 123 | * Some tests that makes no sense in idiomatic Rust. 124 | 125 | ## Usage 126 | 127 | [API Documentation](https://docs.rs/djangohashers/), thanks to **docs.rs** project! 128 | 129 | ### Verifying a Hashed Password 130 | 131 | Function signatures: 132 | 133 | ```rust 134 | pub fn check_password(password: &str, encoded: &str) -> Result {} 135 | pub fn check_password_tolerant(password: &str, encoded: &str) -> bool {} 136 | ``` 137 | 138 | Complete version: 139 | 140 | ```rust 141 | let password = "KRONOS"; // Sent by the user. 142 | let encoded = "pbkdf2_sha256$24000$..."; // Fetched from DB. 143 | 144 | match check_password(password, encoded) { 145 | Ok(valid) => { 146 | if valid { 147 | // Log the user in. 148 | } else { 149 | // Ask the user to try again. 150 | } 151 | } 152 | Err(error) => { 153 | // Deal with the error. 154 | } 155 | } 156 | ``` 157 | 158 | Possible Errors: 159 | 160 | * `HasherError::UnknownAlgorithm`: anything not recognizable as an algorithm. 161 | * `HasherError::BadHash`: Hash string is corrupted. 162 | * `HasherError::InvalidIterations`: number of iterations is not a positive integer. 163 | * `HasherError::EmptyHash`: hash string is empty. 164 | * `HasherError::InvalidArgon2Salt`: Argon2 salt should be Base64 encoded. 165 | 166 | 167 | If you want to automatically assume all errors as *"invalid password"*, there is a shortcut for that: 168 | 169 | ```rust 170 | if check_password_tolerant(password, encoded) { 171 | // Log the user in. 172 | } else { 173 | // Ask the user to try again. 174 | } 175 | ``` 176 | 177 | 178 | ### Generating a Hashed Password 179 | 180 | Function signatures: 181 | 182 | ```rust 183 | pub fn make_password(password: &str) -> String {} 184 | pub fn make_password_with_algorithm(password: &str, algorithm: Algorithm) -> String {} 185 | pub fn make_password_with_settings(password: &str, salt: &str, algorithm: Algorithm) -> String {} 186 | ``` 187 | 188 | Available algorithms: 189 | 190 | * `Algorithm::PBKDF2` (default) 191 | * `Algorithm::PBKDF2SHA1` 192 | * `Algorithm::Argon2` 193 | * `Algorithm::Scrypt` 194 | * `Algorithm::BCryptSHA256` 195 | * `Algorithm::BCrypt` 196 | * `Algorithm::SHA1` 197 | * `Algorithm::MD5` 198 | * `Algorithm::UnsaltedSHA1` 199 | * `Algorithm::UnsaltedMD5` 200 | * `Algorithm::Crypt` 201 | 202 | The algorithms follow the same Django naming model, minus the `PasswordHasher` suffix. 203 | 204 | Using default settings (PBKDF2 algorithm, random salt): 205 | 206 | ```rust 207 | let encoded = make_password("KRONOS"); 208 | // Returns something like: 209 | // pbkdf2_sha256$24000$go9s3b1y1BTe$Pksk4EptJ84KDnI7ciocmhzFAb5lFoFwd6qlPOwwW4Q= 210 | ``` 211 | 212 | Using a defined algorithm (random salt): 213 | 214 | ```rust 215 | let encoded = make_password_with_algorithm("KRONOS", Algorithm::BCryptSHA256); 216 | // Returns something like: 217 | // bcrypt_sha256$$2b$12$e5C3zfswn.CowOBbbb7ngeYbxKzJePCDHwo8AMr/SZeZCoGrk7oue 218 | ``` 219 | 220 | Using a defined algorithm and salt (not recommended, use it only for debug): 221 | 222 | ```rust 223 | let encoded = make_password_with_settings("KRONOS", "seasalt", Algorithm::PBKDF2SHA1); 224 | // Returns exactly this (remember, the salt is fixed!): 225 | // pbkdf2_sha1$24000$seasalt$F+kiWNHXbMBcwgxsvSKFCWHnZZ0= 226 | ``` 227 | 228 | **Warning**: `make_password_with_settings` and `make_password_core` will both panic if salt is not only letters and numbers (`^[A-Za-z0-9]*$`). 229 | 230 | ### Generating a Hashed Password based on a Django version 231 | 232 | 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. 233 | 234 | ```rust 235 | use djangohashers::{Django, DjangoVersion}; 236 | 237 | let django = Django {version: DjangoVersion::V1_8}; // Django 1.8. 238 | let encoded = django.make_password("KRONOS"); 239 | // Returns something like: 240 | // pbkdf2_sha256$20000$u0C1E8jrnAYx$7KIo/fAuBJpswQyL7pTxO06ccrSjGdIe7iSqzdVub1w= 241 | // ||||| 242 | // ...notice the 20000 iterations, used in Django 1.8. 243 | ``` 244 | 245 | Available versions: 246 | 247 | * `DjangoVersion::CURRENT` Current Django version (`5.2` for DjangoHashers `1.8.3`). 248 | * `DjangoVersion::V1_4` Django 1.4 249 | * `DjangoVersion::V1_5` Django 1.5 250 | * `DjangoVersion::V1_6` Django 1.6 251 | * `DjangoVersion::V1_7` Django 1.7 252 | * `DjangoVersion::V1_8` Django 1.8 253 | * `DjangoVersion::V1_9` Django 1.9 254 | * `DjangoVersion::V1_10` Django 1.10 255 | * `DjangoVersion::V1_11` Django 1.11 256 | * `DjangoVersion::V2_0` Django 2.0 257 | * `DjangoVersion::V2_1` Django 2.1 258 | * `DjangoVersion::V2_2` Django 2.2 259 | * `DjangoVersion::V3_0` Django 3.0 260 | * `DjangoVersion::V3_1` Django 3.1 261 | * `DjangoVersion::V3_2` Django 3.2 262 | * `DjangoVersion::V4_0` Django 4.0 263 | * `DjangoVersion::V4_1` Django 4.1 264 | * `DjangoVersion::V4_2` Django 4.2 265 | * `DjangoVersion::V5_0` Django 5.0 266 | * `DjangoVersion::V5_1` Django 5.1 267 | * `DjangoVersion::V5_2` Django 5.2 268 | * `DjangoVersion::V6_0` Django 6.0 269 | * `DjangoVersion::V6_1` Django 6.1 270 | 271 | ### Verifying a Hash Format (pre-crypto) 272 | 273 | Function signature: 274 | 275 | ```rust 276 | pub fn is_password_usable(encoded: &str) -> bool {} 277 | ``` 278 | 279 | You can check if the password hash is properly formatted before running the expensive cryto stuff: 280 | 281 | ```rust 282 | let encoded = "pbkdf2_sha256$24000$..."; // Fetched from DB. 283 | 284 | if is_password_usable(encoded) { 285 | // Go ahead. 286 | } else { 287 | // Check your database or report an issue. 288 | } 289 | ``` 290 | 291 | ## Contributing 292 | 293 | * Be patient with me, I’m new to Rust and this is my first project. 294 | * Don't go nuts with your *mad-rust-skillz*, legibility is a priority. 295 | * Please use [rustfmt](https://github.com/rust-lang-nursery/rustfmt) in your code. 296 | * Always include some test case. 297 | 298 | ## License 299 | 300 | Rust DjangoHashers is released under the **3-Clause BSD License**. 301 | 302 | **tl;dr**: *"free to use as long as you credit me"*. 303 | -------------------------------------------------------------------------------- /tests/django19.rs: -------------------------------------------------------------------------------- 1 | //! This is an almost line-by-line translation from the hashers' test from Django 1.9: 2 | //! https://github.com/django/django/blob/e403f22/tests/auth_tests/test_hashers.py 3 | //! ...except for some cases that don't make sense in Rust, or in the scope of this library. 4 | 5 | use djangohashers::*; 6 | 7 | #[test] 8 | #[cfg(feature = "with_pbkdf2")] 9 | fn test_simple() { 10 | let django = Django { 11 | version: DjangoVersion::V1_9, 12 | }; 13 | let encoded = django.make_password("lètmein"); 14 | assert!(encoded.starts_with("pbkdf2_sha256$")); 15 | assert!(is_password_usable(&encoded)); 16 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 17 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 18 | // Blank passwords 19 | let blank_encoded = django.make_password(""); 20 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 21 | assert!(is_password_usable(&blank_encoded)); 22 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 23 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 24 | } 25 | 26 | #[test] 27 | #[cfg(feature = "with_pbkdf2")] 28 | fn test_pbkdf2() { 29 | let django = Django { 30 | version: DjangoVersion::V1_9, 31 | }; 32 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::PBKDF2); 33 | assert_eq!( 34 | encoded, 35 | "pbkdf2_sha256$24000$seasalt$V9DfCAVoweeLwxC/L2mb+7swhzF0XYdyQMqmusZqiTc=" 36 | ); 37 | assert!(is_password_usable(&encoded)); 38 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 39 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 40 | // Blank passwords 41 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::PBKDF2); 42 | assert!(blank_encoded.starts_with("pbkdf2_sha256$")); 43 | assert!(is_password_usable(&blank_encoded)); 44 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 45 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 46 | } 47 | 48 | #[test] 49 | #[cfg(feature = "with_legacy")] 50 | fn test_sha1() { 51 | let django = Django { 52 | version: DjangoVersion::V1_9, 53 | }; 54 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::SHA1); 55 | assert_eq!( 56 | encoded, 57 | "sha1$seasalt$cff36ea83f5706ce9aa7454e63e431fc726b2dc8" 58 | ); 59 | assert!(is_password_usable(&encoded)); 60 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 61 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 62 | // Blank passwords 63 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::SHA1); 64 | assert!(blank_encoded.starts_with("sha1$")); 65 | assert!(is_password_usable(&blank_encoded)); 66 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 67 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 68 | } 69 | 70 | #[test] 71 | #[cfg(feature = "with_legacy")] 72 | fn test_md5() { 73 | let django = Django { 74 | version: DjangoVersion::V1_9, 75 | }; 76 | let encoded = django.make_password_with_settings("lètmein", "seasalt", Algorithm::MD5); 77 | assert_eq!(encoded, "md5$seasalt$3f86d0d3d465b7b458c231bf3555c0e3"); 78 | assert!(is_password_usable(&encoded)); 79 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 80 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 81 | // Blank passwords 82 | let blank_encoded = django.make_password_with_settings("", "seasalt", Algorithm::MD5); 83 | assert!(blank_encoded.starts_with("md5$")); 84 | assert!(is_password_usable(&blank_encoded)); 85 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 86 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 87 | } 88 | 89 | #[test] 90 | #[cfg(feature = "with_legacy")] 91 | fn test_unsalted_md5() { 92 | let django = Django { 93 | version: DjangoVersion::V1_9, 94 | }; 95 | let encoded = django.make_password_with_settings("lètmein", "", Algorithm::UnsaltedMD5); 96 | assert_eq!(encoded, "88a434c88cca4e900f7874cd98123f43"); 97 | assert!(is_password_usable(&encoded)); 98 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 99 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 100 | // Blank passwords 101 | let blank_encoded = django.make_password_with_settings("", "", Algorithm::UnsaltedMD5); 102 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 103 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 104 | } 105 | 106 | #[test] 107 | #[cfg(feature = "with_legacy")] 108 | fn test_unsalted_sha1() { 109 | let django = Django { 110 | version: DjangoVersion::V1_9, 111 | }; 112 | let encoded = django.make_password_with_settings("lètmein", "", Algorithm::UnsaltedSHA1); 113 | assert_eq!(encoded, "sha1$$6d138ca3ae545631b3abd71a4f076ce759c5700b"); 114 | assert!(is_password_usable(&encoded)); 115 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 116 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 117 | // Raw SHA1 isn't acceptable 118 | assert!(check_password("lètmein", "6d138ca3ae545631b3abd71a4f076ce759c5700b").is_err()); 119 | // Blank passwords 120 | let blank_encoded = django.make_password_with_settings("", "", Algorithm::UnsaltedSHA1); 121 | assert!(blank_encoded.starts_with("sha1$")); 122 | assert!(is_password_usable(&blank_encoded)); 123 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 124 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 125 | } 126 | 127 | #[test] 128 | #[cfg(feature = "with_legacy")] 129 | fn test_crypt() { 130 | let django = Django { 131 | version: DjangoVersion::V1_9, 132 | }; 133 | let encoded = django.make_password_with_settings("lètmei", "ab", Algorithm::Crypt); 134 | assert_eq!(encoded, "crypt$$ab1Hv2Lg7ltQo"); 135 | assert!(is_password_usable(&encoded)); 136 | assert_eq!(check_password("lètmei", &encoded), Ok(true)); 137 | assert_eq!(check_password("lètmeiz", &encoded), Ok(false)); 138 | // Blank passwords 139 | let blank_encoded = django.make_password_with_settings("", "ab", Algorithm::Crypt); 140 | assert!(blank_encoded.starts_with("crypt$")); 141 | assert!(is_password_usable(&blank_encoded)); 142 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 143 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 144 | } 145 | 146 | #[test] 147 | #[cfg(feature = "with_bcrypt")] 148 | fn test_bcrypt_sha256() { 149 | let django = Django { 150 | version: DjangoVersion::V1_9, 151 | }; 152 | let encoded = django.make_password_with_settings("lètmein", "", Algorithm::BCryptSHA256); 153 | assert!(is_password_usable(&encoded)); 154 | assert!(encoded.starts_with("bcrypt_sha256$")); 155 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 156 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 157 | // Verify that password truncation no longer works 158 | let password = "VSK0UYV6FFQVZ0KG88DYN9WADAADZO1CTSIVDJUNZSUML6IBX7LN7ZS3R5JGB3RGZ7VI7G7DJQ9NI8\ 159 | BQFSRPTG6UWTTVESA5ZPUN"; 160 | let trunc_encoded = django.make_password_with_settings(password, "", Algorithm::BCryptSHA256); 161 | assert_eq!(check_password(password, &trunc_encoded), Ok(true)); 162 | assert_eq!(check_password(&password[0..72], &trunc_encoded), Ok(false)); 163 | // Blank passwords 164 | let blank_encoded = django.make_password_with_settings("", "", Algorithm::BCryptSHA256); 165 | assert!(is_password_usable(&blank_encoded)); 166 | assert!(blank_encoded.starts_with("bcrypt_sha256$")); 167 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 168 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 169 | } 170 | 171 | #[test] 172 | #[cfg(feature = "with_bcrypt")] 173 | fn test_bcrypt() { 174 | let django = Django { 175 | version: DjangoVersion::V1_9, 176 | }; 177 | let encoded = django.make_password_with_settings("lètmein", "", Algorithm::BCrypt); 178 | assert!(is_password_usable(&encoded)); 179 | assert!(encoded.starts_with("bcrypt$")); 180 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 181 | assert_eq!(check_password("lètmeinz", &encoded), Ok(false)); 182 | // Blank passwords 183 | let blank_encoded = django.make_password_with_settings("", "", Algorithm::BCrypt); 184 | assert!(is_password_usable(&blank_encoded)); 185 | assert!(blank_encoded.starts_with("bcrypt$")); 186 | assert_eq!(check_password("", &blank_encoded), Ok(true)); 187 | assert_eq!(check_password(" ", &blank_encoded), Ok(false)); 188 | } 189 | 190 | // This library does not fire upgrade callbacks: 191 | // - test_bcrypt_upgrade 192 | 193 | #[test] 194 | fn test_unusable() { 195 | let encoded = "!Q24gQu9Sy3X1PJPCaEMTRrw5eLFWY8htI2FsqCbC"; // From make_password(None) 196 | assert!(encoded.len() == 41); 197 | assert!(!is_password_usable(encoded)); 198 | assert!(check_password(encoded, encoded).is_err()); 199 | assert!(check_password("!", encoded).is_err()); 200 | assert!(check_password("", encoded).is_err()); 201 | assert!(check_password("lètmein", encoded).is_err()); 202 | assert!(check_password("lètmeinz", encoded).is_err()); 203 | } 204 | 205 | // Scenario not possible during run time: 206 | // - test_unspecified_password 207 | // - test_bad_algorithm 208 | 209 | #[test] 210 | fn test_bad_encoded() { 211 | assert!(!is_password_usable("lètmein_badencoded")); 212 | assert!(!is_password_usable("")); 213 | } 214 | 215 | #[test] 216 | #[cfg(feature = "with_pbkdf2")] 217 | fn test_low_level_pbkdf2() { 218 | let django = Django { 219 | version: DjangoVersion::V1_9, 220 | }; 221 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2); 222 | assert_eq!( 223 | encoded, 224 | "pbkdf2_sha256$24000$seasalt2$TUDkfilKHVC7BkaKSZgIKhm0aTtXlmcw/5C1FeS/DPk=" 225 | ); 226 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 227 | } 228 | 229 | #[test] 230 | #[cfg(feature = "with_pbkdf2")] 231 | fn test_low_level_pbkdf2_sha1() { 232 | let django = Django { 233 | version: DjangoVersion::V1_9, 234 | }; 235 | let encoded = django.make_password_with_settings("lètmein", "seasalt2", Algorithm::PBKDF2SHA1); 236 | assert_eq!( 237 | encoded, 238 | "pbkdf2_sha1$24000$seasalt2$L37ETdd9trqrsJDwapU3P+2Edhg=" 239 | ); 240 | assert_eq!(check_password("lètmein", &encoded), Ok(true)); 241 | } 242 | 243 | // This library does not fire upgrade callbacks: 244 | // - test_upgrade 245 | // - test_no_upgrade 246 | // - test_no_upgrade_on_incorrect_pass 247 | // - test_pbkdf2_upgrade 248 | // - test_pbkdf2_upgrade_new_hasher 249 | 250 | // Scenario not possible during run time: 251 | // - test_load_library_no_algorithm 252 | // - test_load_library_importerror 253 | -------------------------------------------------------------------------------- /src/hashers.rs: -------------------------------------------------------------------------------- 1 | use crate::crypto_utils; 2 | use std::str; 3 | 4 | #[cfg(feature = "with_argon2")] 5 | use base64::engine::Engine as _; 6 | #[cfg(feature = "with_argon2")] 7 | use base64::engine::general_purpose; 8 | 9 | #[cfg(feature = "with_pbkdf2")] 10 | static PBKDF2_ITERATIONS_DOS_LIMIT: u32 = 1_500_000; 11 | #[cfg(feature = "with_bcrypt")] 12 | static BCRYPT_COST_DOS_LIMIT: u32 = 16; 13 | 14 | /// Possible errors during a hash creation. 15 | #[derive(PartialEq, Debug)] 16 | pub enum HasherError { 17 | /// Algorithm not recognizable. 18 | UnknownAlgorithm, 19 | /// Hash string is corrupted. 20 | BadHash, 21 | /// Hash string is empty. 22 | EmptyHash, 23 | /// Number of iterations is not a positive integer. 24 | InvalidIterations, // Check PBKDF2_ITERATIONS_DOS_LIMIT. 25 | /// Argon2 salt should be Base64 encoded. 26 | InvalidArgon2Salt, 27 | } 28 | 29 | /// Hasher abstraction, providing methods to encode and verify hashes. 30 | pub trait Hasher { 31 | /// Verifies a password against an encoded hash. 32 | fn verify(&self, password: &str, encoded: &str) -> Result; 33 | /// Generates an encoded hash for a given password and salt. 34 | fn encode(&self, password: &str, salt: &str, iterations: u32) -> String; 35 | } 36 | 37 | // List of Hashers: 38 | 39 | /// Hasher that uses the PBKDF2 key-derivation function with the SHA256 hashing algorithm. 40 | #[cfg(feature = "with_pbkdf2")] 41 | pub struct PBKDF2Hasher; 42 | 43 | #[cfg(feature = "with_pbkdf2")] 44 | impl Hasher for PBKDF2Hasher { 45 | fn verify(&self, password: &str, encoded: &str) -> Result { 46 | let mut encoded_part = encoded.split('$').skip(1); 47 | let iterations = encoded_part 48 | .next() 49 | .ok_or(HasherError::BadHash)? 50 | .parse::() 51 | .map_err(|_| HasherError::InvalidIterations)?; 52 | if iterations > PBKDF2_ITERATIONS_DOS_LIMIT { 53 | return Err(HasherError::InvalidIterations); 54 | } 55 | let salt = encoded_part.next().ok_or(HasherError::BadHash)?; 56 | let hash = encoded_part.next().ok_or(HasherError::BadHash)?; 57 | Ok(crypto_utils::safe_eq( 58 | hash, 59 | crypto_utils::hash_pbkdf2_sha256(password, salt, iterations), 60 | )) 61 | } 62 | 63 | fn encode(&self, password: &str, salt: &str, iterations: u32) -> String { 64 | let hash = crypto_utils::hash_pbkdf2_sha256(password, salt, iterations); 65 | format!("{}${}${}${}", "pbkdf2_sha256", iterations, salt, hash) 66 | } 67 | } 68 | 69 | /// Hasher that uses the PBKDF2 key-derivation function with the SHA1 hashing algorithm. 70 | #[cfg(feature = "with_pbkdf2")] 71 | pub struct PBKDF2SHA1Hasher; 72 | 73 | #[cfg(feature = "with_pbkdf2")] 74 | impl Hasher for PBKDF2SHA1Hasher { 75 | fn verify(&self, password: &str, encoded: &str) -> Result { 76 | let mut encoded_part = encoded.split('$').skip(1); 77 | let iterations = encoded_part 78 | .next() 79 | .ok_or(HasherError::BadHash)? 80 | .parse::() 81 | .map_err(|_| HasherError::InvalidIterations)?; 82 | if iterations > PBKDF2_ITERATIONS_DOS_LIMIT { 83 | return Err(HasherError::InvalidIterations); 84 | } 85 | let salt = encoded_part.next().ok_or(HasherError::BadHash)?; 86 | let hash = encoded_part.next().ok_or(HasherError::BadHash)?; 87 | Ok(crypto_utils::safe_eq( 88 | hash, 89 | crypto_utils::hash_pbkdf2_sha1(password, salt, iterations), 90 | )) 91 | } 92 | 93 | fn encode(&self, password: &str, salt: &str, iterations: u32) -> String { 94 | let hash = crypto_utils::hash_pbkdf2_sha1(password, salt, iterations); 95 | format!("{}${}${}${}", "pbkdf2_sha1", iterations, salt, hash) 96 | } 97 | } 98 | 99 | /// Hasher that uses the Argon2 function (new in Django 1.10). 100 | #[cfg(feature = "with_argon2")] 101 | pub struct Argon2Hasher; 102 | 103 | #[cfg(feature = "with_argon2")] 104 | use argon2::{self, Version}; 105 | 106 | #[cfg(feature = "with_argon2")] 107 | impl Hasher for Argon2Hasher { 108 | fn verify(&self, password: &str, encoded: &str) -> Result { 109 | let encoded_part: Vec<&str> = encoded.split('$').collect(); 110 | let version = match encoded_part.len() { 111 | 6 => Version::Version13, 112 | 5 => Version::Version10, 113 | _ => return Err(HasherError::BadHash), 114 | }; 115 | let segment_shift = 6 - encoded_part.len(); 116 | let settings = encoded_part[3 - segment_shift]; 117 | let salt = encoded_part[4 - segment_shift]; 118 | let string_hash = encoded_part[5 - segment_shift].replace('+', "-"); 119 | let hash = string_hash.as_str(); 120 | let settings_part: Vec<&str> = settings.split(',').collect(); 121 | let memory_cost: u32 = settings_part[0].split('=').collect::>()[1] 122 | .parse::() 123 | .map_err(|_| HasherError::BadHash)?; 124 | let time_cost: u32 = settings_part[1].split('=').collect::>()[1] 125 | .parse::() 126 | .map_err(|_| HasherError::BadHash)?; 127 | let parallelism: u32 = settings_part[2].split('=').collect::>()[1] 128 | .parse::() 129 | .map_err(|_| HasherError::BadHash)?; 130 | 131 | // Django's implementation expects a Base64-encoded salt, if it is not, return an error: 132 | if general_purpose::URL_SAFE_NO_PAD.decode(hash).is_err() { 133 | return Err(HasherError::InvalidArgon2Salt); 134 | } 135 | 136 | // Argon2 has a flexible hash length: 137 | let hash_length = match general_purpose::URL_SAFE_NO_PAD.decode(hash) { 138 | Ok(value) => value.len() as u32, 139 | Err(_) => return Ok(false), 140 | }; 141 | 142 | Ok(crypto_utils::safe_eq( 143 | hash, 144 | crypto_utils::hash_argon2( 145 | password, 146 | salt, 147 | time_cost, 148 | memory_cost, 149 | parallelism, 150 | version, 151 | hash_length, 152 | ), 153 | )) 154 | } 155 | 156 | fn encode(&self, password: &str, salt: &str, iterations: u32) -> String { 157 | // - memory_cost: "kib" in Argon2's lingo. 158 | // - parallelism: "lanes" in Argon2's lingo. 159 | // - time_cost: "passes" in Argon2's lingo. 160 | let (memory_cost, parallelism, time_cost) = match iterations { 161 | 1 => (512, 2, 2), 162 | 2 => (102400, 8, 2), 163 | _ => unreachable!(), 164 | }; 165 | let version = Version::Version13; 166 | let hash_length: u32 = 16; 167 | let hash = crypto_utils::hash_argon2( 168 | password, 169 | salt, 170 | time_cost, 171 | memory_cost, 172 | parallelism, 173 | version, 174 | hash_length, 175 | ); 176 | format!( 177 | "argon2$argon2id$v=19$m={},t={},p={}${}${}", 178 | memory_cost, time_cost, parallelism, salt, hash 179 | ) 180 | } 181 | } 182 | 183 | /// Hasher that uses the Scrypt function (new in Django 4.0). 184 | #[cfg(feature = "with_scrypt")] 185 | pub struct ScryptHasher; 186 | 187 | #[cfg(feature = "with_scrypt")] 188 | impl Hasher for ScryptHasher { 189 | fn verify(&self, password: &str, encoded: &str) -> Result { 190 | let encoded_part: Vec<&str> = encoded.split('$').collect(); 191 | let work_factor: u8 = encoded_part[1].parse::().unwrap().log2() as u8; 192 | let salt = encoded_part[2]; 193 | let block_size = encoded_part[3].parse::().unwrap(); 194 | let parallelism = encoded_part[4].parse::().unwrap(); 195 | let hash = encoded_part[5]; 196 | Ok(crypto_utils::safe_eq( 197 | hash, 198 | crypto_utils::hash_scrypt(password, salt, work_factor, block_size, parallelism), 199 | )) 200 | } 201 | 202 | fn encode(&self, password: &str, salt: &str, iterations: u32) -> String { 203 | // - work_factor: "n" in Scrypt's lingo. 204 | // - block_size: "r" in Scrypt's lingo. 205 | // - parallelism: "p" in Scrypt's lingo. 206 | let (work_factor, block_size, parallelism) = match iterations { 207 | 1 => (14, 8, 1), 208 | _ => unreachable!(), 209 | }; 210 | let hash = crypto_utils::hash_scrypt(password, salt, work_factor, block_size, parallelism); 211 | format!( 212 | "scrypt${}${}${}${}${}", 213 | 2i32.pow(work_factor as u32), 214 | salt, 215 | block_size, 216 | parallelism, 217 | hash 218 | ) 219 | } 220 | } 221 | 222 | /// Hasher that uses the bcrypt key-derivation function with the password padded with SHA256. 223 | #[cfg(feature = "with_bcrypt")] 224 | pub struct BCryptSHA256Hasher; 225 | 226 | #[cfg(feature = "with_bcrypt")] 227 | impl Hasher for BCryptSHA256Hasher { 228 | fn verify(&self, password: &str, encoded: &str) -> Result { 229 | let bcrypt_encoded_part: Vec<&str> = encoded.splitn(2, '$').collect(); 230 | let cost = bcrypt_encoded_part[1] 231 | .split('$') 232 | .nth(2) 233 | .ok_or(HasherError::BadHash)? 234 | .parse::() 235 | .map_err(|_| HasherError::InvalidIterations)?; 236 | if cost > BCRYPT_COST_DOS_LIMIT { 237 | return Err(HasherError::InvalidIterations); 238 | } 239 | let hash = bcrypt_encoded_part[1]; 240 | let hashed_password = crypto_utils::hash_sha256(password); 241 | Ok(bcrypt::verify(hashed_password, hash).unwrap_or(false)) 242 | } 243 | 244 | fn encode(&self, password: &str, _: &str, iterations: u32) -> String { 245 | let hashed_password = crypto_utils::hash_sha256(password); 246 | let hash = bcrypt::hash(hashed_password, iterations).unwrap(); 247 | format!("{}${}", "bcrypt_sha256", hash) 248 | } 249 | } 250 | 251 | /// Hasher that uses the bcrypt key-derivation function without password padding. 252 | #[cfg(feature = "with_bcrypt")] 253 | pub struct BCryptHasher; 254 | 255 | #[cfg(feature = "with_bcrypt")] 256 | impl Hasher for BCryptHasher { 257 | fn verify(&self, password: &str, encoded: &str) -> Result { 258 | let bcrypt_encoded_part: Vec<&str> = encoded.splitn(2, '$').collect(); 259 | let cost = bcrypt_encoded_part[1] 260 | .split('$') 261 | .nth(2) 262 | .ok_or(HasherError::BadHash)? 263 | .parse::() 264 | .map_err(|_| HasherError::InvalidIterations)?; 265 | if cost > BCRYPT_COST_DOS_LIMIT { 266 | return Err(HasherError::InvalidIterations); 267 | } 268 | let hash = bcrypt_encoded_part[1]; 269 | Ok(bcrypt::verify(password, hash).unwrap_or(false)) 270 | } 271 | 272 | fn encode(&self, password: &str, _: &str, iterations: u32) -> String { 273 | let hash = bcrypt::hash(password, iterations).unwrap(); 274 | format!("{}${}", "bcrypt", hash) 275 | } 276 | } 277 | 278 | /// Hasher that uses the SHA1 hashing function over the salted password. 279 | #[cfg(feature = "with_legacy")] 280 | pub struct SHA1Hasher; 281 | 282 | #[cfg(feature = "with_legacy")] 283 | impl Hasher for SHA1Hasher { 284 | fn verify(&self, password: &str, encoded: &str) -> Result { 285 | let mut encoded_part = encoded.split('$').skip(1); 286 | let salt = encoded_part.next().ok_or(HasherError::BadHash)?; 287 | let hash = encoded_part.next().ok_or(HasherError::BadHash)?; 288 | Ok(crypto_utils::safe_eq( 289 | hash, 290 | crypto_utils::hash_sha1(password, salt), 291 | )) 292 | } 293 | 294 | fn encode(&self, password: &str, salt: &str, _: u32) -> String { 295 | let hash = crypto_utils::hash_sha1(password, salt); 296 | format!("{}${}${}", "sha1", salt, hash) 297 | } 298 | } 299 | 300 | /// Hasher that uses the MD5 hashing function over the salted password. 301 | #[cfg(feature = "with_legacy")] 302 | pub struct MD5Hasher; 303 | 304 | #[cfg(feature = "with_legacy")] 305 | impl Hasher for MD5Hasher { 306 | fn verify(&self, password: &str, encoded: &str) -> Result { 307 | let mut encoded_part = encoded.split('$').skip(1); 308 | let salt = encoded_part.next().ok_or(HasherError::BadHash)?; 309 | let hash = encoded_part.next().ok_or(HasherError::BadHash)?; 310 | Ok(crypto_utils::safe_eq( 311 | hash, 312 | crypto_utils::hash_md5(password, salt), 313 | )) 314 | } 315 | 316 | fn encode(&self, password: &str, salt: &str, _: u32) -> String { 317 | let hash = crypto_utils::hash_md5(password, salt); 318 | format!("{}${}${}", "md5", salt, hash) 319 | } 320 | } 321 | 322 | /// Hasher that uses the SHA1 hashing function with no salting. 323 | #[cfg(feature = "with_legacy")] 324 | pub struct UnsaltedSHA1Hasher; 325 | 326 | #[cfg(feature = "with_legacy")] 327 | impl Hasher for UnsaltedSHA1Hasher { 328 | fn verify(&self, password: &str, encoded: &str) -> Result { 329 | let mut encoded_part = encoded.split('$').skip(2); 330 | let hash = encoded_part.next().ok_or(HasherError::BadHash)?; 331 | Ok(crypto_utils::safe_eq( 332 | hash, 333 | crypto_utils::hash_sha1(password, ""), 334 | )) 335 | } 336 | 337 | fn encode(&self, password: &str, _: &str, _: u32) -> String { 338 | let hash = crypto_utils::hash_sha1(password, ""); 339 | format!("{}$${}", "sha1", hash) 340 | } 341 | } 342 | 343 | /// Hasher that uses the MD5 hashing function with no salting. 344 | #[cfg(feature = "with_legacy")] 345 | pub struct UnsaltedMD5Hasher; 346 | 347 | #[cfg(feature = "with_legacy")] 348 | impl Hasher for UnsaltedMD5Hasher { 349 | fn verify(&self, password: &str, encoded: &str) -> Result { 350 | Ok(crypto_utils::safe_eq( 351 | encoded, 352 | crypto_utils::hash_md5(password, ""), 353 | )) 354 | } 355 | 356 | fn encode(&self, password: &str, _: &str, _: u32) -> String { 357 | crypto_utils::hash_md5(password, "") 358 | } 359 | } 360 | 361 | /// Hasher that uses the UNIX's crypt(3) hash function. 362 | #[cfg(feature = "with_legacy")] 363 | pub struct CryptHasher; 364 | 365 | #[cfg(feature = "with_legacy")] 366 | impl Hasher for CryptHasher { 367 | fn verify(&self, password: &str, encoded: &str) -> Result { 368 | let mut encoded_part = encoded.split('$').skip(2); 369 | let hash = encoded_part.next().ok_or(HasherError::BadHash)?; 370 | Ok(crypto_utils::safe_eq( 371 | hash, 372 | crypto_utils::hash_unix_crypt(password, hash), 373 | )) 374 | } 375 | 376 | fn encode(&self, password: &str, salt: &str, _: u32) -> String { 377 | let hash = crypto_utils::hash_unix_crypt(password, salt); 378 | format!("{}$${}", "crypt", hash) 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A Rust port of the password primitives used in [Django Project](https://www.djangoproject.com). 2 | //! 3 | //! Django's `django.contrib.auth.models.User` class has a few methods to deal with passwords, 4 | //! like `set_password()` and `check_password()`; **DjangoHashers** implements the primitive 5 | //! functions behind that methods. All Django's built-in hashers are supported. 6 | //! 7 | //! This library was conceived for Django integration, but is not limited to it; you can use 8 | //! the password hash algorithm in any Rust project (or FFI integration), since its security 9 | //! model is already battle-tested. 10 | 11 | use lazy_static::lazy_static; 12 | use rand::Rng; 13 | use rand::distr::Alphanumeric; 14 | mod crypto_utils; 15 | mod hashers; 16 | use regex::Regex; 17 | 18 | pub use crate::hashers::*; 19 | 20 | /// Algorithms available to use with Hashers. 21 | #[derive(PartialEq)] 22 | #[cfg_attr(test, derive(Debug))] 23 | pub enum Algorithm { 24 | /// PBKDF2 key-derivation function with the SHA256 hashing algorithm. 25 | #[cfg(feature = "with_pbkdf2")] 26 | PBKDF2, 27 | /// PBKDF2 key-derivation function with the SHA1 hashing algorithm. 28 | #[cfg(feature = "with_pbkdf2")] 29 | PBKDF2SHA1, 30 | /// Argon2 key-derivation function. 31 | #[cfg(feature = "with_argon2")] 32 | Argon2, 33 | /// Scrypt key-derivation function. 34 | #[cfg(feature = "with_scrypt")] 35 | Scrypt, 36 | /// Bcrypt key-derivation function with the password padded with SHA256. 37 | #[cfg(feature = "with_bcrypt")] 38 | BCryptSHA256, 39 | /// Bcrypt key-derivation function without password padding. 40 | #[cfg(feature = "with_bcrypt")] 41 | BCrypt, 42 | /// SHA1 hashing function over the salted password. 43 | #[cfg(feature = "with_legacy")] 44 | SHA1, 45 | /// MD5 hashing function over the salted password. 46 | #[cfg(feature = "with_legacy")] 47 | MD5, 48 | /// SHA1 hashing function with no salting. 49 | #[cfg(feature = "with_legacy")] 50 | UnsaltedSHA1, 51 | /// MD5 hashing function with no salting. 52 | #[cfg(feature = "with_legacy")] 53 | UnsaltedMD5, 54 | /// UNIX's crypt(3) hashing algorithm. 55 | #[cfg(feature = "with_legacy")] 56 | Crypt, 57 | } 58 | 59 | // Parses an encoded hash in order to detect the algorithm, returns it in an Option. 60 | fn identify_hasher(encoded: &str) -> Option { 61 | #[cfg(feature = "with_legacy")] 62 | { 63 | if encoded.len() == 32 && !encoded.contains('$') { 64 | return Some(Algorithm::UnsaltedMD5); 65 | } 66 | if encoded.len() == 46 && encoded.starts_with("sha1$$") { 67 | return Some(Algorithm::UnsaltedSHA1); 68 | } 69 | } 70 | 71 | let encoded_part: &str = encoded.split('$').next()?; 72 | match encoded_part { 73 | #[cfg(feature = "with_pbkdf2")] 74 | "pbkdf2_sha256" => Some(Algorithm::PBKDF2), 75 | #[cfg(feature = "with_pbkdf2")] 76 | "pbkdf2_sha1" => Some(Algorithm::PBKDF2SHA1), 77 | #[cfg(feature = "with_argon2")] 78 | "argon2" => Some(Algorithm::Argon2), 79 | #[cfg(feature = "with_scrypt")] 80 | "scrypt" => Some(Algorithm::Scrypt), 81 | #[cfg(feature = "with_bcrypt")] 82 | "bcrypt_sha256" => Some(Algorithm::BCryptSHA256), 83 | #[cfg(feature = "with_bcrypt")] 84 | "bcrypt" => Some(Algorithm::BCrypt), 85 | #[cfg(feature = "with_legacy")] 86 | "sha1" => Some(Algorithm::SHA1), 87 | #[cfg(feature = "with_legacy")] 88 | "md5" => Some(Algorithm::MD5), 89 | #[cfg(feature = "with_legacy")] 90 | "crypt" => Some(Algorithm::Crypt), 91 | _ => None, 92 | } 93 | } 94 | 95 | // Returns an instance of a Hasher based on the algorithm provided. 96 | fn get_hasher(algorithm: &Algorithm) -> Box { 97 | match *algorithm { 98 | #[cfg(feature = "with_pbkdf2")] 99 | Algorithm::PBKDF2 => Box::new(PBKDF2Hasher), 100 | #[cfg(feature = "with_pbkdf2")] 101 | Algorithm::PBKDF2SHA1 => Box::new(PBKDF2SHA1Hasher), 102 | #[cfg(feature = "with_argon2")] 103 | Algorithm::Argon2 => Box::new(Argon2Hasher), 104 | #[cfg(feature = "with_scrypt")] 105 | Algorithm::Scrypt => Box::new(ScryptHasher), 106 | #[cfg(feature = "with_bcrypt")] 107 | Algorithm::BCryptSHA256 => Box::new(BCryptSHA256Hasher), 108 | #[cfg(feature = "with_bcrypt")] 109 | Algorithm::BCrypt => Box::new(BCryptHasher), 110 | #[cfg(feature = "with_legacy")] 111 | Algorithm::SHA1 => Box::new(SHA1Hasher), 112 | #[cfg(feature = "with_legacy")] 113 | Algorithm::MD5 => Box::new(MD5Hasher), 114 | #[cfg(feature = "with_legacy")] 115 | Algorithm::UnsaltedSHA1 => Box::new(UnsaltedSHA1Hasher), 116 | #[cfg(feature = "with_legacy")] 117 | Algorithm::UnsaltedMD5 => Box::new(UnsaltedMD5Hasher), 118 | #[cfg(feature = "with_legacy")] 119 | Algorithm::Crypt => Box::new(CryptHasher), 120 | } 121 | } 122 | 123 | /// Verifies if an encoded hash is properly formatted before check it cryptographically. 124 | pub fn is_password_usable(encoded: &str) -> bool { 125 | !encoded.is_empty() && !encoded.starts_with('!') && identify_hasher(encoded).is_some() 126 | } 127 | 128 | /// Verifies a password against an encoded hash, returns a Result. 129 | pub fn check_password(password: &str, encoded: &str) -> Result { 130 | if encoded.is_empty() { 131 | return Err(HasherError::EmptyHash); 132 | } 133 | let algorithm = identify_hasher(encoded).ok_or(HasherError::UnknownAlgorithm)?; 134 | let hasher = get_hasher(&algorithm); 135 | hasher.verify(password, encoded) 136 | } 137 | 138 | /// Verifies a password against an encoded hash, returns a boolean, even in case of error. 139 | pub fn check_password_tolerant(password: &str, encoded: &str) -> bool { 140 | check_password(password, encoded).unwrap_or(false) 141 | } 142 | 143 | /// Django Version. 144 | #[derive(Clone)] 145 | #[allow(non_camel_case_types)] 146 | pub enum DjangoVersion { 147 | /// Django 1.4. 148 | V1_4, 149 | /// Django 1.5. 150 | V1_5, 151 | /// Django 1.6. 152 | V1_6, 153 | /// Django 1.7. 154 | V1_7, 155 | /// Django 1.8. 156 | V1_8, 157 | /// Django 1.9. 158 | V1_9, 159 | /// Django 1.10. 160 | V1_10, 161 | /// Django 1.11. 162 | V1_11, 163 | /// Django 2.0. 164 | V2_0, 165 | /// Django 2.1. 166 | V2_1, 167 | /// Django 2.2. 168 | V2_2, 169 | /// Django 3.0. 170 | V3_0, 171 | /// Django 3.1. 172 | V3_1, 173 | /// Django 3.2. 174 | V3_2, 175 | /// Django 4.0. 176 | V4_0, 177 | /// Django 4.1. 178 | V4_1, 179 | /// Django 4.2. 180 | V4_2, 181 | /// Django 5.0. 182 | V5_0, 183 | /// Django 5.1. 184 | V5_1, 185 | /// Django 5.2. 186 | V5_2, 187 | /// Django 6.0. 188 | V6_0, 189 | /// Django 6.1. 190 | V6_1, 191 | } 192 | 193 | impl DjangoVersion { 194 | /// Current Django version. 195 | pub const CURRENT: Self = Self::V5_2; 196 | } 197 | 198 | /// Resolves the number of iterations based on the Algorithm and the Django Version. 199 | #[allow(unused_variables)] 200 | fn iterations(version: &DjangoVersion, algorithm: &Algorithm) -> u32 { 201 | match *algorithm { 202 | #[cfg(feature = "with_bcrypt")] 203 | Algorithm::BCryptSHA256 | Algorithm::BCrypt => 12, 204 | #[cfg(feature = "with_pbkdf2")] 205 | Algorithm::PBKDF2 | Algorithm::PBKDF2SHA1 => match *version { 206 | DjangoVersion::V1_4 | DjangoVersion::V1_5 => 10_000, 207 | DjangoVersion::V1_6 | DjangoVersion::V1_7 => 12_000, 208 | DjangoVersion::V1_8 => 20_000, 209 | DjangoVersion::V1_9 => 24_000, 210 | DjangoVersion::V1_10 => 30_000, 211 | DjangoVersion::V1_11 => 36_000, 212 | DjangoVersion::V2_0 => 100_000, 213 | DjangoVersion::V2_1 => 120_000, 214 | DjangoVersion::V2_2 => 150_000, 215 | DjangoVersion::V3_0 => 180_000, 216 | DjangoVersion::V3_1 => 216_000, 217 | DjangoVersion::V3_2 => 260_000, 218 | DjangoVersion::V4_0 => 320_000, 219 | DjangoVersion::V4_1 => 390_000, 220 | DjangoVersion::V4_2 => 600_000, 221 | DjangoVersion::V5_0 => 720_000, 222 | DjangoVersion::V5_1 => 870_000, 223 | DjangoVersion::V5_2 => 1_000_000, 224 | DjangoVersion::V6_0 => 1_200_000, 225 | DjangoVersion::V6_1 => 1_500_000, 226 | }, 227 | #[cfg(feature = "with_argon2")] 228 | Algorithm::Argon2 => match *version { 229 | // For Argon2, this means "Profile 1", not actually "1 integration". 230 | DjangoVersion::V1_4 231 | | DjangoVersion::V1_5 232 | | DjangoVersion::V1_6 233 | | DjangoVersion::V1_7 234 | | DjangoVersion::V1_8 235 | | DjangoVersion::V1_9 236 | | DjangoVersion::V1_10 237 | | DjangoVersion::V1_11 238 | | DjangoVersion::V2_0 239 | | DjangoVersion::V2_1 240 | | DjangoVersion::V2_2 241 | | DjangoVersion::V3_0 242 | | DjangoVersion::V3_1 => 1, 243 | _ => 2, 244 | }, 245 | #[cfg(feature = "with_scrypt")] 246 | Algorithm::Scrypt => 1, 247 | #[cfg(feature = "with_legacy")] 248 | Algorithm::SHA1 249 | | Algorithm::MD5 250 | | Algorithm::UnsaltedSHA1 251 | | Algorithm::UnsaltedMD5 252 | | Algorithm::Crypt => 1, 253 | } 254 | } 255 | 256 | /// Generates a random salt. 257 | fn random_salt() -> String { 258 | rand::rng() 259 | .sample_iter(&Alphanumeric) 260 | .take(12) 261 | .map(|x| x as char) 262 | .collect() 263 | } 264 | 265 | lazy_static! { 266 | pub static ref VALID_SALT_RE: Regex = Regex::new(r"^[A-Za-z0-9]*$").unwrap(); 267 | } 268 | 269 | /// Core function that generates all combinations of passwords: 270 | pub fn make_password_core( 271 | password: &str, 272 | salt: &str, 273 | algorithm: Algorithm, 274 | version: DjangoVersion, 275 | ) -> String { 276 | assert!( 277 | VALID_SALT_RE.is_match(salt), 278 | "Salt can only contain letters and numbers." 279 | ); 280 | let hasher = get_hasher(&algorithm); 281 | hasher.encode(password, salt, iterations(&version, &algorithm)) 282 | } 283 | 284 | /// Based on the current Django version, generates an encoded hash given 285 | /// a complete set of parameters: password, salt and algorithm. 286 | pub fn make_password_with_settings(password: &str, salt: &str, algorithm: Algorithm) -> String { 287 | make_password_core(password, salt, algorithm, DjangoVersion::CURRENT) 288 | } 289 | 290 | /// Based on the current Django version, generates an encoded hash given 291 | /// a password and algorithm, uses a random salt. 292 | pub fn make_password_with_algorithm(password: &str, algorithm: Algorithm) -> String { 293 | make_password_core(password, &random_salt(), algorithm, DjangoVersion::CURRENT) 294 | } 295 | 296 | mod features { 297 | use super::Algorithm; 298 | 299 | #[cfg(feature = "with_pbkdf2")] 300 | pub const PREFERRED_ALGORITHM: Algorithm = Algorithm::PBKDF2; 301 | 302 | #[cfg(all(not(feature = "with_pbkdf2"), feature = "with_bcrypt"))] 303 | pub const PREFERRED_ALGORITHM: Algorithm = Algorithm::BCryptSHA256; 304 | 305 | #[cfg(all( 306 | not(feature = "with_pbkdf2"), 307 | not(feature = "with_bcrypt"), 308 | feature = "with_argon2" 309 | ))] 310 | pub const PREFERRED_ALGORITHM: Algorithm = Algorithm::Argon2; 311 | 312 | #[cfg(all( 313 | not(feature = "with_pbkdf2"), 314 | not(feature = "with_bcrypt"), 315 | not(feature = "with_argon2"), 316 | feature = "with_scrypt" 317 | ))] 318 | pub const PREFERRED_ALGORITHM: Algorithm = Algorithm::Scrypt; 319 | 320 | #[cfg(all( 321 | not(feature = "with_pbkdf2"), 322 | not(feature = "with_bcrypt"), 323 | not(feature = "with_argon2"), 324 | not(feature = "with_scrypt"), 325 | feature = "with_legacy" 326 | ))] 327 | pub const PREFERRED_ALGORITHM: Algorithm = Algorithm::SHA1; 328 | 329 | #[cfg(all( 330 | not(feature = "with_pbkdf2"), 331 | not(feature = "with_bcrypt"), 332 | not(feature = "with_argon2"), 333 | not(feature = "with_legacy"), 334 | not(feature = "with_scrypt"), 335 | ))] 336 | compile_error!( 337 | r#"At least one of the crypto features ("with_pbkdf2", "with_bcrypt", "with_argon2", "with_scrypt" or "with_legacy") must be selected."# 338 | ); 339 | } 340 | 341 | /// Based on the current Django version, generates an encoded hash given 342 | /// only a password, uses a random salt and the PBKDF2 algorithm. 343 | pub fn make_password(password: &str) -> String { 344 | make_password_core( 345 | password, 346 | &random_salt(), 347 | features::PREFERRED_ALGORITHM, 348 | DjangoVersion::CURRENT, 349 | ) 350 | } 351 | 352 | /// Abstraction that exposes the functions that generates 353 | /// passwords compliant with different Django versions. 354 | /// 355 | /// # Example: 356 | /// 357 | /// ``` 358 | /// let django = Django {version: DjangoVersion::V19}; 359 | /// let encoded = django.make_password("KRONOS"); 360 | /// ``` 361 | pub struct Django { 362 | /// Django Version. 363 | pub version: DjangoVersion, 364 | } 365 | 366 | impl Django { 367 | /// Based on the defined Django version, generates an encoded hash given 368 | /// a complete set of parameters: password, salt and algorithm. 369 | pub fn make_password_with_settings( 370 | &self, 371 | password: &str, 372 | salt: &str, 373 | algorithm: Algorithm, 374 | ) -> String { 375 | make_password_core(password, salt, algorithm, self.version.clone()) 376 | } 377 | 378 | /// Based on the defined Django version, generates an encoded hash given 379 | /// a password and algorithm, uses a random salt. 380 | pub fn make_password_with_algorithm(&self, password: &str, algorithm: Algorithm) -> String { 381 | make_password_core(password, &random_salt(), algorithm, self.version.clone()) 382 | } 383 | 384 | /// Based on the defined Django version, generates an encoded hash given 385 | /// only a password, uses a random salt and the PBKDF2 algorithm. 386 | pub fn make_password(&self, password: &str) -> String { 387 | make_password_core( 388 | password, 389 | &random_salt(), 390 | features::PREFERRED_ALGORITHM, 391 | self.version.clone(), 392 | ) 393 | } 394 | } 395 | 396 | #[test] 397 | fn test_identify_hasher() { 398 | // Good hashes: 399 | #[cfg(feature = "with_pbkdf2")] 400 | assert_eq!( 401 | identify_hasher( 402 | "pbkdf2_sha256$24000$KQ8zeK6wKRuR$cmhbSt1XVKuO4FGd9+AX8qSBD4Z0395nZatXTJpEtTY=" 403 | ), 404 | Some(Algorithm::PBKDF2) 405 | ); 406 | #[cfg(feature = "with_pbkdf2")] 407 | assert_eq!( 408 | identify_hasher("pbkdf2_sha1$24000$KQ8zeK6wKRuR$tSJh4xdxfMJotlxfkCGjTFpGYZU="), 409 | Some(Algorithm::PBKDF2SHA1) 410 | ); 411 | #[cfg(feature = "with_legacy")] 412 | assert_eq!( 413 | identify_hasher("sha1$KQ8zeK6wKRuR$f83371bca01fa6089456e673ccfb17f42d810b00"), 414 | Some(Algorithm::SHA1) 415 | ); 416 | #[cfg(feature = "with_legacy")] 417 | assert_eq!( 418 | identify_hasher("md5$KQ8zeK6wKRuR$0137e4d74cb2d9ed9cb1a5f391f6175e"), 419 | Some(Algorithm::MD5) 420 | ); 421 | #[cfg(feature = "with_legacy")] 422 | assert_eq!( 423 | identify_hasher("7cf6409a82cd4c8b96a9ecf6ad679119"), 424 | Some(Algorithm::UnsaltedMD5) 425 | ); 426 | #[cfg(feature = "with_legacy")] 427 | assert_eq!( 428 | identify_hasher("md5$$7cf6409a82cd4c8b96a9ecf6ad679119"), 429 | Some(Algorithm::MD5) 430 | ); 431 | #[cfg(feature = "with_legacy")] 432 | assert_eq!( 433 | identify_hasher("sha1$$22e6217f026c7a395f0840c1ffbdb163072419e7"), 434 | Some(Algorithm::UnsaltedSHA1) 435 | ); 436 | #[cfg(feature = "with_bcrypt")] 437 | assert_eq!( 438 | identify_hasher( 439 | "bcrypt_sha256$$2b$12$LZSJchsWG/DrBy1erNs4eeYo6tZNlLFQmONdxN9HPesa1EyXVcTXK" 440 | ), 441 | Some(Algorithm::BCryptSHA256) 442 | ); 443 | #[cfg(feature = "with_bcrypt")] 444 | assert_eq!( 445 | identify_hasher("bcrypt$$2b$12$LZSJchsWG/DrBy1erNs4ee31eJ7DaWiuwhDOC7aqIyqGGggfu6Y/."), 446 | Some(Algorithm::BCrypt) 447 | ); 448 | #[cfg(feature = "with_legacy")] 449 | assert_eq!( 450 | identify_hasher("crypt$$ab1Hv2Lg7ltQo"), 451 | Some(Algorithm::Crypt) 452 | ); 453 | #[cfg(feature = "with_argon2")] 454 | assert_eq!( 455 | identify_hasher( 456 | "argon2$argon2i$v=19$m=512,t=2,p=2$MktOZjRsaTBNWnVp$/s1VqdEUfHOPKJyIokwa2A" 457 | ), 458 | Some(Algorithm::Argon2) 459 | ); 460 | #[cfg(feature = "with_scrypt")] 461 | assert_eq!( 462 | identify_hasher( 463 | "scrypt$16384$seasalt$8$1$Qj3+9PPyRjSJIebHnG81TMjsqtaIGxNQG/aEB/NYafTJ7tibgfYz71m0ldQESkXFRkdVCBhhY8mx7rQwite/Pw==" 464 | ), 465 | Some(Algorithm::Scrypt) 466 | ); 467 | 468 | // Bad hashes: 469 | assert!(identify_hasher("").is_none()); 470 | assert!(identify_hasher("password").is_none()); 471 | assert!(identify_hasher("7cf6409a82cd4c8b96a9ecf6ad6791190").is_none()); 472 | assert!( 473 | identify_hasher("blah$KQ8zeK6wKRuR$f83371bca01fa6089456e673ccfb17f42d810b00").is_none() 474 | ); 475 | } 476 | 477 | #[test] 478 | #[should_panic] 479 | #[cfg(feature = "with_pbkdf2")] 480 | fn test_invalid_salt_should_panic() { 481 | let _ = make_password_core("pass", "$alt", Algorithm::PBKDF2, DjangoVersion::CURRENT); 482 | } 483 | --------------------------------------------------------------------------------