├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── bcrypt.rs ├── examples ├── bare-metal │ ├── .cargo │ │ └── config.toml │ ├── Cargo.toml │ ├── README.md │ ├── build.rs │ ├── memory.x │ └── src │ │ └── main.rs └── hash.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── hash.rs │ └── verify.rs └── src ├── bcrypt.rs ├── errors.rs └── lib.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | test: 9 | name: test 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | rust: [1.63.0, stable] 14 | 15 | include: 16 | - rust: 1.63.0 17 | test_no_std: false 18 | - rust: 1.63.0 19 | test_no_std: true 20 | - rust: stable 21 | test_no_std: true 22 | fail-fast: false 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v3 27 | 28 | - name: Install Rust 29 | uses: dtolnay/rust-toolchain@master 30 | with: 31 | toolchain: ${{ matrix.rust }} 32 | components: rustfmt 33 | 34 | - name: Check without features 35 | run: cargo check --no-default-features 36 | 37 | - name: Run tests 38 | run: cargo test 39 | 40 | - name: Run tests using no_std 41 | if: matrix.test_no_std == true 42 | run: cargo test --no-default-features --features alloc 43 | 44 | bare-metal: 45 | # tests no alloc, no_std, no getrandom 46 | name: bare-metal 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - name: Checkout repository 51 | uses: actions/checkout@v3 52 | 53 | - name: Install Rust 54 | uses: dtolnay/rust-toolchain@master 55 | with: 56 | toolchain: stable 57 | targets: thumbv6m-none-eabi 58 | 59 | - name: Build 60 | run: cd examples/bare-metal; cargo build 61 | 62 | fmt: 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v3 67 | 68 | - uses: dtolnay/rust-toolchain@master 69 | with: 70 | toolchain: stable 71 | 72 | - run: cargo fmt --all -- --check 73 | 74 | clippy: 75 | runs-on: ubuntu-latest 76 | steps: 77 | - name: Checkout 78 | uses: actions/checkout@v3 79 | 80 | - uses: dtolnay/rust-toolchain@master 81 | with: 82 | toolchain: 1.63.0 83 | components: clippy 84 | 85 | - run: cargo clippy -- -D warnings 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .idea/ 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bcrypt" 3 | version = "0.17.0" 4 | authors = ["Vincent Prouillet "] 5 | license = "MIT" 6 | readme = "README.md" 7 | description = "Easily hash and verify passwords using bcrypt" 8 | homepage = "https://github.com/Keats/rust-bcrypt" 9 | repository = "https://github.com/Keats/rust-bcrypt" 10 | keywords = ["bcrypt", "password", "web", "hash"] 11 | edition = "2021" 12 | include = ["src/**/*", "LICENSE", "README.md"] 13 | 14 | [features] 15 | default = ["std", "zeroize"] 16 | std = ["getrandom/std", "base64/std"] 17 | alloc = ["base64/alloc", "getrandom"] 18 | 19 | [dependencies] 20 | blowfish = { version = "0.9", features = ["bcrypt"] } 21 | getrandom = { version = "0.3", default-features = false, optional = true } 22 | base64 = { version = "0.22", default-features = false } 23 | zeroize = { version = "1.5.4", optional = true } 24 | subtle = { version = "2.4.1", default-features = false } 25 | 26 | [dev-dependencies] 27 | # no default features avoid pulling in log 28 | quickcheck = { version = "1", default-features = false } 29 | 30 | [badges] 31 | maintenance = { status = "passively-maintained" } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Vincent Prouillet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bcrypt 2 | 3 | [![Safety Dance](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) 4 | [![Build Status](https://travis-ci.org/Keats/rust-bcrypt.svg)](https://travis-ci.org/Keats/rust-bcrypt) 5 | [![Documentation](https://docs.rs/bcrypt/badge.svg)](https://docs.rs/bcrypt) 6 | 7 | ## Installation 8 | Add the following to Cargo.toml: 9 | 10 | ```toml 11 | bcrypt = "0.17" 12 | ``` 13 | 14 | The minimum Rust version is 1.63.0. 15 | 16 | ## Usage 17 | The crate makes 3 things public: `DEFAULT_COST`, `hash`, `verify`. 18 | 19 | ```rust 20 | extern crate bcrypt; 21 | 22 | use bcrypt::{DEFAULT_COST, hash, verify}; 23 | 24 | let hashed = hash("hunter2", DEFAULT_COST)?; 25 | let valid = verify("hunter2", &hashed)?; 26 | ``` 27 | 28 | The cost needs to be an integer between 4 and 31 (see benchmarks to have an idea of the speed for each), the `DEFAULT_COST` is 12. 29 | 30 | ## Error on truncation 31 | Most if not all bcrypt implementation truncates the password after 72 bytes. In specific use cases this can break 2nd pre-image resistance. 32 | One can enforce the 72-bytes limit on input by using `non_truncating_hash`, `non_truncating_hash_with_result`, `non_truncating_hash_with_salt`, and `non_truncating_verify`. 33 | The `non_truncating_*` functions behave identically to their truncating counterparts unless the input is longer than 72 bytes, in which case they will return `BcryptError::Truncation`. 34 | 35 | If you are generating hashes from other libraries/languages, do not use the `non_truncating_verify` function. 36 | 37 | ## `no_std` 38 | 39 | `bcrypt` crate supports `no_std` platforms. When `alloc` feature is enabled, 40 | all crate functionality is available. When `alloc` is not enabled only the 41 | raw `bcrypt()` function is usable. 42 | 43 | ## Benchmarks 44 | Speed depends on the cost used: the highest the slowest. 45 | Here are some benchmarks on a 2019 Macbook Pro to give you some ideas on the cost/speed ratio. 46 | Note that I don't go above 14 as it takes too long. 47 | 48 | ``` 49 | test bench_cost_10 ... bench: 51,474,665 ns/iter (+/- 16,006,581) 50 | test bench_cost_14 ... bench: 839,109,086 ns/iter (+/- 274,507,463) 51 | test bench_cost_4 ... bench: 795,814 ns/iter (+/- 42,838) 52 | test bench_cost_default ... bench: 195,344,338 ns/iter (+/- 8,329,675) 53 | ``` 54 | 55 | ## Acknowledgments 56 | This [gist](https://gist.github.com/rgdmarshall/ae3dc072445ed88b357a) for the hash splitting and the null termination. 57 | 58 | ## Recommendations 59 | While bcrypt works well as an algorithm, using something like [Argon2](https://en.wikipedia.org/wiki/Argon2) is recommended 60 | for new projects. 61 | 62 | ## Changelog 63 | 64 | * 0.17.0: update getrandom and remove `js` featuree 65 | * 0.16.0: add `non_truncating_*` functions 66 | * 0.15.1: update base64 dependency 67 | * 0.15.0: add an `alloc` feature that can be disabled. 68 | * 0.14.0: use `subtle` crate for constant time comparison, update base64 and bump to 2021 edition 69 | * 0.13.0: make zeroize dep opt-out and use fixed salt length 70 | * 0.12.1: zero vec containing password in the hashing function before returning the hash 71 | * 0.12.0: allow null bytes in password 72 | * 0.11.0: update deps causing big bump in MSRV 73 | * 0.10.1: fix panic with invalid hashes and allow `2x` 74 | * 0.10.0: update blowfish to 0.8 and minimum Rust version to 1.43.0. 75 | * 0.9.0: update base64 to 0.13 and getrandom to 0.2 76 | * 0.8.2: fix no-std build 77 | * 0.8.0: constant time verification for hash, remove custom base64 code from repo and add `std` feature 78 | * 0.7.0: add HashParts::from_str and remove Error::description impl, it's deprecated 79 | * 0.6.3: add `hash_with_salt` function and make `Version::format_for_version` public 80 | * 0.6.2: update base64 to 0.12 81 | * 0.6.1: update base64 to 0.11 82 | * 0.6.0: allow users to choose the bcrypt version and default to 2b instead of 2y 83 | * 0.5.0: expose the inner `bcrypt` function + edition 2018 84 | * 0.4.0: make DEFAULT_COST const instead of static 85 | * 0.3.0: forbid NULL bytes in passwords & update dependencies 86 | * 0.2.2: update rand 87 | * 0.2.1: update rand 88 | * 0.2.0: replace rust-crypto with blowfish, use some more modern Rust things like `?` and handle more errors 89 | * 0.1.6: update rand and base64 deps 90 | * 0.1.5: update lazy-static to 1.0 91 | * 0.1.4: Replace rustc-serialize dependency with bcrypt 92 | * 0.1.3: Fix panic when password > 72 chars 93 | * 0.1.1: make BcryptResult, BcryptError public and update dependencies 94 | * 0.1.0: initial release 95 | -------------------------------------------------------------------------------- /benches/bcrypt.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate bcrypt; 3 | extern crate test; 4 | 5 | use bcrypt::{hash, DEFAULT_COST}; 6 | 7 | #[bench] 8 | fn bench_cost_4(b: &mut test::Bencher) { 9 | b.iter(|| hash("hunter2", 4)); 10 | } 11 | 12 | #[bench] 13 | fn bench_cost_10(b: &mut test::Bencher) { 14 | b.iter(|| hash("hunter2", 10)); 15 | } 16 | 17 | #[bench] 18 | fn bench_cost_default(b: &mut test::Bencher) { 19 | b.iter(|| hash("hunter2", DEFAULT_COST)); 20 | } 21 | 22 | #[bench] 23 | fn bench_cost_14(b: &mut test::Bencher) { 24 | b.iter(|| hash("hunter2", 14)); 25 | } 26 | -------------------------------------------------------------------------------- /examples/bare-metal/.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Cortex-M0 and Cortex-M0+ 3 | target = "thumbv6m-none-eabi" 4 | -------------------------------------------------------------------------------- /examples/bare-metal/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | name = "bcrypt-bare-metal-test" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | bcrypt = { path = "../../", default-features = false } 8 | 9 | cortex-m-rt = "0.6.10" 10 | panic-halt = "0.2.0" 11 | -------------------------------------------------------------------------------- /examples/bare-metal/README.md: -------------------------------------------------------------------------------- 1 | Bare metal build test 2 | == 3 | 4 | Use by CI to ensure that `bcrypt` with `no-default-features` can work 5 | in a bare metal program. The platform doesn't have any of 6 | `std`, `alloc`, `getrandom`. 7 | 8 | Based on https://github.com/rust-embedded/cortex-m-quickstart 9 | under a MIT license. 10 | -------------------------------------------------------------------------------- /examples/bare-metal/build.rs: -------------------------------------------------------------------------------- 1 | //! This build script copies the `memory.x` file from the crate root into 2 | //! a directory where the linker can always find it at build time. 3 | //! For many projects this is optional, as the linker always searches the 4 | //! project root directory -- wherever `Cargo.toml` is. However, if you 5 | //! are using a workspace or have a more complicated build setup, this 6 | //! build script becomes required. Additionally, by requesting that 7 | //! Cargo re-run the build script whenever `memory.x` is changed, 8 | //! updating `memory.x` ensures a rebuild of the application with the 9 | //! new memory settings. 10 | //! 11 | //! The build script also sets the linker flags to tell it which link script to use. 12 | 13 | use std::env; 14 | use std::fs::File; 15 | use std::io::Write; 16 | use std::path::PathBuf; 17 | 18 | fn main() { 19 | // Put `memory.x` in our output directory and ensure it's 20 | // on the linker search path. 21 | let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); 22 | File::create(out.join("memory.x")) 23 | .unwrap() 24 | .write_all(include_bytes!("memory.x")) 25 | .unwrap(); 26 | println!("cargo:rustc-link-search={}", out.display()); 27 | 28 | // By default, Cargo will re-run a build script whenever 29 | // any file in the project changes. By specifying `memory.x` 30 | // here, we ensure the build script is only re-run when 31 | // `memory.x` is changed. 32 | println!("cargo:rerun-if-changed=memory.x"); 33 | 34 | // Specify linker arguments. 35 | 36 | // `--nmagic` is required if memory section addresses are not aligned to 0x10000, 37 | // for example the FLASH and RAM sections in your `memory.x`. 38 | // See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 39 | println!("cargo:rustc-link-arg=--nmagic"); 40 | 41 | // Set the linker script to the one provided by cortex-m-rt. 42 | println!("cargo:rustc-link-arg=-Tlink.x"); 43 | } 44 | -------------------------------------------------------------------------------- /examples/bare-metal/memory.x: -------------------------------------------------------------------------------- 1 | MEMORY 2 | { 3 | /* These values correspond to the LM3S6965, one of the few devices QEMU can emulate */ 4 | FLASH : ORIGIN = 0x00000000, LENGTH = 256K 5 | RAM : ORIGIN = 0x20000000, LENGTH = 64K 6 | } 7 | -------------------------------------------------------------------------------- /examples/bare-metal/src/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | 4 | use panic_halt as _; 5 | use cortex_m_rt::entry; 6 | 7 | #[entry] 8 | fn main() -> ! { 9 | let salt = [1u8; 16]; 10 | let _crypt = bcrypt::bcrypt(6, salt, b"password"); 11 | loop {} 12 | } 13 | -------------------------------------------------------------------------------- /examples/hash.rs: -------------------------------------------------------------------------------- 1 | extern crate bcrypt; 2 | 3 | #[cfg(any(feature = "alloc", feature = "std"))] 4 | use bcrypt::{hash, verify, DEFAULT_COST}; 5 | 6 | #[cfg(any(feature = "alloc", feature = "std"))] 7 | fn main() { 8 | let hashed = hash("hunter2", DEFAULT_COST).unwrap(); 9 | let valid = verify("hunter2", &hashed).unwrap(); 10 | println!("{:?}", valid); 11 | } 12 | 13 | #[cfg(not(any(feature = "alloc", feature = "std")))] 14 | fn main() {} 15 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "bcrypt-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.bcrypt] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "verify" 24 | path = "fuzz_targets/verify.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "hash" 30 | path = "fuzz_targets/hash.rs" 31 | test = false 32 | doc = false 33 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/hash.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &str| { 5 | let _ = bcrypt::hash(&data, 4); 6 | }); 7 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/verify.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | let _ = bcrypt::hash(&data, 4); 6 | }); 7 | -------------------------------------------------------------------------------- /src/bcrypt.rs: -------------------------------------------------------------------------------- 1 | use blowfish::Blowfish; 2 | 3 | fn setup(cost: u32, salt: &[u8], key: &[u8]) -> Blowfish { 4 | assert!(cost < 32); 5 | let mut state = Blowfish::bc_init_state(); 6 | 7 | state.salted_expand_key(salt, key); 8 | for _ in 0..1u32 << cost { 9 | state.bc_expand_key(key); 10 | state.bc_expand_key(salt); 11 | } 12 | 13 | state 14 | } 15 | 16 | pub fn bcrypt(cost: u32, salt: [u8; 16], password: &[u8]) -> [u8; 24] { 17 | assert!(!password.is_empty() && password.len() <= 72); 18 | 19 | let mut output = [0; 24]; 20 | 21 | let state = setup(cost, &salt, password); 22 | // OrpheanBeholderScryDoubt 23 | #[allow(clippy::unreadable_literal)] 24 | let mut ctext = [ 25 | 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274, 26 | ]; 27 | for i in 0..3 { 28 | let i: usize = i * 2; 29 | for _ in 0..64 { 30 | let [l, r] = state.bc_encrypt([ctext[i], ctext[i + 1]]); 31 | ctext[i] = l; 32 | ctext[i + 1] = r; 33 | } 34 | 35 | let buf = ctext[i].to_be_bytes(); 36 | output[i * 4..][..4].copy_from_slice(&buf); 37 | let buf = ctext[i + 1].to_be_bytes(); 38 | output[(i + 1) * 4..][..4].copy_from_slice(&buf); 39 | } 40 | 41 | output 42 | } 43 | 44 | #[cfg(test)] 45 | mod tests { 46 | use super::bcrypt; 47 | 48 | #[test] 49 | fn raw_bcrypt() { 50 | // test vectors unbase64ed from 51 | // https://github.com/djmdjm/jBCrypt/blob/master/test/org/mindrot/jbcrypt/TestBCrypt.java 52 | 53 | // $2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s. 54 | let pw = b"\0"; 55 | let cost = 6; 56 | let salt = [ 57 | 0x14, 0x4b, 0x3d, 0x69, 0x1a, 0x7b, 0x4e, 0xcf, 0x39, 0xcf, 0x73, 0x5c, 0x7f, 0xa7, 58 | 0xa7, 0x9c, 59 | ]; 60 | let result = [ 61 | 0x55, 0x7e, 0x94, 0xf3, 0x4b, 0xf2, 0x86, 0xe8, 0x71, 0x9a, 0x26, 0xbe, 0x94, 0xac, 62 | 0x1e, 0x16, 0xd9, 0x5e, 0xf9, 0xf8, 0x19, 0xde, 0xe0, 63 | ]; 64 | assert_eq!(bcrypt(cost, salt, pw)[..23], result); 65 | 66 | // $2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe 67 | let pw = b"a\0"; 68 | let cost = 6; 69 | let salt = [ 70 | 0xa3, 0x61, 0x2d, 0x8c, 0x9a, 0x37, 0xda, 0xc2, 0xf9, 0x9d, 0x94, 0xda, 0x3, 0xbd, 71 | 0x45, 0x21, 72 | ]; 73 | let result = [ 74 | 0xe6, 0xd5, 0x38, 0x31, 0xf8, 0x20, 0x60, 0xdc, 0x8, 0xa2, 0xe8, 0x48, 0x9c, 0xe8, 75 | 0x50, 0xce, 0x48, 0xfb, 0xf9, 0x76, 0x97, 0x87, 0x38, 76 | ]; 77 | assert_eq!(bcrypt(cost, salt, pw)[..23], result); 78 | 79 | // // $2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz. 80 | let pw = b"abcdefghijklmnopqrstuvwxyz\0"; 81 | let cost = 8; 82 | let salt = [ 83 | 0x71, 0x5b, 0x96, 0xca, 0xed, 0x2a, 0xc9, 0x2c, 0x35, 0x4e, 0xd1, 0x6c, 0x1e, 0x19, 84 | 0xe3, 0x8a, 85 | ]; 86 | let result = [ 87 | 0x98, 0xbf, 0x9f, 0xfc, 0x1f, 0x5b, 0xe4, 0x85, 0xf9, 0x59, 0xe8, 0xb1, 0xd5, 0x26, 88 | 0x39, 0x2f, 0xbd, 0x4e, 0xd2, 0xd5, 0x71, 0x9f, 0x50, 89 | ]; 90 | assert_eq!(bcrypt(cost, salt, pw)[..23], result); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "alloc", feature = "std"))] 2 | use alloc::string::String; 3 | use core::fmt; 4 | 5 | #[cfg(feature = "std")] 6 | use std::error; 7 | #[cfg(feature = "std")] 8 | use std::io; 9 | 10 | /// Library generic result type. 11 | pub type BcryptResult = Result; 12 | 13 | #[derive(Debug)] 14 | /// All the errors we can encounter while hashing/verifying 15 | /// passwords 16 | pub enum BcryptError { 17 | #[cfg(feature = "std")] 18 | Io(io::Error), 19 | CostNotAllowed(u32), 20 | #[cfg(any(feature = "alloc", feature = "std"))] 21 | InvalidCost(String), 22 | #[cfg(any(feature = "alloc", feature = "std"))] 23 | InvalidPrefix(String), 24 | #[cfg(any(feature = "alloc", feature = "std"))] 25 | InvalidHash(String), 26 | InvalidSaltLen(usize), 27 | InvalidBase64(base64::DecodeError), 28 | #[cfg(any(feature = "alloc", feature = "std"))] 29 | Rand(getrandom::Error), 30 | /// Return this error if the input contains more than 72 bytes. This variant contains the 31 | /// length of the input in bytes. 32 | /// Only returned when calling `non_truncating_*` functions 33 | Truncation(usize), 34 | } 35 | 36 | macro_rules! impl_from_error { 37 | ($f: ty, $e: expr) => { 38 | impl From<$f> for BcryptError { 39 | fn from(f: $f) -> BcryptError { 40 | $e(f) 41 | } 42 | } 43 | }; 44 | } 45 | 46 | impl_from_error!(base64::DecodeError, BcryptError::InvalidBase64); 47 | #[cfg(feature = "std")] 48 | impl_from_error!(io::Error, BcryptError::Io); 49 | #[cfg(any(feature = "alloc", feature = "std"))] 50 | impl_from_error!(getrandom::Error, BcryptError::Rand); 51 | 52 | impl fmt::Display for BcryptError { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 54 | match *self { 55 | #[cfg(feature = "std")] 56 | BcryptError::Io(ref err) => write!(f, "IO error: {}", err), 57 | #[cfg(any(feature = "alloc", feature = "std"))] 58 | BcryptError::InvalidCost(ref cost) => write!(f, "Invalid Cost: {}", cost), 59 | BcryptError::CostNotAllowed(ref cost) => write!( 60 | f, 61 | "Cost needs to be between {} and {}, got {}", 62 | crate::MIN_COST, 63 | crate::MAX_COST, 64 | cost 65 | ), 66 | #[cfg(any(feature = "alloc", feature = "std"))] 67 | BcryptError::InvalidPrefix(ref prefix) => write!(f, "Invalid Prefix: {}", prefix), 68 | #[cfg(any(feature = "alloc", feature = "std"))] 69 | BcryptError::InvalidHash(ref hash) => write!(f, "Invalid hash: {}", hash), 70 | BcryptError::InvalidBase64(ref err) => write!(f, "Base64 error: {}", err), 71 | BcryptError::InvalidSaltLen(len) => { 72 | write!(f, "Invalid salt len: expected 16, received {}", len) 73 | } 74 | #[cfg(any(feature = "alloc", feature = "std"))] 75 | BcryptError::Rand(ref err) => write!(f, "Rand error: {}", err), 76 | BcryptError::Truncation(len) => { 77 | write!(f, "Expected 72 bytes or fewer; found {len} bytes") 78 | } 79 | } 80 | } 81 | } 82 | 83 | #[cfg(feature = "std")] 84 | impl error::Error for BcryptError { 85 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 86 | match *self { 87 | BcryptError::Io(ref err) => Some(err), 88 | BcryptError::InvalidCost(_) 89 | | BcryptError::CostNotAllowed(_) 90 | | BcryptError::InvalidPrefix(_) 91 | | BcryptError::InvalidHash(_) 92 | | BcryptError::InvalidSaltLen(_) 93 | | BcryptError::Truncation(_) => None, 94 | BcryptError::InvalidBase64(ref err) => Some(err), 95 | BcryptError::Rand(ref err) => Some(err), 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Easily hash and verify passwords using bcrypt 2 | #![forbid(unsafe_code)] 3 | #![cfg_attr(not(feature = "std"), no_std)] 4 | 5 | #[cfg(any(feature = "alloc", feature = "std", test))] 6 | extern crate alloc; 7 | 8 | #[cfg(any(feature = "alloc", feature = "std", test))] 9 | use alloc::{ 10 | string::{String, ToString}, 11 | vec::Vec, 12 | }; 13 | 14 | #[cfg(feature = "zeroize")] 15 | use zeroize::Zeroize; 16 | 17 | use base64::{alphabet::BCRYPT, engine::general_purpose::NO_PAD, engine::GeneralPurpose}; 18 | use core::fmt; 19 | #[cfg(any(feature = "alloc", feature = "std"))] 20 | use {base64::Engine, core::convert::AsRef, core::str::FromStr}; 21 | 22 | mod bcrypt; 23 | mod errors; 24 | 25 | pub use crate::bcrypt::bcrypt; 26 | pub use crate::errors::{BcryptError, BcryptResult}; 27 | 28 | // Cost constants 29 | const MIN_COST: u32 = 4; 30 | const MAX_COST: u32 = 31; 31 | pub const DEFAULT_COST: u32 = 12; 32 | pub const BASE_64: GeneralPurpose = GeneralPurpose::new(&BCRYPT, NO_PAD); 33 | 34 | #[cfg(any(feature = "alloc", feature = "std"))] 35 | #[derive(Debug, PartialEq, Eq)] 36 | /// A bcrypt hash result before concatenating 37 | pub struct HashParts { 38 | cost: u32, 39 | salt: String, 40 | hash: String, 41 | } 42 | 43 | #[derive(Clone, Debug)] 44 | /// BCrypt hash version 45 | /// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history 46 | pub enum Version { 47 | TwoA, 48 | TwoX, 49 | TwoY, 50 | TwoB, 51 | } 52 | 53 | #[cfg(any(feature = "alloc", feature = "std"))] 54 | impl HashParts { 55 | /// Creates the bcrypt hash string from all its parts 56 | fn format(self) -> String { 57 | self.format_for_version(Version::TwoB) 58 | } 59 | 60 | /// Get the bcrypt hash cost 61 | pub fn get_cost(&self) -> u32 { 62 | self.cost 63 | } 64 | 65 | /// Get the bcrypt hash salt 66 | pub fn get_salt(&self) -> String { 67 | self.salt.clone() 68 | } 69 | 70 | /// Creates the bcrypt hash string from all its part, allowing to customize the version. 71 | pub fn format_for_version(&self, version: Version) -> String { 72 | // Cost need to have a length of 2 so padding with a 0 if cost < 10 73 | alloc::format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash) 74 | } 75 | } 76 | 77 | #[cfg(any(feature = "alloc", feature = "std"))] 78 | impl FromStr for HashParts { 79 | type Err = BcryptError; 80 | 81 | fn from_str(s: &str) -> Result { 82 | split_hash(s) 83 | } 84 | } 85 | 86 | #[cfg(any(feature = "alloc", feature = "std"))] 87 | impl ToString for HashParts { 88 | fn to_string(&self) -> String { 89 | self.format_for_version(Version::TwoY) 90 | } 91 | } 92 | 93 | impl fmt::Display for Version { 94 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 95 | let str = match self { 96 | Version::TwoA => "2a", 97 | Version::TwoB => "2b", 98 | Version::TwoX => "2x", 99 | Version::TwoY => "2y", 100 | }; 101 | write!(f, "{}", str) 102 | } 103 | } 104 | 105 | /// The main meat: actually does the hashing and does some verification with 106 | /// the cost to ensure it's a correct one. If err_on_truncation, this method will return 107 | /// `BcryptError::Truncation`; otherwise it will truncate the password. 108 | #[cfg(any(feature = "alloc", feature = "std"))] 109 | fn _hash_password( 110 | password: &[u8], 111 | cost: u32, 112 | salt: [u8; 16], 113 | err_on_truncation: bool, 114 | ) -> BcryptResult { 115 | if !(MIN_COST..=MAX_COST).contains(&cost) { 116 | return Err(BcryptError::CostNotAllowed(cost)); 117 | } 118 | 119 | // Passwords need to be null terminated 120 | let mut vec = Vec::with_capacity(password.len() + 1); 121 | vec.extend_from_slice(password); 122 | vec.push(0); 123 | // We only consider the first 72 chars; truncate if necessary. 124 | // `bcrypt` below will panic if len > 72 125 | let truncated = if vec.len() > 72 { 126 | if err_on_truncation { 127 | return Err(BcryptError::Truncation(vec.len())); 128 | } 129 | &vec[..72] 130 | } else { 131 | &vec 132 | }; 133 | 134 | let output = bcrypt::bcrypt(cost, salt, truncated); 135 | 136 | #[cfg(feature = "zeroize")] 137 | vec.zeroize(); 138 | 139 | Ok(HashParts { 140 | cost, 141 | salt: BASE_64.encode(salt), 142 | hash: BASE_64.encode(&output[..23]), // remember to remove the last byte 143 | }) 144 | } 145 | 146 | /// Takes a full hash and split it into 3 parts: 147 | /// cost, salt and hash 148 | #[cfg(any(feature = "alloc", feature = "std"))] 149 | fn split_hash(hash: &str) -> BcryptResult { 150 | let mut parts = HashParts { 151 | cost: 0, 152 | salt: "".to_string(), 153 | hash: "".to_string(), 154 | }; 155 | 156 | // Should be [prefix, cost, hash] 157 | let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect(); 158 | 159 | if raw_parts.len() != 3 { 160 | return Err(BcryptError::InvalidHash(hash.to_string())); 161 | } 162 | 163 | if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" && raw_parts[0] != "2x" 164 | { 165 | return Err(BcryptError::InvalidPrefix(raw_parts[0].to_string())); 166 | } 167 | 168 | if let Ok(c) = raw_parts[1].parse::() { 169 | parts.cost = c; 170 | } else { 171 | return Err(BcryptError::InvalidCost(raw_parts[1].to_string())); 172 | } 173 | 174 | if raw_parts[2].len() == 53 && raw_parts[2].is_char_boundary(22) { 175 | parts.salt = raw_parts[2][..22].chars().collect(); 176 | parts.hash = raw_parts[2][22..].chars().collect(); 177 | } else { 178 | return Err(BcryptError::InvalidHash(hash.to_string())); 179 | } 180 | 181 | Ok(parts) 182 | } 183 | 184 | /// Generates a password hash using the cost given. 185 | /// The salt is generated randomly using the OS randomness 186 | #[cfg(any(feature = "alloc", feature = "std"))] 187 | pub fn hash>(password: P, cost: u32) -> BcryptResult { 188 | hash_with_result(password, cost).map(|r| r.format()) 189 | } 190 | 191 | /// Generates a password hash using the cost given. 192 | /// The salt is generated randomly using the OS randomness 193 | /// Will return BcryptError::Truncation if password is longer than 72 bytes 194 | #[cfg(any(feature = "alloc", feature = "std"))] 195 | pub fn non_truncating_hash>(password: P, cost: u32) -> BcryptResult { 196 | non_truncating_hash_with_result(password, cost).map(|r| r.format()) 197 | } 198 | 199 | /// Generates a password hash using the cost given. 200 | /// The salt is generated randomly using the OS randomness. 201 | /// The function returns a result structure and allows to format the hash in different versions. 202 | #[cfg(any(feature = "alloc", feature = "std"))] 203 | pub fn hash_with_result>(password: P, cost: u32) -> BcryptResult { 204 | let salt = { 205 | let mut s = [0u8; 16]; 206 | getrandom::fill(&mut s).map(|_| s) 207 | }?; 208 | 209 | _hash_password(password.as_ref(), cost, salt, false) 210 | } 211 | 212 | /// Generates a password hash using the cost given. 213 | /// The salt is generated randomly using the OS randomness. 214 | /// The function returns a result structure and allows to format the hash in different versions. 215 | /// Will return BcryptError::Truncation if password is longer than 72 bytes 216 | #[cfg(any(feature = "alloc", feature = "std"))] 217 | pub fn non_truncating_hash_with_result>( 218 | password: P, 219 | cost: u32, 220 | ) -> BcryptResult { 221 | let salt = { 222 | let mut s = [0u8; 16]; 223 | getrandom::fill(&mut s).map(|_| s) 224 | }?; 225 | 226 | _hash_password(password.as_ref(), cost, salt, true) 227 | } 228 | 229 | /// Generates a password given a hash and a cost. 230 | /// The function returns a result structure and allows to format the hash in different versions. 231 | #[cfg(any(feature = "alloc", feature = "std"))] 232 | pub fn hash_with_salt>( 233 | password: P, 234 | cost: u32, 235 | salt: [u8; 16], 236 | ) -> BcryptResult { 237 | _hash_password(password.as_ref(), cost, salt, false) 238 | } 239 | 240 | /// Generates a password given a hash and a cost. 241 | /// The function returns a result structure and allows to format the hash in different versions. 242 | /// Will return BcryptError::Truncation if password is longer than 72 bytes 243 | #[cfg(any(feature = "alloc", feature = "std"))] 244 | pub fn non_truncating_hash_with_salt>( 245 | password: P, 246 | cost: u32, 247 | salt: [u8; 16], 248 | ) -> BcryptResult { 249 | _hash_password(password.as_ref(), cost, salt, true) 250 | } 251 | 252 | /// Verify the password against the hash by extracting the salt from the hash and recomputing the 253 | /// hash from the password. If `err_on_truncation` is set to true, then this method will return 254 | /// `BcryptError::Truncation`. 255 | #[cfg(any(feature = "alloc", feature = "std"))] 256 | fn _verify>(password: P, hash: &str, err_on_truncation: bool) -> BcryptResult { 257 | use subtle::ConstantTimeEq; 258 | 259 | let parts = split_hash(hash)?; 260 | let salt = BASE_64.decode(&parts.salt)?; 261 | let salt_len = salt.len(); 262 | let generated = _hash_password( 263 | password.as_ref(), 264 | parts.cost, 265 | salt.try_into() 266 | .map_err(|_| BcryptError::InvalidSaltLen(salt_len))?, 267 | err_on_truncation, 268 | )?; 269 | let source_decoded = BASE_64.decode(parts.hash)?; 270 | let generated_decoded = BASE_64.decode(generated.hash)?; 271 | 272 | Ok(source_decoded.ct_eq(&generated_decoded).into()) 273 | } 274 | 275 | /// Verify that a password is equivalent to the hash provided 276 | #[cfg(any(feature = "alloc", feature = "std"))] 277 | pub fn verify>(password: P, hash: &str) -> BcryptResult { 278 | _verify(password, hash, false) 279 | } 280 | 281 | /// Verify that a password is equivalent to the hash provided. 282 | /// Only use this if you are only using `non_truncating_hash` to generate the hash. 283 | /// It will return an error for inputs that will work if generated from other sources. 284 | #[cfg(any(feature = "alloc", feature = "std"))] 285 | pub fn non_truncating_verify>(password: P, hash: &str) -> BcryptResult { 286 | _verify(password, hash, true) 287 | } 288 | 289 | #[cfg(all(test, any(feature = "alloc", feature = "std")))] 290 | mod tests { 291 | use crate::non_truncating_hash; 292 | 293 | use super::{ 294 | _hash_password, 295 | alloc::{ 296 | string::{String, ToString}, 297 | vec, 298 | vec::Vec, 299 | }, 300 | hash, hash_with_salt, non_truncating_verify, split_hash, verify, BcryptError, BcryptResult, 301 | HashParts, Version, DEFAULT_COST, 302 | }; 303 | use core::convert::TryInto; 304 | use core::iter; 305 | use core::str::FromStr; 306 | use quickcheck::{quickcheck, TestResult}; 307 | 308 | #[test] 309 | fn can_split_hash() { 310 | let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; 311 | let output = split_hash(hash).unwrap(); 312 | let expected = HashParts { 313 | cost: 12, 314 | salt: "L6Bc/AlTQHyd9liGgGEZyO".to_string(), 315 | hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u".to_string(), 316 | }; 317 | assert_eq!(output, expected); 318 | } 319 | 320 | #[test] 321 | fn can_output_cost_and_salt_from_parsed_hash() { 322 | let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; 323 | let parsed = HashParts::from_str(hash).unwrap(); 324 | assert_eq!(parsed.get_cost(), 12); 325 | assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string()); 326 | } 327 | 328 | #[test] 329 | fn returns_an_error_if_a_parsed_hash_is_baddly_formated() { 330 | let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; 331 | assert!(HashParts::from_str(hash1).is_err()); 332 | 333 | let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; 334 | assert!(HashParts::from_str(hash2).is_err()); 335 | 336 | let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; 337 | assert!(HashParts::from_str(hash3).is_err()); 338 | } 339 | 340 | #[test] 341 | fn can_verify_hash_generated_from_some_online_tool() { 342 | let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96"; 343 | assert!(verify("password", hash).unwrap()); 344 | } 345 | 346 | #[test] 347 | fn can_verify_hash_generated_from_python() { 348 | let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; 349 | assert!(verify("correctbatteryhorsestapler", hash).unwrap()); 350 | } 351 | 352 | #[test] 353 | fn can_verify_hash_generated_from_node() { 354 | let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi"; 355 | assert!(verify("correctbatteryhorsestapler", hash).unwrap()); 356 | } 357 | 358 | #[test] 359 | fn can_verify_hash_generated_from_go() { 360 | /* 361 | package main 362 | import ( 363 | "io" 364 | "os" 365 | "golang.org/x/crypto/bcrypt" 366 | ) 367 | func main() { 368 | buf, err := io.ReadAll(os.Stdin) 369 | if err != nil { 370 | panic(err) 371 | } 372 | out, err := bcrypt.GenerateFromPassword(buf, bcrypt.MinCost) 373 | if err != nil { 374 | panic(err) 375 | } 376 | os.Stdout.Write(out) 377 | os.Stdout.Write([]byte("\n")) 378 | } 379 | */ 380 | let binary_input = vec![ 381 | 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124, 382 | 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135, 383 | ]; 384 | let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w."; 385 | assert!(verify(binary_input, hash).unwrap()); 386 | } 387 | 388 | #[test] 389 | fn invalid_hash_does_not_panic() { 390 | let binary_input = vec![ 391 | 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124, 392 | 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135, 393 | ]; 394 | let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9a."; 395 | assert!(verify(binary_input, hash).is_err()); 396 | } 397 | 398 | #[test] 399 | fn a_wrong_password_is_false() { 400 | let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; 401 | assert!(!verify("wrong", hash).unwrap()); 402 | } 403 | 404 | #[test] 405 | fn errors_with_invalid_hash() { 406 | // there is another $ in the hash part 407 | let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; 408 | assert!(verify("correctbatteryhorsestapler", hash).is_err()); 409 | } 410 | 411 | #[test] 412 | fn errors_with_non_number_cost() { 413 | // the cost is not a number 414 | let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; 415 | assert!(verify("correctbatteryhorsestapler", hash).is_err()); 416 | } 417 | 418 | #[test] 419 | fn errors_with_a_hash_too_long() { 420 | // the cost is not a number 421 | let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri"; 422 | assert!(verify("correctbatteryhorsestapler", hash).is_err()); 423 | } 424 | 425 | #[test] 426 | fn can_verify_own_generated() { 427 | let hashed = hash("hunter2", 4).unwrap(); 428 | assert_eq!(true, verify("hunter2", &hashed).unwrap()); 429 | } 430 | 431 | #[test] 432 | fn long_passwords_truncate_correctly() { 433 | // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")' 434 | let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2"; 435 | assert!(verify(iter::repeat("x").take(100).collect::(), hash).unwrap()); 436 | } 437 | 438 | #[test] 439 | fn non_truncating_operations() { 440 | assert!(matches!( 441 | non_truncating_hash(iter::repeat("x").take(72).collect::(), DEFAULT_COST), 442 | BcryptResult::Err(BcryptError::Truncation(73)) 443 | )); 444 | assert!(matches!( 445 | non_truncating_hash(iter::repeat("x").take(71).collect::(), DEFAULT_COST), 446 | BcryptResult::Ok(_) 447 | )); 448 | 449 | let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2"; 450 | assert!(matches!( 451 | non_truncating_verify(iter::repeat("x").take(100).collect::(), hash), 452 | Err(BcryptError::Truncation(101)) 453 | )); 454 | } 455 | 456 | #[test] 457 | fn generate_versions() { 458 | let password = "hunter2".as_bytes(); 459 | let salt = vec![0; 16]; 460 | let result = 461 | _hash_password(password, DEFAULT_COST, salt.try_into().unwrap(), false).unwrap(); 462 | assert_eq!( 463 | "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", 464 | result.format_for_version(Version::TwoA) 465 | ); 466 | assert_eq!( 467 | "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", 468 | result.format_for_version(Version::TwoB) 469 | ); 470 | assert_eq!( 471 | "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", 472 | result.format_for_version(Version::TwoX) 473 | ); 474 | assert_eq!( 475 | "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", 476 | result.format_for_version(Version::TwoY) 477 | ); 478 | let hash = result.to_string(); 479 | assert_eq!(true, verify("hunter2", &hash).unwrap()); 480 | } 481 | 482 | #[test] 483 | fn allow_null_bytes() { 484 | // hash p1, check the hash against p2: 485 | fn hash_and_check(p1: &[u8], p2: &[u8]) -> Result { 486 | let fast_cost = 4; 487 | match hash(p1, fast_cost) { 488 | Ok(s) => verify(p2, &s), 489 | Err(e) => Err(e), 490 | } 491 | } 492 | fn assert_valid_password(p1: &[u8], p2: &[u8], expected: bool) { 493 | match hash_and_check(p1, p2) { 494 | Ok(checked) => { 495 | if checked != expected { 496 | panic!( 497 | "checked {:?} against {:?}, incorrect result {}", 498 | p1, p2, checked 499 | ) 500 | } 501 | } 502 | Err(e) => panic!("error evaluating password: {} for {:?}.", e, p1), 503 | } 504 | } 505 | 506 | // bcrypt should consider all of these distinct: 507 | let test_passwords = vec![ 508 | "\0", 509 | "passw0rd\0", 510 | "password\0with tail", 511 | "\0passw0rd", 512 | "a", 513 | "a\0", 514 | "a\0b\0", 515 | ]; 516 | 517 | for (i, p1) in test_passwords.iter().enumerate() { 518 | for (j, p2) in test_passwords.iter().enumerate() { 519 | assert_valid_password(p1.as_bytes(), p2.as_bytes(), i == j); 520 | } 521 | } 522 | 523 | // this is a quirk of the bcrypt algorithm: passwords that are entirely null 524 | // bytes hash to the same value, even if they are different lengths: 525 | assert_valid_password("\0\0\0\0\0\0\0\0".as_bytes(), "\0".as_bytes(), true); 526 | } 527 | 528 | #[test] 529 | fn hash_with_fixed_salt() { 530 | let salt = [ 531 | 38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18, 532 | ]; 533 | let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt) 534 | .unwrap() 535 | .to_string(); 536 | assert_eq!( 537 | "$2y$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe", 538 | &hashed 539 | ); 540 | } 541 | 542 | quickcheck! { 543 | fn can_verify_arbitrary_own_generated(pass: Vec) -> BcryptResult { 544 | let mut pass = pass; 545 | pass.retain(|&b| b != 0); 546 | let hashed = hash(&pass, 4)?; 547 | verify(pass, &hashed) 548 | } 549 | 550 | fn doesnt_verify_different_passwords(a: Vec, b: Vec) -> BcryptResult { 551 | let mut a = a; 552 | a.retain(|&b| b != 0); 553 | let mut b = b; 554 | b.retain(|&b| b != 0); 555 | if a == b { 556 | return Ok(TestResult::discard()); 557 | } 558 | let hashed = hash(a, 4)?; 559 | Ok(TestResult::from_bool(!verify(b, &hashed)?)) 560 | } 561 | } 562 | 563 | #[test] 564 | fn does_no_error_on_char_boundary_splitting() { 565 | // Just checks that it does not panic 566 | let _ = verify( 567 | &[], 568 | "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", 569 | ); 570 | } 571 | } 572 | --------------------------------------------------------------------------------