bcrypt-0.15.1/.cargo_vcs_info.json0000644000000001360000000000100124230ustar { "git": { "sha1": "b05e4f6097fac16cbb2c7a04d80fd53d20d37257" }, "path_in_vcs": "" }bcrypt-0.15.1/Cargo.lock0000644000000160110000000000100103750ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "base64" version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bcrypt" version = "0.15.1" dependencies = [ "base64", "blowfish", "getrandom", "quickcheck", "subtle", "zeroize", ] [[package]] name = "blowfish" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" dependencies = [ "byteorder", "cipher", ] [[package]] name = "bumpalo" version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", ] [[package]] name = "crypto-common" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", ] [[package]] name = "generic-array" version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", ] [[package]] name = "getrandom" version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "inout" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ "generic-array", ] [[package]] name = "js-sys" version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "log" version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "proc-macro2" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] [[package]] name = "quickcheck" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6" dependencies = [ "rand", ] [[package]] name = "quote" version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "subtle" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" bcrypt-0.15.1/Cargo.toml0000644000000030200000000000100104140ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "bcrypt" version = "0.15.1" authors = ["Vincent Prouillet "] include = [ "src/**/*", "LICENSE", "README.md", ] description = "Easily hash and verify passwords using bcrypt" homepage = "https://github.com/Keats/rust-bcrypt" readme = "README.md" keywords = [ "bcrypt", "password", "web", "hash", ] license = "MIT" repository = "https://github.com/Keats/rust-bcrypt" [dependencies.base64] version = "0.22" default-features = false [dependencies.blowfish] version = "0.9" features = ["bcrypt"] [dependencies.getrandom] version = "0.2" optional = true default-features = false [dependencies.subtle] version = "2.4.1" default-features = false [dependencies.zeroize] version = "1.5.4" optional = true [dev-dependencies.quickcheck] version = "1" default-features = false [features] alloc = [ "base64/alloc", "getrandom", ] default = [ "std", "zeroize", ] js = ["getrandom/js"] std = [ "getrandom/std", "base64/std", ] [badges.maintenance] status = "passively-maintained" bcrypt-0.15.1/Cargo.toml.orig000064400000000000000000000020261046102023000141020ustar 00000000000000[package] name = "bcrypt" version = "0.15.1" authors = ["Vincent Prouillet "] license = "MIT" readme = "README.md" description = "Easily hash and verify passwords using bcrypt" homepage = "https://github.com/Keats/rust-bcrypt" repository = "https://github.com/Keats/rust-bcrypt" keywords = ["bcrypt", "password", "web", "hash"] edition = "2021" include = ["src/**/*", "LICENSE", "README.md"] [features] default = ["std", "zeroize"] std = ["getrandom/std", "base64/std"] alloc = ["base64/alloc", "getrandom"] js = ["getrandom/js"] [dependencies] blowfish = { version = "0.9", features = ["bcrypt"] } getrandom = { version = "0.2", default-features = false, optional = true } base64 = { version = "0.22", default-features = false } zeroize = { version = "1.5.4", optional = true } subtle = { version = "2.4.1", default-features = false } [dev-dependencies] # no default features avoid pulling in log quickcheck = { version = "1", default-features = false } [badges] maintenance = { status = "passively-maintained" } bcrypt-0.15.1/LICENSE000064400000000000000000000020741046102023000122230ustar 00000000000000The MIT License (MIT) Copyright (c) 2020 Vincent Prouillet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. bcrypt-0.15.1/README.md000064400000000000000000000066211046102023000124770ustar 00000000000000# bcrypt [![Safety Dance](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) [![Build Status](https://travis-ci.org/Keats/rust-bcrypt.svg)](https://travis-ci.org/Keats/rust-bcrypt) [![Documentation](https://docs.rs/bcrypt/badge.svg)](https://docs.rs/bcrypt) ## Installation Add the following to Cargo.toml: ```toml bcrypt = "0.15" ``` The minimum Rust version is 1.60.0. ## Usage The crate makes 3 things public: `DEFAULT_COST`, `hash`, `verify`. ```rust extern crate bcrypt; use bcrypt::{DEFAULT_COST, hash, verify}; let hashed = hash("hunter2", DEFAULT_COST)?; let valid = verify("hunter2", &hashed)?; ``` 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. ## `no_std` `bcrypt` crate supports `no_std` platforms. When `alloc` feature is enabled, all crate functionality is available. When `alloc` is not enabled only the raw `bcrypt()` function is usable. ## Benchmarks Speed depends on the cost used: the highest the slowest. Here are some benchmarks on a 2019 Macbook Pro to give you some ideas on the cost/speed ratio. Note that I don't go above 14 as it takes too long. ``` test bench_cost_10 ... bench: 51,474,665 ns/iter (+/- 16,006,581) test bench_cost_14 ... bench: 839,109,086 ns/iter (+/- 274,507,463) test bench_cost_4 ... bench: 795,814 ns/iter (+/- 42,838) test bench_cost_default ... bench: 195,344,338 ns/iter (+/- 8,329,675) ``` ## Acknowledgments This [gist](https://gist.github.com/rgdmarshall/ae3dc072445ed88b357a) for the hash splitting and the null termination. ## Recommendations While bcrypt works well as an algorithm, using something like [Argon2](https://en.wikipedia.org/wiki/Argon2) is recommended for new projects. ## Changelog * 0.15.1: update base64 dependency * 0.15.0: add an `alloc` feature that can be disabled. * 0.14.0: use `subtle` crate for constant time comparison, update base64 and bump to 2021 edition * 0.13.0: make zeroize dep opt-out and use fixed salt length * 0.12.1: zero vec containing password in the hashing function before returning the hash * 0.12.0: allow null bytes in password * 0.11.0: update deps causing big bump in MSRV * 0.10.1: fix panic with invalid hashes and allow `2x` * 0.10.0: update blowfish to 0.8 and minimum Rust version to 1.43.0. * 0.9.0: update base64 to 0.13 and getrandom to 0.2 * 0.8.2: fix no-std build * 0.8.0: constant time verification for hash, remove custom base64 code from repo and add `std` feature * 0.7.0: add HashParts::from_str and remove Error::description impl, it's deprecated * 0.6.3: add `hash_with_salt` function and make `Version::format_for_version` public * 0.6.2: update base64 to 0.12 * 0.6.1: update base64 to 0.11 * 0.6.0: allow users to choose the bcrypt version and default to 2b instead of 2y * 0.5.0: expose the inner `bcrypt` function + edition 2018 * 0.4.0: make DEFAULT_COST const instead of static * 0.3.0: forbid NULL bytes in passwords & update dependencies * 0.2.2: update rand * 0.2.1: update rand * 0.2.0: replace rust-crypto with blowfish, use some more modern Rust things like `?` and handle more errors * 0.1.6: update rand and base64 deps * 0.1.5: update lazy-static to 1.0 * 0.1.4: Replace rustc-serialize dependency with bcrypt * 0.1.3: Fix panic when password > 72 chars * 0.1.1: make BcryptResult, BcryptError public and update dependencies * 0.1.0: initial release bcrypt-0.15.1/src/bcrypt.rs000064400000000000000000000056311046102023000136600ustar 00000000000000use blowfish::Blowfish; fn setup(cost: u32, salt: &[u8], key: &[u8]) -> Blowfish { assert!(cost < 32); let mut state = Blowfish::bc_init_state(); state.salted_expand_key(salt, key); for _ in 0..1u32 << cost { state.bc_expand_key(key); state.bc_expand_key(salt); } state } pub fn bcrypt(cost: u32, salt: [u8; 16], password: &[u8]) -> [u8; 24] { assert!(!password.is_empty() && password.len() <= 72); let mut output = [0; 24]; let state = setup(cost, &salt, password); // OrpheanBeholderScryDoubt #[allow(clippy::unreadable_literal)] let mut ctext = [ 0x4f727068, 0x65616e42, 0x65686f6c, 0x64657253, 0x63727944, 0x6f756274, ]; for i in 0..3 { let i: usize = i * 2; for _ in 0..64 { let [l, r] = state.bc_encrypt([ctext[i], ctext[i + 1]]); ctext[i] = l; ctext[i + 1] = r; } let buf = ctext[i].to_be_bytes(); output[i * 4..][..4].copy_from_slice(&buf); let buf = ctext[i + 1].to_be_bytes(); output[(i + 1) * 4..][..4].copy_from_slice(&buf); } output } #[cfg(test)] mod tests { use super::bcrypt; #[test] fn raw_bcrypt() { // test vectors unbase64ed from // https://github.com/djmdjm/jBCrypt/blob/master/test/org/mindrot/jbcrypt/TestBCrypt.java // $2a$06$DCq7YPn5Rq63x1Lad4cll.TV4S6ytwfsfvkgY8jIucDrjc8deX1s. let pw = b"\0"; let cost = 6; let salt = [ 0x14, 0x4b, 0x3d, 0x69, 0x1a, 0x7b, 0x4e, 0xcf, 0x39, 0xcf, 0x73, 0x5c, 0x7f, 0xa7, 0xa7, 0x9c, ]; let result = [ 0x55, 0x7e, 0x94, 0xf3, 0x4b, 0xf2, 0x86, 0xe8, 0x71, 0x9a, 0x26, 0xbe, 0x94, 0xac, 0x1e, 0x16, 0xd9, 0x5e, 0xf9, 0xf8, 0x19, 0xde, 0xe0, ]; assert_eq!(bcrypt(cost, salt, pw)[..23], result); // $2a$06$m0CrhHm10qJ3lXRY.5zDGO3rS2KdeeWLuGmsfGlMfOxih58VYVfxe let pw = b"a\0"; let cost = 6; let salt = [ 0xa3, 0x61, 0x2d, 0x8c, 0x9a, 0x37, 0xda, 0xc2, 0xf9, 0x9d, 0x94, 0xda, 0x3, 0xbd, 0x45, 0x21, ]; let result = [ 0xe6, 0xd5, 0x38, 0x31, 0xf8, 0x20, 0x60, 0xdc, 0x8, 0xa2, 0xe8, 0x48, 0x9c, 0xe8, 0x50, 0xce, 0x48, 0xfb, 0xf9, 0x76, 0x97, 0x87, 0x38, ]; assert_eq!(bcrypt(cost, salt, pw)[..23], result); // // $2a$08$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz. let pw = b"abcdefghijklmnopqrstuvwxyz\0"; let cost = 8; let salt = [ 0x71, 0x5b, 0x96, 0xca, 0xed, 0x2a, 0xc9, 0x2c, 0x35, 0x4e, 0xd1, 0x6c, 0x1e, 0x19, 0xe3, 0x8a, ]; let result = [ 0x98, 0xbf, 0x9f, 0xfc, 0x1f, 0x5b, 0xe4, 0x85, 0xf9, 0x59, 0xe8, 0xb1, 0xd5, 0x26, 0x39, 0x2f, 0xbd, 0x4e, 0xd2, 0xd5, 0x71, 0x9f, 0x50, ]; assert_eq!(bcrypt(cost, salt, pw)[..23], result); } } bcrypt-0.15.1/src/errors.rs000064400000000000000000000060571046102023000136740ustar 00000000000000#[cfg(any(feature = "alloc", feature = "std"))] use alloc::string::String; use core::fmt; #[cfg(feature = "std")] use std::error; #[cfg(feature = "std")] use std::io; /// Library generic result type. pub type BcryptResult = Result; #[derive(Debug)] /// All the errors we can encounter while hashing/verifying /// passwords pub enum BcryptError { #[cfg(feature = "std")] Io(io::Error), CostNotAllowed(u32), #[cfg(any(feature = "alloc", feature = "std"))] InvalidCost(String), #[cfg(any(feature = "alloc", feature = "std"))] InvalidPrefix(String), #[cfg(any(feature = "alloc", feature = "std"))] InvalidHash(String), InvalidSaltLen(usize), InvalidBase64(base64::DecodeError), #[cfg(any(feature = "alloc", feature = "std"))] Rand(getrandom::Error), } macro_rules! impl_from_error { ($f: ty, $e: expr) => { impl From<$f> for BcryptError { fn from(f: $f) -> BcryptError { $e(f) } } }; } impl_from_error!(base64::DecodeError, BcryptError::InvalidBase64); #[cfg(feature = "std")] impl_from_error!(io::Error, BcryptError::Io); #[cfg(any(feature = "alloc", feature = "std"))] impl_from_error!(getrandom::Error, BcryptError::Rand); impl fmt::Display for BcryptError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { #[cfg(feature = "std")] BcryptError::Io(ref err) => write!(f, "IO error: {}", err), #[cfg(any(feature = "alloc", feature = "std"))] BcryptError::InvalidCost(ref cost) => write!(f, "Invalid Cost: {}", cost), BcryptError::CostNotAllowed(ref cost) => write!( f, "Cost needs to be between {} and {}, got {}", crate::MIN_COST, crate::MAX_COST, cost ), #[cfg(any(feature = "alloc", feature = "std"))] BcryptError::InvalidPrefix(ref prefix) => write!(f, "Invalid Prefix: {}", prefix), #[cfg(any(feature = "alloc", feature = "std"))] BcryptError::InvalidHash(ref hash) => write!(f, "Invalid hash: {}", hash), BcryptError::InvalidBase64(ref err) => write!(f, "Base64 error: {}", err), BcryptError::InvalidSaltLen(len) => { write!(f, "Invalid salt len: expected 16, received {}", len) } #[cfg(any(feature = "alloc", feature = "std"))] BcryptError::Rand(ref err) => write!(f, "Rand error: {}", err), } } } #[cfg(feature = "std")] impl error::Error for BcryptError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match *self { BcryptError::Io(ref err) => Some(err), BcryptError::InvalidCost(_) | BcryptError::CostNotAllowed(_) | BcryptError::InvalidPrefix(_) | BcryptError::InvalidHash(_) | BcryptError::InvalidSaltLen(_) => None, BcryptError::InvalidBase64(ref err) => Some(err), BcryptError::Rand(ref err) => Some(err), } } } bcrypt-0.15.1/src/lib.rs000064400000000000000000000365171046102023000131320ustar 00000000000000//! Easily hash and verify passwords using bcrypt #![forbid(unsafe_code)] #![cfg_attr(not(feature = "std"), no_std)] #[cfg(any(feature = "alloc", feature = "std", test))] extern crate alloc; #[cfg(any(feature = "alloc", feature = "std", test))] use alloc::{ string::{String, ToString}, vec::Vec, }; #[cfg(feature = "zeroize")] use zeroize::Zeroize; use base64::{alphabet::BCRYPT, engine::general_purpose::NO_PAD, engine::GeneralPurpose}; use core::fmt; #[cfg(any(feature = "alloc", feature = "std"))] use {base64::Engine, core::convert::AsRef, core::str::FromStr, getrandom::getrandom}; mod bcrypt; mod errors; pub use crate::bcrypt::bcrypt; pub use crate::errors::{BcryptError, BcryptResult}; // Cost constants const MIN_COST: u32 = 4; const MAX_COST: u32 = 31; pub const DEFAULT_COST: u32 = 12; pub const BASE_64: GeneralPurpose = GeneralPurpose::new(&BCRYPT, NO_PAD); #[cfg(any(feature = "alloc", feature = "std"))] #[derive(Debug, PartialEq)] /// A bcrypt hash result before concatenating pub struct HashParts { cost: u32, salt: String, hash: String, } /// BCrypt hash version /// https://en.wikipedia.org/wiki/Bcrypt#Versioning_history pub enum Version { TwoA, TwoX, TwoY, TwoB, } #[cfg(any(feature = "alloc", feature = "std"))] impl HashParts { /// Creates the bcrypt hash string from all its parts fn format(self) -> String { self.format_for_version(Version::TwoB) } /// Get the bcrypt hash cost pub fn get_cost(&self) -> u32 { self.cost } /// Get the bcrypt hash salt pub fn get_salt(&self) -> String { self.salt.clone() } /// Creates the bcrypt hash string from all its part, allowing to customize the version. pub fn format_for_version(&self, version: Version) -> String { // Cost need to have a length of 2 so padding with a 0 if cost < 10 alloc::format!("${}${:02}${}{}", version, self.cost, self.salt, self.hash) } } #[cfg(any(feature = "alloc", feature = "std"))] impl FromStr for HashParts { type Err = BcryptError; fn from_str(s: &str) -> Result { split_hash(s) } } #[cfg(any(feature = "alloc", feature = "std"))] impl ToString for HashParts { fn to_string(&self) -> String { self.format_for_version(Version::TwoY) } } impl fmt::Display for Version { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let str = match self { Version::TwoA => "2a", Version::TwoB => "2b", Version::TwoX => "2x", Version::TwoY => "2y", }; write!(f, "{}", str) } } /// The main meat: actually does the hashing and does some verification with /// the cost to ensure it's a correct one #[cfg(any(feature = "alloc", feature = "std"))] fn _hash_password(password: &[u8], cost: u32, salt: [u8; 16]) -> BcryptResult { if !(MIN_COST..=MAX_COST).contains(&cost) { return Err(BcryptError::CostNotAllowed(cost)); } // Passwords need to be null terminated let mut vec = Vec::with_capacity(password.len() + 1); vec.extend_from_slice(password); vec.push(0); // We only consider the first 72 chars; truncate if necessary. // `bcrypt` below will panic if len > 72 let truncated = if vec.len() > 72 { &vec[..72] } else { &vec }; let output = bcrypt::bcrypt(cost, salt, truncated); #[cfg(feature = "zeroize")] vec.zeroize(); Ok(HashParts { cost, salt: BASE_64.encode(salt), hash: BASE_64.encode(&output[..23]), // remember to remove the last byte }) } /// Takes a full hash and split it into 3 parts: /// cost, salt and hash #[cfg(any(feature = "alloc", feature = "std"))] fn split_hash(hash: &str) -> BcryptResult { let mut parts = HashParts { cost: 0, salt: "".to_string(), hash: "".to_string(), }; // Should be [prefix, cost, hash] let raw_parts: Vec<_> = hash.split('$').filter(|s| !s.is_empty()).collect(); if raw_parts.len() != 3 { return Err(BcryptError::InvalidHash(hash.to_string())); } if raw_parts[0] != "2y" && raw_parts[0] != "2b" && raw_parts[0] != "2a" && raw_parts[0] != "2x" { return Err(BcryptError::InvalidPrefix(raw_parts[0].to_string())); } if let Ok(c) = raw_parts[1].parse::() { parts.cost = c; } else { return Err(BcryptError::InvalidCost(raw_parts[1].to_string())); } if raw_parts[2].len() == 53 && raw_parts[2].is_char_boundary(22) { parts.salt = raw_parts[2][..22].chars().collect(); parts.hash = raw_parts[2][22..].chars().collect(); } else { return Err(BcryptError::InvalidHash(hash.to_string())); } Ok(parts) } /// Generates a password hash using the cost given. /// The salt is generated randomly using the OS randomness #[cfg(any(feature = "alloc", feature = "std"))] pub fn hash>(password: P, cost: u32) -> BcryptResult { hash_with_result(password, cost).map(|r| r.format()) } /// Generates a password hash using the cost given. /// The salt is generated randomly using the OS randomness. /// The function returns a result structure and allows to format the hash in different versions. #[cfg(any(feature = "alloc", feature = "std"))] pub fn hash_with_result>(password: P, cost: u32) -> BcryptResult { let salt = { let mut s = [0u8; 16]; getrandom(&mut s).map(|_| s) }?; _hash_password(password.as_ref(), cost, salt) } /// Generates a password given a hash and a cost. /// The function returns a result structure and allows to format the hash in different versions. #[cfg(any(feature = "alloc", feature = "std"))] pub fn hash_with_salt>( password: P, cost: u32, salt: [u8; 16], ) -> BcryptResult { _hash_password(password.as_ref(), cost, salt) } /// Verify that a password is equivalent to the hash provided #[cfg(any(feature = "alloc", feature = "std"))] pub fn verify>(password: P, hash: &str) -> BcryptResult { use subtle::ConstantTimeEq; let parts = split_hash(hash)?; let salt = BASE_64.decode(&parts.salt)?; let salt_len = salt.len(); let generated = _hash_password( password.as_ref(), parts.cost, salt.try_into() .map_err(|_| BcryptError::InvalidSaltLen(salt_len))?, )?; let source_decoded = BASE_64.decode(parts.hash)?; let generated_decoded = BASE_64.decode(generated.hash)?; Ok(source_decoded.ct_eq(&generated_decoded).into()) } #[cfg(test)] mod tests { use super::{ _hash_password, alloc::{ string::{String, ToString}, vec, vec::Vec, }, hash, hash_with_salt, split_hash, verify, BcryptError, BcryptResult, HashParts, Version, DEFAULT_COST, }; use core::convert::TryInto; use core::iter; use core::str::FromStr; use quickcheck::{quickcheck, TestResult}; #[test] fn can_split_hash() { let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; let output = split_hash(hash).unwrap(); let expected = HashParts { cost: 12, salt: "L6Bc/AlTQHyd9liGgGEZyO".to_string(), hash: "FLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u".to_string(), }; assert_eq!(output, expected); } #[test] fn can_output_cost_and_salt_from_parsed_hash() { let hash = "$2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; let parsed = HashParts::from_str(hash).unwrap(); assert_eq!(parsed.get_cost(), 12); assert_eq!(parsed.get_salt(), "L6Bc/AlTQHyd9liGgGEZyO".to_string()); } #[test] fn returns_an_error_if_a_parsed_hash_is_baddly_formated() { let hash1 = "$2y$12$L6Bc/AlTQHyd9lGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; assert!(HashParts::from_str(hash1).is_err()); let hash2 = "!2y$12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; assert!(HashParts::from_str(hash2).is_err()); let hash3 = "$2y$-12$L6Bc/AlTQHyd9liGgGEZyOFLPHNgyxeEPfgYfBCVxJ7JIlwxyVU3u"; assert!(HashParts::from_str(hash3).is_err()); } #[test] fn can_verify_hash_generated_from_some_online_tool() { let hash = "$2a$04$UuTkLRZZ6QofpDOlMz32MuuxEHA43WOemOYHPz6.SjsVsyO1tDU96"; assert!(verify("password", hash).unwrap()); } #[test] fn can_verify_hash_generated_from_python() { let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; assert!(verify("correctbatteryhorsestapler", hash).unwrap()); } #[test] fn can_verify_hash_generated_from_node() { let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk5bektyVVa5xnIi"; assert!(verify("correctbatteryhorsestapler", hash).unwrap()); } #[test] fn can_verify_hash_generated_from_go() { /* package main import ( "io" "os" "golang.org/x/crypto/bcrypt" ) func main() { buf, err := io.ReadAll(os.Stdin) if err != nil { panic(err) } out, err := bcrypt.GenerateFromPassword(buf, bcrypt.MinCost) if err != nil { panic(err) } os.Stdout.Write(out) os.Stdout.Write([]byte("\n")) } */ let binary_input = vec![ 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124, 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135, ]; let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9aDw5eFdstYLR8nFCNaOQmsH9XD23w."; assert!(verify(binary_input, hash).unwrap()); } #[test] fn invalid_hash_does_not_panic() { let binary_input = vec![ 29, 225, 195, 167, 223, 236, 85, 195, 114, 227, 7, 0, 209, 239, 189, 24, 51, 105, 124, 168, 151, 75, 144, 64, 198, 197, 196, 4, 241, 97, 110, 135, ]; let hash = "$2a$04$tjARW6ZON3PhrAIRW2LG/u9a."; assert!(verify(binary_input, hash).is_err()); } #[test] fn a_wrong_password_is_false() { let hash = "$2b$04$EGdrhbKUv8Oc9vGiXX0HQOxSg445d458Muh7DAHskb6QbtCvdxcie"; assert!(!verify("wrong", hash).unwrap()); } #[test] fn errors_with_invalid_hash() { // there is another $ in the hash part let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; assert!(verify("correctbatteryhorsestapler", hash).is_err()); } #[test] fn errors_with_non_number_cost() { // the cost is not a number let hash = "$2a$ab$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIi"; assert!(verify("correctbatteryhorsestapler", hash).is_err()); } #[test] fn errors_with_a_hash_too_long() { // the cost is not a number let hash = "$2a$04$n4Uy0eSnMfvnESYL.bLwuuj0U/ETSsoTpRT9GVk$5bektyVVa5xnIerererereri"; assert!(verify("correctbatteryhorsestapler", hash).is_err()); } #[test] fn can_verify_own_generated() { let hashed = hash("hunter2", 4).unwrap(); assert_eq!(true, verify("hunter2", &hashed).unwrap()); } #[test] fn long_passwords_truncate_correctly() { // produced with python -c 'import bcrypt; bcrypt.hashpw(b"x"*100, b"$2a$05$...............................")' let hash = "$2a$05$......................YgIDy4hFBdVlc/6LHnD9mX488r9cLd2"; assert!(verify(iter::repeat("x").take(100).collect::(), hash).unwrap()); } #[test] fn generate_versions() { let password = "hunter2".as_bytes(); let salt = vec![0; 16]; let result = _hash_password(password, DEFAULT_COST, salt.try_into().unwrap()).unwrap(); assert_eq!( "$2a$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", result.format_for_version(Version::TwoA) ); assert_eq!( "$2b$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", result.format_for_version(Version::TwoB) ); assert_eq!( "$2x$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", result.format_for_version(Version::TwoX) ); assert_eq!( "$2y$12$......................21jzCB1r6pN6rp5O2Ev0ejjTAboskKm", result.format_for_version(Version::TwoY) ); let hash = result.to_string(); assert_eq!(true, verify("hunter2", &hash).unwrap()); } #[test] fn allow_null_bytes() { // hash p1, check the hash against p2: fn hash_and_check(p1: &[u8], p2: &[u8]) -> Result { let fast_cost = 4; match hash(p1, fast_cost) { Ok(s) => verify(p2, &s), Err(e) => Err(e), } } fn assert_valid_password(p1: &[u8], p2: &[u8], expected: bool) { match hash_and_check(p1, p2) { Ok(checked) => { if checked != expected { panic!( "checked {:?} against {:?}, incorrect result {}", p1, p2, checked ) } } Err(e) => panic!("error evaluating password: {} for {:?}.", e, p1), } } // bcrypt should consider all of these distinct: let test_passwords = vec![ "\0", "passw0rd\0", "password\0with tail", "\0passw0rd", "a", "a\0", "a\0b\0", ]; for (i, p1) in test_passwords.iter().enumerate() { for (j, p2) in test_passwords.iter().enumerate() { assert_valid_password(p1.as_bytes(), p2.as_bytes(), i == j); } } // this is a quirk of the bcrypt algorithm: passwords that are entirely null // bytes hash to the same value, even if they are different lengths: assert_valid_password("\0\0\0\0\0\0\0\0".as_bytes(), "\0".as_bytes(), true); } #[test] fn hash_with_fixed_salt() { let salt = [ 38, 113, 212, 141, 108, 213, 195, 166, 201, 38, 20, 13, 47, 40, 104, 18, ]; let hashed = hash_with_salt("My S3cre7 P@55w0rd!", 5, salt) .unwrap() .to_string(); assert_eq!( "$2y$05$HlFShUxTu4ZHHfOLJwfmCeDj/kuKFKboanXtDJXxCC7aIPTUgxNDe", &hashed ); } quickcheck! { fn can_verify_arbitrary_own_generated(pass: Vec) -> BcryptResult { let mut pass = pass; pass.retain(|&b| b != 0); let hashed = hash(&pass, 4)?; verify(pass, &hashed) } fn doesnt_verify_different_passwords(a: Vec, b: Vec) -> BcryptResult { let mut a = a; a.retain(|&b| b != 0); let mut b = b; b.retain(|&b| b != 0); if a == b { return Ok(TestResult::discard()); } let hashed = hash(a, 4)?; Ok(TestResult::from_bool(!verify(b, &hashed)?)) } } #[test] fn does_no_error_on_char_boundary_splitting() { // Just checks that it does not panic let _ = verify( &[], "2a$$$0$OOOOOOOOOOOOOOOOOOOOO£OOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", ); } }