password-hash-0.5.0/.cargo_vcs_info.json0000644000000001530000000000100136200ustar { "git": { "sha1": "d80228437165f30bb20a2136d39e4c2de9cae5a5" }, "path_in_vcs": "password-hash" }password-hash-0.5.0/CHANGELOG.md000064400000000000000000000120071046102023000142220ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## 0.5.0 (2023-03-04) ### Added - `Error::OutputSize` ([#1026]) - `std::error::Error::source` for `Error` ([#1264]) - `getrandom` feature ([#1267]) ### Changed - Use `Salt` type with `PasswordHasher` ([#1187]) - Rename `Salt::new` => `Salt::from_b64` ([#1266]) - Rename `Salt::b64_decode` => `Salt::decode_b64` ([#1266]) - Rename `SaltString::new` => `SaltString::from_b64` ([#1266]) - Rename `SaltString::b64_decode` => `SaltString::decode_b64` ([#1266]) - Rename `SaltString::b64_encode` => `SaltString::encode_b64` ([#1266]) ### Fixed - Allow `Salt` to be exactly the same amount as `MAX_LENGTH` value ([#1246]) [#1026]: https://github.com/RustCrypto/traits/pull/1026 [#1187]: https://github.com/RustCrypto/traits/pull/1187 [#1246]: https://github.com/RustCrypto/traits/pull/1246 [#1264]: https://github.com/RustCrypto/traits/pull/1264 [#1266]: https://github.com/RustCrypto/traits/pull/1266 [#1267]: https://github.com/RustCrypto/traits/pull/1267 ## 0.4.2 (2022-06-27) ### Fixed - docs.rs metadata ([#1031]) [#1031]: https://github.com/RustCrypto/traits/pull/1031 ## 0.4.1 (2022-04-22) ### Added - `authentication` category to Cargo.toml ([#976]) [#976]: https://github.com/RustCrypto/traits/pull/976 ## 0.4.0 (2022-03-09) ### Changed - Leverage `const_panic`; MSRV 1.57 ([#896]) - Rust 2021 edition upgrade ([#897]) - Make `Ident::new` fallible; add `Ident::new_unwrap` ([#896], [#960]) ### Fixed - Better `Debug`/`Display` impls for `SaltString` ([#804]) ### Removed - `base64ct` version restrictions ([#914]) [#804]: https://github.com/RustCrypto/traits/pull/804 [#896]: https://github.com/RustCrypto/traits/pull/896 [#897]: https://github.com/RustCrypto/traits/pull/897 [#897]: https://github.com/RustCrypto/traits/pull/897 [#914]: https://github.com/RustCrypto/traits/pull/914 [#960]: https://github.com/RustCrypto/traits/pull/960 ## 0.3.2 (2021-09-15) ### Fixed - Remove unused lifetimes ([#760]) [#760]: https://github.com/RustCrypto/traits/pull/760 ## 0.3.1 (2021-09-14) [YANKED] ### Added - `PasswordHashString` ([#758]) ### Fixed - Handling of empty salts in `fmt::Display` impl for PasswordHash ([#748]) - MSRV regression from `base64ct` ([#757]) [#748]: https://github.com/RustCrypto/traits/pull/748 [#757]: https://github.com/RustCrypto/traits/pull/757 [#758]: https://github.com/RustCrypto/traits/pull/758 ## 0.3.0 (2021-08-27) [YANKED] ### Added - More details to `ParamValueInvalid` ([#713]) - `SaltInvalid` error ([#713]) - `version` param to `PasswordHasher` ([#719]) - `ParamsString::add_b64_bytes` method ([#722]) ### Changed - Rename `PasswordHash::hash_password_simple` => `PasswordHash::hash_password` ([#720]) - Rename `PasswordHash::hash_password` => `PasswordHash::hash_password_customized` ([#720]) - Rename `Error::B64` => `Error::B64Encoding` ([#721]) [#713]: https://github.com/RustCrypto/traits/pull/713 [#719]: https://github.com/RustCrypto/traits/pull/719 [#720]: https://github.com/RustCrypto/traits/pull/720 [#721]: https://github.com/RustCrypto/traits/pull/721 [#722]: https://github.com/RustCrypto/traits/pull/722 ## 0.2.3 (2021-08-23) ### Changed - Make max lengths of `Value` and `Salt` both 64 ([#707]) [#707]: https://github.com/RustCrypto/traits/pull/707 ## 0.2.2 (2021-07-20) ### Changed - Pin `subtle` dependency to v2.4 ([#689]) ### Added - Re-export `rand_core` ([#683]) [#683]: https://github.com/RustCrypto/traits/pull/683 [#689]: https://github.com/RustCrypto/traits/pull/689 ## 0.2.1 (2021-05-05) ### Changed - Use `subtle` crate for comparing hash `Output` ([#631]) [#631]: https://github.com/RustCrypto/traits/pull/631 ## 0.2.0 (2021-04-29) ### Changed - Allow specifying output length and version with params ([#615]) - Allow passing `&str`, `&Salt`, or `&SaltString` as salt ([#615]) - Simplify error handling ([#615]) [#615]: https://github.com/RustCrypto/traits/pull/615 ## 0.1.4 (2021-04-19) ### Added - Length constants ([#600]) ### Changed - Deprecate functions for obtaining length constants ([#600]) [#600]: https://github.com/RustCrypto/traits/pull/600 ## 0.1.3 (2021-04-17) ### Changed - Update docs for PHC string field ([#593]) ### Fixed - Broken `b64` links in rustdoc ([#594]) [#593]: https://github.com/RustCrypto/traits/pull/593 [#594]: https://github.com/RustCrypto/traits/pull/594 ## 0.1.2 (2021-03-17) ### Changed - Bump `base64ct` dependency to v1.0 ([#579]) [#579]: https://github.com/RustCrypto/traits/pull/579 ## 0.1.1 (2021-02-01) ### Added - `Encoding` enum with bcrypt and `crypt(3)` Base64 support ([#515]) - Support for using `PasswordHash` with an alternate `Encoding` ([#518]) ### Changed - Bump `base64ct` dependency to v0.2 ([#519]) [#515]: https://github.com/RustCrypto/traits/pull/515 [#518]: https://github.com/RustCrypto/traits/pull/518 [#519]: https://github.com/RustCrypto/traits/pull/519 ## 0.1.0 (2021-01-28) - Initial release password-hash-0.5.0/Cargo.toml0000644000000030710000000000100116200ustar # 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" rust-version = "1.60" name = "password-hash" version = "0.5.0" authors = ["RustCrypto Developers"] description = """ Traits which describe the functionality of password hashing algorithms, as well as a `no_std`-friendly implementation of the PHC string format (a well-defined subset of the Modular Crypt Format a.k.a. MCF) """ documentation = "https://docs.rs/password-hash" readme = "README.md" keywords = [ "crypt", "mcf", "password", "pbkdf", "phc", ] categories = [ "authentication", "cryptography", "no-std", ] license = "MIT OR Apache-2.0" repository = "https://github.com/RustCrypto/traits/tree/master/password-hash" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [dependencies.base64ct] version = "1" [dependencies.rand_core] version = "0.6.4" optional = true default-features = false [dependencies.subtle] version = "2" default-features = false [features] alloc = ["base64ct/alloc"] default = ["rand_core"] getrandom = ["rand_core/getrandom"] std = [ "alloc", "base64ct/std", "rand_core/std", ] password-hash-0.5.0/Cargo.toml.orig000064400000000000000000000020471046102023000153030ustar 00000000000000[package] name = "password-hash" description = """ Traits which describe the functionality of password hashing algorithms, as well as a `no_std`-friendly implementation of the PHC string format (a well-defined subset of the Modular Crypt Format a.k.a. MCF) """ version = "0.5.0" authors = ["RustCrypto Developers"] license = "MIT OR Apache-2.0" readme = "README.md" documentation = "https://docs.rs/password-hash" repository = "https://github.com/RustCrypto/traits/tree/master/password-hash" categories = ["authentication", "cryptography", "no-std"] keywords = ["crypt", "mcf", "password", "pbkdf", "phc"] edition = "2021" rust-version = "1.60" [dependencies] base64ct = "1" subtle = { version = "2", default-features = false } # optional dependencies rand_core = { version = "0.6.4", optional = true, default-features = false } [features] default = ["rand_core"] alloc = ["base64ct/alloc"] std = ["alloc", "base64ct/std", "rand_core/std"] getrandom = ["rand_core/getrandom"] [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] password-hash-0.5.0/LICENSE-APACHE000064400000000000000000000251411046102023000143400ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. password-hash-0.5.0/LICENSE-MIT000064400000000000000000000020561046102023000140500ustar 00000000000000Copyright (c) 2020-2023 RustCrypto Developers 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. password-hash-0.5.0/README.md000064400000000000000000000054541046102023000137000ustar 00000000000000# RustCrypto: Password Hashing Traits [![crate][crate-image]][crate-link] [![Docs][docs-image]][docs-link] [![Build Status][build-image]][build-link] ![Apache2/MIT licensed][license-image] ![Rust Version][rustc-image] [![Project Chat][chat-image]][chat-link] Traits which describe the functionality of [password hashing algorithms]. [Documentation][docs-link] ## About Provides a `no_std`-friendly implementation of the [Password Hashing Competition (PHC) string format specification][PHC] (a well-defined subset of the [Modular Crypt Format a.k.a. MCF][MCF]) which works in conjunction with the traits this crate defines. ## Supported Crates See [RustCrypto/password-hashes] for algorithm implementations which use this crate for interoperability: - [`argon2`] - Argon2 memory hard key derivation function - [`pbkdf2`] - Password-Based Key Derivation Function v2 - [`scrypt`] - scrypt key derivation function ## Minimum Supported Rust Version Rust **1.60** or higher. Minimum supported Rust version may be changed in the future, but it will be accompanied by a minor version bump. ## SemVer Policy - All on-by-default features of this library are covered by SemVer - MSRV is considered exempt from SemVer as noted above ## License Licensed under either of: - [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) - [MIT license](https://opensource.org/licenses/MIT) at your option. ### Contribution Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. [//]: # (badges) [crate-image]: https://buildstats.info/crate/password-hash [crate-link]: https://crates.io/crates/password-hash [docs-image]: https://docs.rs/password-hash/badge.svg [docs-link]: https://docs.rs/password-hash/ [build-image]: https://github.com/RustCrypto/traits/workflows/password-hash/badge.svg?branch=master&event=push [build-link]: https://github.com/RustCrypto/traits/actions?query=workflow:password-hash [license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg [rustc-image]: https://img.shields.io/badge/rustc-1.60+-blue.svg [chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg [chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/260046-password-hashes [//]: # (general links) [password hashing algorithms]: https://en.wikipedia.org/wiki/Cryptographic_hash_function#Password_verification [PHC]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md [MCF]: https://passlib.readthedocs.io/en/stable/modular_crypt_format.html [RustCrypto/password-hashes]: https://github.com/RustCrypto/password-hashes [`argon2`]: https://docs.rs/argon2 [`pbkdf2`]: https://docs.rs/pbkdf2 [`scrypt`]: https://docs.rs/scrypt password-hash-0.5.0/src/encoding.rs000064400000000000000000000041611046102023000153360ustar 00000000000000//! Base64 encoding variants. use base64ct::{ Base64Bcrypt, Base64Crypt, Base64Unpadded as B64, Encoding as _, Error as B64Error, }; /// Base64 encoding variants. #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub enum Encoding { /// "B64" encoding: standard Base64 without padding. /// /// ```text /// [A-Z] [a-z] [0-9] + / /// 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f /// ``` /// B64, /// bcrypt encoding. /// /// ```text /// ./ [A-Z] [a-z] [0-9] /// 0x2e-0x2f, 0x41-0x5a, 0x61-0x7a, 0x30-0x39 /// ``` Bcrypt, /// `crypt(3)` encoding. /// /// ```text /// [.-9] [A-Z] [a-z] /// 0x2e-0x39, 0x41-0x5a, 0x61-0x7a /// ``` Crypt, } impl Default for Encoding { fn default() -> Self { Self::B64 } } impl Encoding { /// Decode a Base64 string into the provided destination buffer. pub fn decode(self, src: impl AsRef<[u8]>, dst: &mut [u8]) -> Result<&[u8], B64Error> { match self { Self::B64 => B64::decode(src, dst), Self::Bcrypt => Base64Bcrypt::decode(src, dst), Self::Crypt => Base64Crypt::decode(src, dst), } } /// Encode the input byte slice as Base64. /// /// Writes the result into the provided destination slice, returning an /// ASCII-encoded Base64 string value. pub fn encode<'a>(self, src: &[u8], dst: &'a mut [u8]) -> Result<&'a str, B64Error> { match self { Self::B64 => B64::encode(src, dst), Self::Bcrypt => Base64Bcrypt::encode(src, dst), Self::Crypt => Base64Crypt::encode(src, dst), } .map_err(Into::into) } /// Get the length of Base64 produced by encoding the given bytes. pub fn encoded_len(self, bytes: &[u8]) -> usize { match self { Self::B64 => B64::encoded_len(bytes), Self::Bcrypt => Base64Bcrypt::encoded_len(bytes), Self::Crypt => Base64Crypt::encoded_len(bytes), } } } password-hash-0.5.0/src/errors.rs000064400000000000000000000121341046102023000150630ustar 00000000000000//! Error types. pub use base64ct::Error as B64Error; use core::cmp::Ordering; use core::fmt; /// Result type. pub type Result = core::result::Result; /// Password hashing errors. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum Error { /// Unsupported algorithm. Algorithm, /// "B64" encoding error. B64Encoding(B64Error), /// Cryptographic error. Crypto, /// Output size unexpected. OutputSize { /// Indicates why the output size is unexpected. /// /// - [`Ordering::Less`]: Size is too small. /// - [`Ordering::Equal`]: Size is not exactly as `expected`. /// - [`Ordering::Greater`]: Size is too long. provided: Ordering, /// Expected output size in relation to `provided`. /// /// - [`Ordering::Less`]: Minimum size. /// - [`Ordering::Equal`]: Expecrted size. /// - [`Ordering::Greater`]: Maximum size. expected: usize, }, /// Duplicate parameter name encountered. ParamNameDuplicated, /// Invalid parameter name. ParamNameInvalid, /// Invalid parameter value. ParamValueInvalid(InvalidValue), /// Maximum number of parameters exceeded. ParamsMaxExceeded, /// Invalid password. Password, /// Password hash string invalid. PhcStringField, /// Password hash string contains trailing data. PhcStringTrailingData, /// Salt invalid. SaltInvalid(InvalidValue), /// Invalid algorithm version. Version, } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> { match self { Self::Algorithm => write!(f, "unsupported algorithm"), Self::B64Encoding(err) => write!(f, "{}", err), Self::Crypto => write!(f, "cryptographic error"), Self::OutputSize { provided, expected } => match provided { Ordering::Less => write!( f, "output size too short, expected at least {} bytes", expected ), Ordering::Equal => write!(f, "output size unexpected, expected {} bytes", expected), Ordering::Greater => write!( f, "output size too long, expected at most {} bytes", expected ), }, Self::ParamNameDuplicated => f.write_str("duplicate parameter"), Self::ParamNameInvalid => f.write_str("invalid parameter name"), Self::ParamValueInvalid(val_err) => write!(f, "invalid parameter value: {}", val_err), Self::ParamsMaxExceeded => f.write_str("maximum number of parameters reached"), Self::Password => write!(f, "invalid password"), Self::PhcStringField => write!(f, "password hash string missing field"), Self::PhcStringTrailingData => { write!(f, "password hash string contains trailing characters") } Self::SaltInvalid(val_err) => write!(f, "salt invalid: {}", val_err), Self::Version => write!(f, "invalid algorithm version"), } } } #[cfg(feature = "std")] impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::B64Encoding(err) => Some(err), Self::ParamValueInvalid(err) => Some(err), Self::SaltInvalid(err) => Some(err), _ => None, } } } impl From for Error { fn from(err: B64Error) -> Error { Error::B64Encoding(err) } } impl From for Error { fn from(_: base64ct::InvalidLengthError) -> Error { Error::B64Encoding(B64Error::InvalidLength) } } /// Parse errors relating to invalid parameter values or salts. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[non_exhaustive] pub enum InvalidValue { /// Character is not in the allowed set. InvalidChar(char), /// Format is invalid. InvalidFormat, /// Value is malformed. Malformed, /// Value exceeds the maximum allowed length. TooLong, /// Value does not satisfy the minimum length. TooShort, } impl InvalidValue { /// Create an [`Error::ParamValueInvalid`] which warps this error. pub fn param_error(self) -> Error { Error::ParamValueInvalid(self) } /// Create an [`Error::SaltInvalid`] which wraps this error. pub fn salt_error(self) -> Error { Error::SaltInvalid(self) } } impl fmt::Display for InvalidValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> { match self { Self::InvalidChar(c) => write!(f, "contains invalid character: '{}'", c), Self::InvalidFormat => f.write_str("value format is invalid"), Self::Malformed => f.write_str("value malformed"), Self::TooLong => f.write_str("value to long"), Self::TooShort => f.write_str("value to short"), } } } #[cfg(feature = "std")] impl std::error::Error for InvalidValue {} password-hash-0.5.0/src/ident.rs000064400000000000000000000125121046102023000146520ustar 00000000000000//! Algorithm or parameter identifier. //! //! Implements the following parts of the [PHC string format specification][1]: //! //! > The function symbolic name is a sequence of characters in: `[a-z0-9-]` //! > (lowercase letters, digits, and the minus sign). No other character is //! > allowed. Each function defines its own identifier (or identifiers in case //! > of a function family); identifiers should be explicit (human readable, //! > not a single digit), with a length of about 5 to 10 characters. An //! > identifier name MUST NOT exceed 32 characters in length. //! > //! > Each parameter name shall be a sequence of characters in: `[a-z0-9-]` //! > (lowercase letters, digits, and the minus sign). No other character is //! > allowed. Parameter names SHOULD be readable for a human user. A //! > parameter name MUST NOT exceed 32 characters in length. //! //! [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md use crate::{Error, Result}; use core::{fmt, ops::Deref, str}; /// Algorithm or parameter identifier. /// /// This type encompasses both the "function symbolic name" and "parameter name" /// use cases as described in the [PHC string format specification][1]. /// /// # Constraints /// - ASCII-encoded string consisting of the characters `[a-z0-9-]` /// (lowercase letters, digits, and the minus sign) /// - Minimum length: 1 ASCII character (i.e. 1-byte) /// - Maximum length: 32 ASCII characters (i.e. 32-bytes) /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md #[derive(Copy, Clone, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct Ident<'a>(&'a str); impl<'a> Ident<'a> { /// Maximum length of an [`Ident`] - 32 ASCII characters (i.e. 32-bytes). /// /// This value corresponds to the maximum size of a function symbolic names /// and parameter names according to the PHC string format. /// Maximum length of an [`Ident`] - 32 ASCII characters (i.e. 32-bytes). /// /// This value corresponds to the maximum size of a function symbolic names /// and parameter names according to the PHC string format. const MAX_LENGTH: usize = 32; /// Parse an [`Ident`] from a string. /// /// String must conform to the constraints given in the type-level /// documentation. pub const fn new(s: &'a str) -> Result { let input = s.as_bytes(); match input.len() { 1..=Self::MAX_LENGTH => { let mut i = 0; while i < input.len() { if !matches!(input[i], b'a'..=b'z' | b'0'..=b'9' | b'-') { return Err(Error::ParamNameInvalid); } i += 1; } Ok(Self(s)) } _ => Err(Error::ParamNameInvalid), } } /// Parse an [`Ident`] from a string, panicking on parse errors. /// /// This function exists as a workaround for `unwrap` not yet being /// stable in `const fn` contexts, and is intended to allow the result to /// be bound to a constant value. pub const fn new_unwrap(s: &'a str) -> Self { assert!(!s.is_empty(), "PHC ident string can't be empty"); assert!(s.len() <= Self::MAX_LENGTH, "PHC ident string too long"); match Self::new(s) { Ok(ident) => ident, Err(_) => panic!("invalid PHC string format identifier"), } } /// Borrow this ident as a `str` pub fn as_str(&self) -> &'a str { self.0 } } impl<'a> AsRef for Ident<'a> { fn as_ref(&self) -> &str { self.as_str() } } impl<'a> Deref for Ident<'a> { type Target = str; fn deref(&self) -> &str { self.as_str() } } // Note: this uses `TryFrom` instead of `FromStr` to support a lifetime on // the `str` the value is being parsed from. impl<'a> TryFrom<&'a str> for Ident<'a> { type Error = Error; fn try_from(s: &'a str) -> Result { Self::new(s) } } impl<'a> fmt::Display for Ident<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self) } } impl<'a> fmt::Debug for Ident<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Ident").field(&self.as_ref()).finish() } } #[cfg(test)] mod tests { use super::{Error, Ident}; // Invalid ident examples const INVALID_EMPTY: &str = ""; const INVALID_CHAR: &str = "argon2;d"; const INVALID_TOO_LONG: &str = "012345678911234567892123456789312"; const INVALID_CHAR_AND_TOO_LONG: &str = "0!2345678911234567892123456789312"; #[test] fn parse_valid() { let valid_examples = ["6", "x", "argon2d", "01234567891123456789212345678931"]; for &example in &valid_examples { assert_eq!(example, &*Ident::new(example).unwrap()); } } #[test] fn reject_empty() { assert_eq!(Ident::new(INVALID_EMPTY), Err(Error::ParamNameInvalid)); } #[test] fn reject_invalid() { assert_eq!(Ident::new(INVALID_CHAR), Err(Error::ParamNameInvalid)); } #[test] fn reject_too_long() { assert_eq!(Ident::new(INVALID_TOO_LONG), Err(Error::ParamNameInvalid)); } #[test] fn reject_invalid_char_and_too_long() { assert_eq!( Ident::new(INVALID_CHAR_AND_TOO_LONG), Err(Error::ParamNameInvalid) ); } } password-hash-0.5.0/src/lib.rs000064400000000000000000000252671046102023000143300ustar 00000000000000#![no_std] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc = include_str!("../README.md")] #![doc( html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg", html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg" )] #![forbid(unsafe_code)] #![warn(missing_docs, rust_2018_idioms, unused_lifetimes)] //! //! # Usage //! //! This crate represents password hashes using the [`PasswordHash`] type, which //! represents a parsed "PHC string" with the following format: //! //! ```text //! $[$v=][$=(,=)*][$[$]] //! ``` //! //! For more information, please see the documentation for [`PasswordHash`]. #[cfg(feature = "alloc")] extern crate alloc; #[cfg(feature = "std")] extern crate std; #[cfg(feature = "rand_core")] pub use rand_core; pub mod errors; mod encoding; mod ident; mod output; mod params; mod salt; mod traits; mod value; pub use crate::{ encoding::Encoding, errors::{Error, Result}, ident::Ident, output::Output, params::ParamsString, salt::{Salt, SaltString}, traits::{McfHasher, PasswordHasher, PasswordVerifier}, value::{Decimal, Value}, }; use core::fmt::{self, Debug}; #[cfg(feature = "alloc")] use alloc::{ str::FromStr, string::{String, ToString}, }; /// Separator character used in password hashes (e.g. `$6$...`). const PASSWORD_HASH_SEPARATOR: char = '$'; /// Password hash. /// /// This type corresponds to the parsed representation of a PHC string as /// described in the [PHC string format specification][1]. /// /// PHC strings have the following format: /// /// ```text /// $[$v=][$=(,=)*][$[$]] /// ``` /// /// where: /// /// - `` is the symbolic name for the function /// - `` is the algorithm version /// - `` is a parameter name /// - `` is a parameter value /// - `` is an encoding of the salt /// - `` is an encoding of the hash output /// /// The string is then the concatenation, in that order, of: /// /// - a `$` sign; /// - the function symbolic name; /// - optionally, a `$` sign followed by the algorithm version with a `v=version` format; /// - optionally, a `$` sign followed by one or several parameters, each with a `name=value` format; /// the parameters are separated by commas; /// - optionally, a `$` sign followed by the (encoded) salt value; /// - optionally, a `$` sign followed by the (encoded) hash output (the hash output may be present /// only if the salt is present). /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification #[derive(Clone, Debug, Eq, PartialEq)] pub struct PasswordHash<'a> { /// Password hashing algorithm identifier. /// /// This corresponds to the `` field in a PHC string, a.k.a. the /// symbolic name for the function. pub algorithm: Ident<'a>, /// Optional version field. /// /// This corresponds to the `` field in a PHC string. pub version: Option, /// Algorithm-specific parameters. /// /// This corresponds to the set of `$=(,=)*` /// name/value pairs in a PHC string. pub params: ParamsString, /// [`Salt`] string for personalizing a password hash output. /// /// This corresponds to the `` value in a PHC string. pub salt: Option>, /// Password hashing function [`Output`], a.k.a. hash/digest. /// /// This corresponds to the `` output in a PHC string. pub hash: Option, } impl<'a> PasswordHash<'a> { /// Parse a password hash from a string in the PHC string format. pub fn new(s: &'a str) -> Result { Self::parse(s, Encoding::default()) } /// Parse a password hash from the given [`Encoding`]. pub fn parse(s: &'a str, encoding: Encoding) -> Result { if s.is_empty() { return Err(Error::PhcStringField); } let mut fields = s.split(PASSWORD_HASH_SEPARATOR); let beginning = fields.next().expect("no first field"); if beginning.chars().next().is_some() { return Err(Error::PhcStringField); } let algorithm = fields .next() .ok_or(Error::PhcStringField) .and_then(Ident::try_from)?; let mut version = None; let mut params = ParamsString::new(); let mut salt = None; let mut hash = None; let mut next_field = fields.next(); if let Some(field) = next_field { // v= if field.starts_with("v=") && !field.contains(params::PARAMS_DELIMITER) { version = Some(Value::new(&field[2..]).and_then(|value| value.decimal())?); next_field = None; } } if next_field.is_none() { next_field = fields.next(); } if let Some(field) = next_field { // = if field.contains(params::PAIR_DELIMITER) { params = field.parse()?; next_field = None; } } if next_field.is_none() { next_field = fields.next(); } if let Some(s) = next_field { salt = Some(s.try_into()?); } if let Some(field) = fields.next() { hash = Some(Output::decode(field, encoding)?); } if fields.next().is_some() { return Err(Error::PhcStringTrailingData); } Ok(Self { algorithm, version, params, salt, hash, }) } /// Generate a password hash using the supplied algorithm. pub fn generate( phf: impl PasswordHasher, password: impl AsRef<[u8]>, salt: impl Into>, ) -> Result { phf.hash_password(password.as_ref(), salt) } /// Verify this password hash using the specified set of supported /// [`PasswordHasher`] trait objects. pub fn verify_password( &self, phfs: &[&dyn PasswordVerifier], password: impl AsRef<[u8]>, ) -> Result<()> { for &phf in phfs { if phf.verify_password(password.as_ref(), self).is_ok() { return Ok(()); } } Err(Error::Password) } /// Get the [`Encoding`] that this [`PasswordHash`] is serialized with. pub fn encoding(&self) -> Encoding { self.hash.map(|h| h.encoding()).unwrap_or_default() } /// Serialize this [`PasswordHash`] as a [`PasswordHashString`]. #[cfg(feature = "alloc")] pub fn serialize(&self) -> PasswordHashString { self.into() } } // Note: this uses `TryFrom` instead of `FromStr` to support a lifetime on // the `str` the value is being parsed from. impl<'a> TryFrom<&'a str> for PasswordHash<'a> { type Error = Error; fn try_from(s: &'a str) -> Result { Self::new(s) } } impl<'a> fmt::Display for PasswordHash<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.algorithm)?; if let Some(version) = self.version { write!(f, "{}v={}", PASSWORD_HASH_SEPARATOR, version)?; } if !self.params.is_empty() { write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, self.params)?; } if let Some(salt) = &self.salt { write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, salt)?; if let Some(hash) = &self.hash { write!(f, "{}{}", PASSWORD_HASH_SEPARATOR, hash)?; } } Ok(()) } } /// Serialized [`PasswordHash`]. /// /// This type contains a serialized password hash string which is ensured to /// parse successfully. // TODO(tarcieri): cached parsed representations? or at least structural data #[cfg(feature = "alloc")] #[derive(Clone, Debug, Eq, PartialEq)] pub struct PasswordHashString { /// String value string: String, /// String encoding encoding: Encoding, } #[cfg(feature = "alloc")] #[allow(clippy::len_without_is_empty)] impl PasswordHashString { /// Parse a password hash from a string in the PHC string format. pub fn new(s: &str) -> Result { Self::parse(s, Encoding::default()) } /// Parse a password hash from the given [`Encoding`]. pub fn parse(s: &str, encoding: Encoding) -> Result { Ok(PasswordHash::parse(s, encoding)?.into()) } /// Parse this owned string as a [`PasswordHash`]. pub fn password_hash(&self) -> PasswordHash<'_> { PasswordHash::parse(&self.string, self.encoding).expect("malformed password hash") } /// Get the [`Encoding`] that this [`PasswordHashString`] is serialized with. pub fn encoding(&self) -> Encoding { self.encoding } /// Borrow this value as a `str`. pub fn as_str(&self) -> &str { self.string.as_str() } /// Borrow this value as bytes. pub fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } /// Get the length of this value in ASCII characters. pub fn len(&self) -> usize { self.as_str().len() } /// Password hashing algorithm identifier. pub fn algorithm(&self) -> Ident<'_> { self.password_hash().algorithm } /// Optional version field. pub fn version(&self) -> Option { self.password_hash().version } /// Algorithm-specific parameters. pub fn params(&self) -> ParamsString { self.password_hash().params } /// [`Salt`] string for personalizing a password hash output. pub fn salt(&self) -> Option> { self.password_hash().salt } /// Password hashing function [`Output`], a.k.a. hash/digest. pub fn hash(&self) -> Option { self.password_hash().hash } } #[cfg(feature = "alloc")] impl AsRef for PasswordHashString { fn as_ref(&self) -> &str { self.as_str() } } #[cfg(feature = "alloc")] impl From> for PasswordHashString { fn from(hash: PasswordHash<'_>) -> PasswordHashString { PasswordHashString::from(&hash) } } #[cfg(feature = "alloc")] impl From<&PasswordHash<'_>> for PasswordHashString { fn from(hash: &PasswordHash<'_>) -> PasswordHashString { PasswordHashString { string: hash.to_string(), encoding: hash.encoding(), } } } #[cfg(feature = "alloc")] impl FromStr for PasswordHashString { type Err = Error; fn from_str(s: &str) -> Result { Self::new(s) } } #[cfg(feature = "alloc")] impl fmt::Display for PasswordHashString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } password-hash-0.5.0/src/output.rs000064400000000000000000000264071046102023000151170ustar 00000000000000//! Outputs from password hashing functions. use crate::{Encoding, Error, Result}; use core::{ cmp::{Ordering, PartialEq}, fmt, str::FromStr, }; use subtle::{Choice, ConstantTimeEq}; /// Output from password hashing functions, i.e. the "hash" or "digest" /// as raw bytes. /// /// The [`Output`] type implements the RECOMMENDED best practices described in /// the [PHC string format specification][1], namely: /// /// > The hash output, for a verification, must be long enough to make preimage /// > attacks at least as hard as password guessing. To promote wide acceptance, /// > a default output size of 256 bits (32 bytes, encoded as 43 characters) is /// > recommended. Function implementations SHOULD NOT allow outputs of less /// > than 80 bits to be used for password verification. /// /// # Recommended length /// Per the description above, the recommended default length for an [`Output`] /// of a password hashing function is **32-bytes** (256-bits). /// /// # Constraints /// The above guidelines are interpreted into the following constraints: /// /// - Minimum length: **10**-bytes (80-bits) /// - Maximum length: **64**-bytes (512-bits) /// /// The specific recommendation of a 64-byte maximum length is taken as a best /// practice from the hash output guidelines for [Argon2 Encoding][2] given in /// the same document: /// /// > The hash output...length shall be between 12 and 64 bytes (16 and 86 /// > characters, respectively). The default output length is 32 bytes /// > (43 characters). /// /// Based on this guidance, this type enforces an upper bound of 64-bytes /// as a reasonable maximum, and recommends using 32-bytes. /// /// # Constant-time comparisons /// The [`Output`] type impls the [`ConstantTimeEq`] trait from the [`subtle`] /// crate and uses it to perform constant-time comparisons. /// /// Additionally the [`PartialEq`] and [`Eq`] trait impls for [`Output`] use /// [`ConstantTimeEq`] when performing comparisons. /// /// ## Attacks on non-constant-time password hash comparisons /// Comparing password hashes in constant-time is known to mitigate at least /// one [poorly understood attack][3] involving an adversary with the following /// knowledge/capabilities: /// /// - full knowledge of what password hashing algorithm is being used /// including any relevant configurable parameters /// - knowledge of the salt for a particular victim /// - ability to accurately measure a timing side-channel on comparisons /// of the password hash over the network /// /// An attacker with the above is able to perform an offline computation of /// the hash for any chosen password in such a way that it will match the /// hash computed by the server. /// /// As noted above, they also measure timing variability in the server's /// comparison of the hash it computes for a given password and a target hash /// the attacker is trying to learn. /// /// When the attacker observes a hash comparison that takes longer than their /// previous attempts, they learn that they guessed another byte in the /// password hash correctly. They can leverage repeated measurements and /// observations with different candidate passwords to learn the password /// hash a byte-at-a-time in a manner similar to other such timing side-channel /// attacks. /// /// The attack may seem somewhat counterintuitive since learning prefixes of a /// password hash does not reveal any additional information about the password /// itself. However, the above can be combined with an offline dictionary /// attack where the attacker is able to determine candidate passwords to send /// to the server by performing a brute force search offline and selecting /// candidate passwords whose hashes match the portion of the prefix they have /// learned so far. /// /// As the attacker learns a longer and longer prefix of the password hash, /// they are able to more effectively eliminate candidate passwords offline as /// part of a dictionary attack, until they eventually guess the correct /// password or exhaust their set of candidate passwords. /// /// ## Mitigations /// While we have taken care to ensure password hashes are compared in constant /// time, we would also suggest preventing such attacks by using randomly /// generated salts and keeping those salts secret. /// /// The [`SaltString::generate`][`crate::SaltString::generate`] function can be /// used to generate random high-entropy salt values. /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties /// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding /// [3]: https://web.archive.org/web/20130208100210/http://security-assessment.com/files/documents/presentations/TimingAttackPresentation2012.pdf #[derive(Copy, Clone, Eq)] pub struct Output { /// Byte array containing a password hashing function output. bytes: [u8; Self::MAX_LENGTH], /// Length of the password hashing function output in bytes. length: u8, /// Encoding which output should be serialized with. encoding: Encoding, } #[allow(clippy::len_without_is_empty)] impl Output { /// Minimum length of a [`Output`] string: 10-bytes. pub const MIN_LENGTH: usize = 10; /// Maximum length of [`Output`] string: 64-bytes. /// /// See type-level documentation about [`Output`] for more information. pub const MAX_LENGTH: usize = 64; /// Maximum length of [`Output`] when encoded as B64 string: 86-bytes /// (i.e. 86 ASCII characters) pub const B64_MAX_LENGTH: usize = ((Self::MAX_LENGTH * 4) / 3) + 1; /// Create a [`Output`] from the given byte slice, validating it according /// to [`Output::MIN_LENGTH`] and [`Output::MAX_LENGTH`] restrictions. pub fn new(input: &[u8]) -> Result { Self::init_with(input.len(), |bytes| { bytes.copy_from_slice(input); Ok(()) }) } /// Create a [`Output`] from the given byte slice and [`Encoding`], /// validating it according to [`Output::MIN_LENGTH`] and /// [`Output::MAX_LENGTH`] restrictions. pub fn new_with_encoding(input: &[u8], encoding: Encoding) -> Result { let mut result = Self::new(input)?; result.encoding = encoding; Ok(result) } /// Initialize an [`Output`] using the provided method, which is given /// a mutable byte slice into which it should write the output. /// /// The `output_size` (in bytes) must be known in advance, as well as at /// least [`Output::MIN_LENGTH`] bytes and at most [`Output::MAX_LENGTH`] /// bytes. pub fn init_with(output_size: usize, f: F) -> Result where F: FnOnce(&mut [u8]) -> Result<()>, { if output_size < Self::MIN_LENGTH { return Err(Error::OutputSize { provided: Ordering::Less, expected: Self::MIN_LENGTH, }); } if output_size > Self::MAX_LENGTH { return Err(Error::OutputSize { provided: Ordering::Greater, expected: Self::MAX_LENGTH, }); } let mut bytes = [0u8; Self::MAX_LENGTH]; f(&mut bytes[..output_size])?; Ok(Self { bytes, length: output_size as u8, encoding: Encoding::default(), }) } /// Borrow the output value as a byte slice. pub fn as_bytes(&self) -> &[u8] { &self.bytes[..self.len()] } /// Get the [`Encoding`] that this [`Output`] is serialized with. pub fn encoding(&self) -> Encoding { self.encoding } /// Get the length of the output value as a byte slice. pub fn len(&self) -> usize { usize::from(self.length) } /// Parse B64-encoded [`Output`], i.e. using the PHC string /// specification's restricted interpretation of Base64. pub fn b64_decode(input: &str) -> Result { Self::decode(input, Encoding::B64) } /// Write B64-encoded [`Output`] to the provided buffer, returning /// a sub-slice containing the encoded data. /// /// Returns an error if the buffer is too short to contain the output. pub fn b64_encode<'a>(&self, out: &'a mut [u8]) -> Result<&'a str> { self.encode(out, Encoding::B64) } /// Decode the given input string using the specified [`Encoding`]. pub fn decode(input: &str, encoding: Encoding) -> Result { let mut bytes = [0u8; Self::MAX_LENGTH]; encoding .decode(input, &mut bytes) .map_err(Into::into) .and_then(|decoded| Self::new_with_encoding(decoded, encoding)) } /// Encode this [`Output`] using the specified [`Encoding`]. pub fn encode<'a>(&self, out: &'a mut [u8], encoding: Encoding) -> Result<&'a str> { Ok(encoding.encode(self.as_ref(), out)?) } /// Get the length of this [`Output`] when encoded as B64. pub fn b64_len(&self) -> usize { Encoding::B64.encoded_len(self.as_ref()) } } impl AsRef<[u8]> for Output { fn as_ref(&self) -> &[u8] { self.as_bytes() } } impl ConstantTimeEq for Output { fn ct_eq(&self, other: &Self) -> Choice { self.as_ref().ct_eq(other.as_ref()) } } impl FromStr for Output { type Err = Error; fn from_str(s: &str) -> Result { Self::b64_decode(s) } } impl PartialEq for Output { fn eq(&self, other: &Self) -> bool { self.ct_eq(other).into() } } impl TryFrom<&[u8]> for Output { type Error = Error; fn try_from(input: &[u8]) -> Result { Self::new(input) } } impl fmt::Display for Output { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut buffer = [0u8; Self::B64_MAX_LENGTH]; self.encode(&mut buffer, self.encoding) .map_err(|_| fmt::Error) .and_then(|encoded| f.write_str(encoded)) } } impl fmt::Debug for Output { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Output(\"{}\")", self) } } #[cfg(test)] mod tests { use super::{Error, Ordering, Output}; #[test] fn new_with_valid_min_length_input() { let bytes = [10u8; 10]; let output = Output::new(&bytes).unwrap(); assert_eq!(output.as_ref(), &bytes); } #[test] fn new_with_valid_max_length_input() { let bytes = [64u8; 64]; let output = Output::new(&bytes).unwrap(); assert_eq!(output.as_ref(), &bytes); } #[test] fn reject_new_too_short() { let bytes = [9u8; 9]; let err = Output::new(&bytes).err().unwrap(); assert_eq!( err, Error::OutputSize { provided: Ordering::Less, expected: Output::MIN_LENGTH } ); } #[test] fn reject_new_too_long() { let bytes = [65u8; 65]; let err = Output::new(&bytes).err().unwrap(); assert_eq!( err, Error::OutputSize { provided: Ordering::Greater, expected: Output::MAX_LENGTH } ); } #[test] fn partialeq_true() { let a = Output::new(&[1u8; 32]).unwrap(); let b = Output::new(&[1u8; 32]).unwrap(); assert_eq!(a, b); } #[test] fn partialeq_false() { let a = Output::new(&[1u8; 32]).unwrap(); let b = Output::new(&[2u8; 32]).unwrap(); assert_ne!(a, b); } } password-hash-0.5.0/src/params.rs000064400000000000000000000305761046102023000150440ustar 00000000000000//! Algorithm parameters. use crate::errors::InvalidValue; use crate::{ value::{Decimal, Value}, Encoding, Error, Ident, Result, }; use core::{ fmt::{self, Debug, Write}, iter::FromIterator, str::{self, FromStr}, }; /// Individual parameter name/value pair. pub type Pair<'a> = (Ident<'a>, Value<'a>); /// Delimiter character between name/value pairs. pub(crate) const PAIR_DELIMITER: char = '='; /// Delimiter character between parameters. pub(crate) const PARAMS_DELIMITER: char = ','; /// Maximum number of supported parameters. const MAX_LENGTH: usize = 127; /// Error message used with `expect` for when internal invariants are violated /// (i.e. the contents of a [`ParamsString`] should always be valid) const INVARIANT_VIOLATED_MSG: &str = "PHC params invariant violated"; /// Algorithm parameter string. /// /// The [PHC string format specification][1] defines a set of optional /// algorithm-specific name/value pairs which can be encoded into a /// PHC-formatted parameter string as follows: /// /// ```text /// $=(,=)* /// ``` /// /// This type represents that set of parameters. /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#specification #[derive(Clone, Default, Eq, PartialEq)] pub struct ParamsString(Buffer); impl ParamsString { /// Create new empty [`ParamsString`]. pub fn new() -> Self { Self::default() } /// Add the given byte value to the [`ParamsString`], encoding it as "B64". pub fn add_b64_bytes<'a>(&mut self, name: impl TryInto>, bytes: &[u8]) -> Result<()> { if !self.is_empty() { self.0 .write_char(PARAMS_DELIMITER) .map_err(|_| Error::ParamsMaxExceeded)? } let name = name.try_into().map_err(|_| Error::ParamNameInvalid)?; // Add param name let offset = self.0.length; if write!(self.0, "{}=", name).is_err() { self.0.length = offset; return Err(Error::ParamsMaxExceeded); } // Encode B64 value let offset = self.0.length as usize; let written = Encoding::B64 .encode(bytes, &mut self.0.bytes[offset..])? .len(); self.0.length += written as u8; Ok(()) } /// Add a key/value pair with a decimal value to the [`ParamsString`]. pub fn add_decimal<'a>(&mut self, name: impl TryInto>, value: Decimal) -> Result<()> { let name = name.try_into().map_err(|_| Error::ParamNameInvalid)?; self.add(name, value) } /// Add a key/value pair with a string value to the [`ParamsString`]. pub fn add_str<'a>( &mut self, name: impl TryInto>, value: impl TryInto>, ) -> Result<()> { let name = name.try_into().map_err(|_| Error::ParamNameInvalid)?; let value = value .try_into() .map_err(|_| Error::ParamValueInvalid(InvalidValue::InvalidFormat))?; self.add(name, value) } /// Borrow the contents of this [`ParamsString`] as a byte slice. pub fn as_bytes(&self) -> &[u8] { self.as_str().as_bytes() } /// Borrow the contents of this [`ParamsString`] as a `str`. pub fn as_str(&self) -> &str { self.0.as_ref() } /// Get the count of the number ASCII characters in this [`ParamsString`]. pub fn len(&self) -> usize { self.as_str().len() } /// Is this set of parameters empty? pub fn is_empty(&self) -> bool { self.len() == 0 } /// Iterate over the parameters. pub fn iter(&self) -> Iter<'_> { Iter::new(self.as_str()) } /// Get a parameter [`Value`] by name. pub fn get<'a>(&self, name: impl TryInto>) -> Option> { let name = name.try_into().ok()?; for (n, v) in self.iter() { if name == n { return Some(v); } } None } /// Get a parameter as a `str`. pub fn get_str<'a>(&self, name: impl TryInto>) -> Option<&str> { self.get(name).map(|value| value.as_str()) } /// Get a parameter as a [`Decimal`]. /// /// See [`Value::decimal`] for format information. pub fn get_decimal<'a>(&self, name: impl TryInto>) -> Option { self.get(name).and_then(|value| value.decimal().ok()) } /// Add a value to this [`ParamsString`] using the provided callback. fn add(&mut self, name: Ident<'_>, value: impl fmt::Display) -> Result<()> { if self.get(name).is_some() { return Err(Error::ParamNameDuplicated); } let orig_len = self.0.length; if !self.is_empty() { self.0 .write_char(PARAMS_DELIMITER) .map_err(|_| Error::ParamsMaxExceeded)? } if write!(self.0, "{}={}", name, value).is_err() { self.0.length = orig_len; return Err(Error::ParamsMaxExceeded); } Ok(()) } } impl FromStr for ParamsString { type Err = Error; fn from_str(s: &str) -> Result { if s.as_bytes().len() > MAX_LENGTH { return Err(Error::ParamsMaxExceeded); } if s.is_empty() { return Ok(ParamsString::new()); } // Validate the string is well-formed for mut param in s.split(PARAMS_DELIMITER).map(|p| p.split(PAIR_DELIMITER)) { // Validate name param .next() .ok_or(Error::ParamNameInvalid) .and_then(Ident::try_from)?; // Validate value param .next() .ok_or(Error::ParamValueInvalid(InvalidValue::Malformed)) .and_then(Value::try_from)?; if param.next().is_some() { return Err(Error::ParamValueInvalid(InvalidValue::Malformed)); } } let mut bytes = [0u8; MAX_LENGTH]; bytes[..s.as_bytes().len()].copy_from_slice(s.as_bytes()); Ok(Self(Buffer { bytes, length: s.as_bytes().len() as u8, })) } } impl<'a> FromIterator> for ParamsString { fn from_iter(iter: I) -> Self where I: IntoIterator>, { let mut params = ParamsString::new(); for pair in iter { params.add_str(pair.0, pair.1).expect("PHC params error"); } params } } impl fmt::Display for ParamsString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } impl fmt::Debug for ParamsString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_map().entries(self.iter()).finish() } } /// Iterator over algorithm parameters stored in a [`ParamsString`] struct. pub struct Iter<'a> { inner: Option>, } impl<'a> Iter<'a> { /// Create a new [`Iter`]. fn new(s: &'a str) -> Self { if s.is_empty() { Self { inner: None } } else { Self { inner: Some(s.split(PARAMS_DELIMITER)), } } } } impl<'a> Iterator for Iter<'a> { type Item = Pair<'a>; fn next(&mut self) -> Option> { let mut param = self.inner.as_mut()?.next()?.split(PAIR_DELIMITER); let name = param .next() .and_then(|id| Ident::try_from(id).ok()) .expect(INVARIANT_VIOLATED_MSG); let value = param .next() .and_then(|value| Value::try_from(value).ok()) .expect(INVARIANT_VIOLATED_MSG); debug_assert_eq!(param.next(), None); Some((name, value)) } } /// Parameter buffer. #[derive(Clone, Debug, Eq)] struct Buffer { /// Byte array containing an ASCII-encoded string. bytes: [u8; MAX_LENGTH], /// Length of the string in ASCII characters (i.e. bytes). length: u8, } impl AsRef for Buffer { fn as_ref(&self) -> &str { str::from_utf8(&self.bytes[..(self.length as usize)]).expect(INVARIANT_VIOLATED_MSG) } } impl Default for Buffer { fn default() -> Buffer { Buffer { bytes: [0u8; MAX_LENGTH], length: 0, } } } impl PartialEq for Buffer { fn eq(&self, other: &Self) -> bool { // Ensure comparisons always honor the initialized portion of the buffer self.as_ref().eq(other.as_ref()) } } impl Write for Buffer { fn write_str(&mut self, input: &str) -> fmt::Result { let bytes = input.as_bytes(); let length = self.length as usize; if length + bytes.len() > MAX_LENGTH { return Err(fmt::Error); } self.bytes[length..(length + bytes.len())].copy_from_slice(bytes); self.length += bytes.len() as u8; Ok(()) } } #[cfg(test)] mod tests { use super::{Error, FromIterator, Ident, ParamsString, Value}; #[cfg(feature = "alloc")] use alloc::string::ToString; use core::str::FromStr; #[test] fn add() { let mut params = ParamsString::new(); params.add_str("a", "1").unwrap(); params.add_decimal("b", 2).unwrap(); params.add_str("c", "3").unwrap(); assert_eq!(params.iter().count(), 3); assert_eq!(params.get_decimal("a").unwrap(), 1); assert_eq!(params.get_decimal("b").unwrap(), 2); assert_eq!(params.get_decimal("c").unwrap(), 3); } #[test] #[cfg(feature = "alloc")] fn add_b64_bytes() { let mut params = ParamsString::new(); params.add_b64_bytes("a", &[1]).unwrap(); params.add_b64_bytes("b", &[2, 3]).unwrap(); params.add_b64_bytes("c", &[4, 5, 6]).unwrap(); assert_eq!(params.to_string(), "a=AQ,b=AgM,c=BAUG"); } #[test] fn duplicate_names() { let name = Ident::new("a").unwrap(); let mut params = ParamsString::new(); params.add_decimal(name, 1).unwrap(); let err = params.add_decimal(name, 2u32.into()).err().unwrap(); assert_eq!(err, Error::ParamNameDuplicated); } #[test] fn from_iter() { let params = ParamsString::from_iter( [ (Ident::new("a").unwrap(), Value::try_from("1").unwrap()), (Ident::new("b").unwrap(), Value::try_from("2").unwrap()), (Ident::new("c").unwrap(), Value::try_from("3").unwrap()), ] .iter() .cloned(), ); assert_eq!(params.iter().count(), 3); assert_eq!(params.get_decimal("a").unwrap(), 1); assert_eq!(params.get_decimal("b").unwrap(), 2); assert_eq!(params.get_decimal("c").unwrap(), 3); } #[test] fn iter() { let mut params = ParamsString::new(); params.add_str("a", "1").unwrap(); params.add_str("b", "2").unwrap(); params.add_str("c", "3").unwrap(); let mut i = params.iter(); for (name, value) in &[("a", "1"), ("b", "2"), ("c", "3")] { let name = Ident::new(name).unwrap(); let value = Value::try_from(*value).unwrap(); assert_eq!(i.next(), Some((name, value))); } assert_eq!(i.next(), None); } // // `FromStr` tests // #[test] fn parse_empty() { let params = ParamsString::from_str("").unwrap(); assert!(params.is_empty()); } #[test] fn parse_one() { let params = ParamsString::from_str("a=1").unwrap(); assert_eq!(params.iter().count(), 1); assert_eq!(params.get("a").unwrap().decimal().unwrap(), 1); } #[test] fn parse_many() { let params = ParamsString::from_str("a=1,b=2,c=3").unwrap(); assert_eq!(params.iter().count(), 3); assert_eq!(params.get_decimal("a").unwrap(), 1); assert_eq!(params.get_decimal("b").unwrap(), 2); assert_eq!(params.get_decimal("c").unwrap(), 3); } // // `Display` tests // #[test] #[cfg(feature = "alloc")] fn display_empty() { let params = ParamsString::new(); assert_eq!(params.to_string(), ""); } #[test] #[cfg(feature = "alloc")] fn display_one() { let params = ParamsString::from_str("a=1").unwrap(); assert_eq!(params.to_string(), "a=1"); } #[test] #[cfg(feature = "alloc")] fn display_many() { let params = ParamsString::from_str("a=1,b=2,c=3").unwrap(); assert_eq!(params.to_string(), "a=1,b=2,c=3"); } } password-hash-0.5.0/src/salt.rs000064400000000000000000000306301046102023000145130ustar 00000000000000//! Salt string support. use crate::{Encoding, Error, Result, Value}; use core::{fmt, str}; use crate::errors::InvalidValue; #[cfg(feature = "rand_core")] use rand_core::CryptoRngCore; /// Error message used with `expect` for when internal invariants are violated /// (i.e. the contents of a [`Salt`] should always be valid) const INVARIANT_VIOLATED_MSG: &str = "salt string invariant violated"; /// Salt string. /// /// In password hashing, a "salt" is an additional value used to /// personalize/tweak the output of a password hashing function for a given /// input password. /// /// Salts help defend against attacks based on precomputed tables of hashed /// passwords, i.e. "[rainbow tables][1]". /// /// The [`Salt`] type implements the RECOMMENDED best practices for salts /// described in the [PHC string format specification][2], namely: /// /// > - Maximum lengths for salt, output and parameter values are meant to help /// > consumer implementations, in particular written in C and using /// > stack-allocated buffers. These buffers must account for the worst case, /// > i.e. the maximum defined length. Therefore, keep these lengths low. /// > - The role of salts is to achieve uniqueness. A random salt is fine for /// > that as long as its length is sufficient; a 16-byte salt would work well /// > (by definition, UUID are very good salts, and they encode over exactly /// > 16 bytes). 16 bytes encode as 22 characters in B64. Functions should /// > disallow salt values that are too small for security (4 bytes should be /// > viewed as an absolute minimum). /// /// # Recommended length /// The recommended default length for a salt string is **16-bytes** (128-bits). /// /// See [`Salt::RECOMMENDED_LENGTH`] for more information. /// /// # Constraints /// Salt strings are constrained to the following set of characters per the /// PHC spec: /// /// > The salt consists in a sequence of characters in: `[a-zA-Z0-9/+.-]` /// > (lowercase letters, uppercase letters, digits, /, +, . and -). /// /// Additionally the following length restrictions are enforced based on the /// guidelines from the spec: /// /// - Minimum length: **4**-bytes /// - Maximum length: **64**-bytes /// /// A maximum length is enforced based on the above recommendation for /// supporting stack-allocated buffers (which this library uses), and the /// specific determination of 64-bytes is taken as a best practice from the /// [Argon2 Encoding][3] specification in the same document: /// /// > The length in bytes of the salt is between 8 and 64 bytes, thus /// > yielding a length in characters between 11 and 64 characters (and that /// > length is never equal to 1 modulo 4). The default byte length of the salt /// > is 16 bytes (22 characters in B64 encoding). An encoded UUID, or a /// > sequence of 16 bytes produced with a cryptographically strong PRNG, are /// > appropriate salt values. /// > /// > The Argon2 specification states that the salt can be much longer, up /// > to 2^32-1 bytes, but this makes little sense for password hashing. /// > Specifying a relatively small maximum length allows for parsing with a /// > stack allocated buffer.) /// /// Based on this guidance, this type enforces an upper bound of 64-bytes /// as a reasonable maximum, and recommends using 16-bytes. /// /// [1]: https://en.wikipedia.org/wiki/Rainbow_table /// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties /// [3]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding #[derive(Copy, Clone, Eq, PartialEq)] pub struct Salt<'a>(Value<'a>); #[allow(clippy::len_without_is_empty)] impl<'a> Salt<'a> { /// Minimum length of a [`Salt`] string: 4-bytes. pub const MIN_LENGTH: usize = 4; /// Maximum length of a [`Salt`] string: 64-bytes. /// /// See type-level documentation about [`Salt`] for more information. pub const MAX_LENGTH: usize = 64; /// Recommended length of a salt: 16-bytes. /// /// This recommendation comes from the [PHC string format specification]: /// /// > The role of salts is to achieve uniqueness. A *random* salt is fine /// > for that as long as its length is sufficient; a 16-byte salt would /// > work well (by definition, UUID are very good salts, and they encode /// > over exactly 16 bytes). 16 bytes encode as 22 characters in B64. /// /// [PHC string format specification]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#function-duties pub const RECOMMENDED_LENGTH: usize = 16; /// Create a [`Salt`] from the given B64-encoded input string, validating /// [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions. pub fn from_b64(input: &'a str) -> Result { let length = input.as_bytes().len(); if length < Self::MIN_LENGTH { return Err(Error::SaltInvalid(InvalidValue::TooShort)); } if length > Self::MAX_LENGTH { return Err(Error::SaltInvalid(InvalidValue::TooLong)); } // TODO(tarcieri): full B64 decoding check? for char in input.chars() { // From the PHC string format spec: // // > The salt consists in a sequence of characters in: `[a-zA-Z0-9/+.-]` // > (lowercase letters, uppercase letters, digits, /, +, . and -). if !matches!(char, 'a'..='z' | 'A'..='Z' | '0'..='9' | '/' | '+' | '.' | '-') { return Err(Error::SaltInvalid(InvalidValue::InvalidChar(char))); } } input.try_into().map(Self).map_err(|e| match e { Error::ParamValueInvalid(value_err) => Error::SaltInvalid(value_err), err => err, }) } /// Attempt to decode a B64-encoded [`Salt`] into bytes, writing the /// decoded output into the provided buffer, and returning a slice of the /// portion of the buffer containing the decoded result on success. pub fn decode_b64<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> { self.0.b64_decode(buf) } /// Borrow this value as a `str`. pub fn as_str(&self) -> &'a str { self.0.as_str() } /// Get the length of this value in ASCII characters. pub fn len(&self) -> usize { self.as_str().len() } /// Create a [`Salt`] from the given B64-encoded input string, validating /// [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions. #[deprecated(since = "0.5.0", note = "use `from_b64` instead")] pub fn new(input: &'a str) -> Result { Self::from_b64(input) } /// Attempt to decode a B64-encoded [`Salt`] into bytes, writing the /// decoded output into the provided buffer, and returning a slice of the /// portion of the buffer containing the decoded result on success. #[deprecated(since = "0.5.0", note = "use `decode_b64` instead")] pub fn b64_decode<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> { self.decode_b64(buf) } } impl<'a> AsRef for Salt<'a> { fn as_ref(&self) -> &str { self.as_str() } } impl<'a> TryFrom<&'a str> for Salt<'a> { type Error = Error; fn try_from(input: &'a str) -> Result { Self::from_b64(input) } } impl<'a> fmt::Display for Salt<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } impl<'a> fmt::Debug for Salt<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Salt({:?})", self.as_str()) } } /// Owned stack-allocated equivalent of [`Salt`]. #[derive(Clone, Eq)] pub struct SaltString { /// ASCII-encoded characters which comprise the salt. chars: [u8; Salt::MAX_LENGTH], /// Length of the string in ASCII characters (i.e. bytes). length: u8, } #[allow(clippy::len_without_is_empty)] impl SaltString { /// Generate a random B64-encoded [`SaltString`]. #[cfg(feature = "rand_core")] pub fn generate(mut rng: impl CryptoRngCore) -> Self { let mut bytes = [0u8; Salt::RECOMMENDED_LENGTH]; rng.fill_bytes(&mut bytes); Self::encode_b64(&bytes).expect(INVARIANT_VIOLATED_MSG) } /// Create a new [`SaltString`] from the given B64-encoded input string, /// validating [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions. pub fn from_b64(s: &str) -> Result { // Assert `s` parses successfully as a `Salt` Salt::from_b64(s)?; let len = s.as_bytes().len(); let mut bytes = [0u8; Salt::MAX_LENGTH]; bytes[..len].copy_from_slice(s.as_bytes()); Ok(SaltString { chars: bytes, length: len as u8, // `Salt::from_b64` check prevents overflow }) } /// Decode this [`SaltString`] from B64 into the provided output buffer. pub fn decode_b64<'a>(&self, buf: &'a mut [u8]) -> Result<&'a [u8]> { self.as_salt().decode_b64(buf) } /// Encode the given byte slice as B64 into a new [`SaltString`]. /// /// Returns `Error` if the slice is too long. pub fn encode_b64(input: &[u8]) -> Result { let mut bytes = [0u8; Salt::MAX_LENGTH]; let length = Encoding::B64.encode(input, &mut bytes)?.len() as u8; Ok(Self { chars: bytes, length, }) } /// Borrow the contents of a [`SaltString`] as a [`Salt`]. pub fn as_salt(&self) -> Salt<'_> { Salt::from_b64(self.as_str()).expect(INVARIANT_VIOLATED_MSG) } /// Borrow the contents of a [`SaltString`] as a `str`. pub fn as_str(&self) -> &str { str::from_utf8(&self.chars[..(self.length as usize)]).expect(INVARIANT_VIOLATED_MSG) } /// Get the length of this value in ASCII characters. pub fn len(&self) -> usize { self.as_str().len() } /// Create a new [`SaltString`] from the given B64-encoded input string, /// validating [`Salt::MIN_LENGTH`] and [`Salt::MAX_LENGTH`] restrictions. #[deprecated(since = "0.5.0", note = "use `from_b64` instead")] pub fn new(s: &str) -> Result { Self::from_b64(s) } /// Decode this [`SaltString`] from B64 into the provided output buffer. #[deprecated(since = "0.5.0", note = "use `decode_b64` instead")] pub fn b64_decode<'a>(&self, buf: &'a mut [u8]) -> Result<&'a [u8]> { self.decode_b64(buf) } /// Encode the given byte slice as B64 into a new [`SaltString`]. /// /// Returns `Error` if the slice is too long. #[deprecated(since = "0.5.0", note = "use `encode_b64` instead")] pub fn b64_encode(input: &[u8]) -> Result { Self::encode_b64(input) } } impl AsRef for SaltString { fn as_ref(&self) -> &str { self.as_str() } } impl PartialEq for SaltString { fn eq(&self, other: &Self) -> bool { // Ensure comparisons always honor the initialized portion of the buffer self.as_ref().eq(other.as_ref()) } } impl<'a> From<&'a SaltString> for Salt<'a> { fn from(salt_string: &'a SaltString) -> Salt<'a> { salt_string.as_salt() } } impl fmt::Display for SaltString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } impl fmt::Debug for SaltString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "SaltString({:?})", self.as_str()) } } #[cfg(test)] mod tests { use super::{Error, Salt}; use crate::errors::InvalidValue; #[test] fn new_with_valid_min_length_input() { let s = "abcd"; let salt = Salt::from_b64(s).unwrap(); assert_eq!(salt.as_ref(), s); } #[test] fn new_with_valid_max_length_input() { let s = "012345678911234567892123456789312345678941234567"; let salt = Salt::from_b64(s).unwrap(); assert_eq!(salt.as_ref(), s); } #[test] fn reject_new_too_short() { for &too_short in &["", "a", "ab", "abc"] { let err = Salt::from_b64(too_short).err().unwrap(); assert_eq!(err, Error::SaltInvalid(InvalidValue::TooShort)); } } #[test] fn reject_new_too_long() { let s = "01234567891123456789212345678931234567894123456785234567896234567"; let err = Salt::from_b64(s).err().unwrap(); assert_eq!(err, Error::SaltInvalid(InvalidValue::TooLong)); } #[test] fn reject_new_invalid_char() { let s = "01234_abcd"; let err = Salt::from_b64(s).err().unwrap(); assert_eq!(err, Error::SaltInvalid(InvalidValue::InvalidChar('_'))); } } password-hash-0.5.0/src/traits.rs000064400000000000000000000067711046102023000150670ustar 00000000000000//! Trait definitions. use crate::{Decimal, Error, Ident, ParamsString, PasswordHash, Result, Salt}; use core::fmt::Debug; /// Trait for password hashing functions. pub trait PasswordHasher { /// Algorithm-specific parameters. type Params: Clone + Debug + Default + for<'a> TryFrom<&'a PasswordHash<'a>, Error = Error> + TryInto; /// Compute a [`PasswordHash`] from the provided password using an /// explicit set of customized algorithm parameters as opposed to the /// defaults. /// /// When in doubt, use [`PasswordHasher::hash_password`] instead. fn hash_password_customized<'a>( &self, password: &[u8], algorithm: Option>, version: Option, params: Self::Params, salt: impl Into>, ) -> Result>; /// Simple API for computing a [`PasswordHash`] from a password and /// salt value. /// /// Uses the default recommended parameters for a given algorithm. fn hash_password<'a>( &self, password: &[u8], salt: impl Into>, ) -> Result> { self.hash_password_customized(password, None, None, Self::Params::default(), salt) } } /// Trait for password verification. /// /// Automatically impl'd for any type that impls [`PasswordHasher`]. /// /// This trait is object safe and can be used to implement abstractions over /// multiple password hashing algorithms. One such abstraction is provided by /// the [`PasswordHash::verify_password`] method. pub trait PasswordVerifier { /// Compute this password hashing function against the provided password /// using the parameters from the provided password hash and see if the /// computed output matches. fn verify_password(&self, password: &[u8], hash: &PasswordHash<'_>) -> Result<()>; } impl PasswordVerifier for T { fn verify_password(&self, password: &[u8], hash: &PasswordHash<'_>) -> Result<()> { if let (Some(salt), Some(expected_output)) = (&hash.salt, &hash.hash) { let computed_hash = self.hash_password_customized( password, Some(hash.algorithm), hash.version, T::Params::try_from(hash)?, *salt, )?; if let Some(computed_output) = &computed_hash.hash { // See notes on `Output` about the use of a constant-time comparison if expected_output == computed_output { return Ok(()); } } } Err(Error::Password) } } /// Trait for password hashing algorithms which support the legacy /// [Modular Crypt Format (MCF)][MCF]. /// /// [MCF]: https://passlib.readthedocs.io/en/stable/modular_crypt_format.html pub trait McfHasher { /// Upgrade an MCF hash to a PHC hash. MCF follow this rough format: /// /// ```text /// $$ /// ``` /// /// MCF hashes are otherwise largely unstructured and parsed according to /// algorithm-specific rules so hashers must parse a raw string themselves. fn upgrade_mcf_hash<'a>(&self, hash: &'a str) -> Result>; /// Verify a password hash in MCF format against the provided password. fn verify_mcf_hash(&self, password: &[u8], mcf_hash: &str) -> Result<()> where Self: PasswordVerifier, { self.verify_password(password, &self.upgrade_mcf_hash(mcf_hash)?) } } password-hash-0.5.0/src/value.rs000064400000000000000000000240161046102023000146650ustar 00000000000000//! Algorithm parameter value as defined by the [PHC string format]. //! //! Implements the following parts of the specification: //! //! > The value for each parameter consists in characters in: `[a-zA-Z0-9/+.-]` //! > (lowercase letters, uppercase letters, digits, /, +, . and -). No other //! > character is allowed. Interpretation of the value depends on the //! > parameter and the function. The function specification MUST unambiguously //! > define the set of valid parameter values. The function specification MUST //! > define a maximum length (in characters) for each parameter. For numerical //! > parameters, functions SHOULD use plain decimal encoding (other encodings //! > are possible as long as they are clearly defined). //! //! [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md use crate::errors::InvalidValue; use crate::{Encoding, Error, Result}; use core::{fmt, str}; /// Type used to represent decimal (i.e. integer) values. pub type Decimal = u32; /// Algorithm parameter value string. /// /// Parameter values are defined in the [PHC string format specification][1]. /// /// # Constraints /// - ASCII-encoded string consisting of the characters `[a-zA-Z0-9/+.-]` /// (lowercase letters, digits, and the minus sign) /// - Minimum length: 0 (i.e. empty values are allowed) /// - Maximum length: 64 ASCII characters (i.e. 64-bytes) /// /// # Additional Notes /// The PHC spec allows for algorithm-defined maximum lengths for parameter /// values, however this library defines a [`Value::MAX_LENGTH`] of 64 ASCII /// characters. /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md /// [2]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct Value<'a>(&'a str); impl<'a> Value<'a> { /// Maximum length of an [`Value`] - 64 ASCII characters (i.e. 64-bytes). /// /// This value is selected to match the maximum length of a [`Salt`][`crate::Salt`] /// as this library internally uses this type to represent salts. pub const MAX_LENGTH: usize = 64; /// Parse a [`Value`] from the provided `str`, validating it according to /// the PHC string format's rules. pub fn new(input: &'a str) -> Result { if input.as_bytes().len() > Self::MAX_LENGTH { return Err(Error::ParamValueInvalid(InvalidValue::TooLong)); } // Check that the characters are permitted in a PHC parameter value. assert_valid_value(input)?; Ok(Self(input)) } /// Attempt to decode a B64-encoded [`Value`], writing the decoded /// result into the provided buffer, and returning a slice of the buffer /// containing the decoded result on success. /// /// Examples of "B64"-encoded parameters in practice are the `keyid` and /// `data` parameters used by the [Argon2 Encoding][1] as described in the /// PHC string format specification. /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#argon2-encoding pub fn b64_decode<'b>(&self, buf: &'b mut [u8]) -> Result<&'b [u8]> { Ok(Encoding::B64.decode(self.as_str(), buf)?) } /// Borrow this value as a `str`. pub fn as_str(&self) -> &'a str { self.0 } /// Borrow this value as bytes. pub fn as_bytes(&self) -> &'a [u8] { self.as_str().as_bytes() } /// Get the length of this value in ASCII characters. pub fn len(&self) -> usize { self.as_str().len() } /// Is this value empty? pub fn is_empty(&self) -> bool { self.as_str().is_empty() } /// Attempt to parse this [`Value`] as a PHC-encoded decimal (i.e. integer). /// /// Decimal values are integers which follow the rules given in the /// ["Decimal Encoding" section of the PHC string format specification][1]. /// /// The decimal encoding rules are as follows: /// > For an integer value x, its decimal encoding consist in the following: /// > /// > - If x < 0, then its decimal encoding is the minus sign - followed by the decimal /// > encoding of -x. /// > - If x = 0, then its decimal encoding is the single character 0. /// > - If x > 0, then its decimal encoding is the smallest sequence of ASCII digits that /// > matches its value (i.e. there is no leading zero). /// > /// > Thus, a value is a valid decimal for an integer x if and only if all of the following hold true: /// > /// > - The first character is either a - sign, or an ASCII digit. /// > - All characters other than the first are ASCII digits. /// > - If the first character is - sign, then there is at least another character, and the /// > second character is not a 0. /// > - If the string consists in more than one character, then the first one cannot be a 0. /// /// Note: this implementation does not support negative decimals despite /// them being allowed per the spec above. If you need to parse a negative /// number, please parse it from the string representation directly e.g. /// `value.as_str().parse::()` /// /// [1]: https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#decimal-encoding pub fn decimal(&self) -> Result { let value = self.as_str(); // Empty strings aren't decimals if value.is_empty() { return Err(Error::ParamValueInvalid(InvalidValue::Malformed)); } // Ensure all characters are digits for c in value.chars() { if !c.is_ascii_digit() { return Err(Error::ParamValueInvalid(InvalidValue::InvalidChar(c))); } } // Disallow leading zeroes if value.starts_with('0') && value.len() > 1 { return Err(Error::ParamValueInvalid(InvalidValue::InvalidFormat)); } value.parse().map_err(|_| { // In theory a value overflow should be the only potential error here. // When `ParseIntError::kind` is stable it might be good to double check: // Error::ParamValueInvalid(InvalidValue::InvalidFormat) }) } /// Does this value parse successfully as a decimal? pub fn is_decimal(&self) -> bool { self.decimal().is_ok() } } impl<'a> AsRef for Value<'a> { fn as_ref(&self) -> &str { self.as_str() } } impl<'a> TryFrom<&'a str> for Value<'a> { type Error = Error; fn try_from(input: &'a str) -> Result { Self::new(input) } } impl<'a> TryFrom> for Decimal { type Error = Error; fn try_from(value: Value<'a>) -> Result { Decimal::try_from(&value) } } impl<'a> TryFrom<&Value<'a>> for Decimal { type Error = Error; fn try_from(value: &Value<'a>) -> Result { value.decimal() } } impl<'a> fmt::Display for Value<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.as_str()) } } /// Are all of the given bytes allowed in a [`Value`]? fn assert_valid_value(input: &str) -> Result<()> { for c in input.chars() { if !is_char_valid(c) { return Err(Error::ParamValueInvalid(InvalidValue::InvalidChar(c))); } } Ok(()) } /// Ensure the given ASCII character (i.e. byte) is allowed in a [`Value`]. fn is_char_valid(c: char) -> bool { matches!(c, 'A' ..= 'Z' | 'a'..='z' | '0'..='9' | '/' | '+' | '.' | '-') } #[cfg(test)] mod tests { use super::{Error, InvalidValue, Value}; // Invalid value examples const INVALID_CHAR: &str = "x;y"; const INVALID_TOO_LONG: &str = "01234567891123456789212345678931234567894123456785234567896234567"; const INVALID_CHAR_AND_TOO_LONG: &str = "0!234567891123456789212345678931234567894123456785234567896234567"; // // Decimal parsing tests // #[test] fn decimal_value() { let valid_decimals = &[("0", 0u32), ("1", 1u32), ("4294967295", u32::MAX)]; for &(s, i) in valid_decimals { let value = Value::new(s).unwrap(); assert!(value.is_decimal()); assert_eq!(value.decimal().unwrap(), i) } } #[test] fn reject_decimal_with_leading_zero() { let value = Value::new("01").unwrap(); let err = u32::try_from(value).err().unwrap(); assert!(matches!( err, Error::ParamValueInvalid(InvalidValue::InvalidFormat) )); } #[test] fn reject_overlong_decimal() { let value = Value::new("4294967296").unwrap(); let err = u32::try_from(value).err().unwrap(); assert_eq!(err, Error::ParamValueInvalid(InvalidValue::InvalidFormat)); } #[test] fn reject_negative() { let value = Value::new("-1").unwrap(); let err = u32::try_from(value).err().unwrap(); assert!(matches!( err, Error::ParamValueInvalid(InvalidValue::InvalidChar(_)) )); } // // String parsing tests // #[test] fn string_value() { let valid_examples = [ "", "X", "x", "xXx", "a+b.c-d", "1/2", "01234567891123456789212345678931", ]; for &example in &valid_examples { let value = Value::new(example).unwrap(); assert_eq!(value.as_str(), example); } } #[test] fn reject_invalid_char() { let err = Value::new(INVALID_CHAR).err().unwrap(); assert!(matches!( err, Error::ParamValueInvalid(InvalidValue::InvalidChar(_)) )); } #[test] fn reject_too_long() { let err = Value::new(INVALID_TOO_LONG).err().unwrap(); assert_eq!(err, Error::ParamValueInvalid(InvalidValue::TooLong)); } #[test] fn reject_invalid_char_and_too_long() { let err = Value::new(INVALID_CHAR_AND_TOO_LONG).err().unwrap(); assert_eq!(err, Error::ParamValueInvalid(InvalidValue::TooLong)); } } password-hash-0.5.0/tests/encoding.rs000064400000000000000000000023761046102023000157170ustar 00000000000000//! Base64 encoding tests. //! //! # B64 Notes //! //! "B64" is a ubset of the standard Base64 encoding (RFC 4648, section 4) which //! omits padding (`=`) as well as extra whitespace, as described in the PHC //! string format specification: //! //! use password_hash::{Output, Salt}; // Example salt encoded as a B64 string. const EXAMPLE_SALT_B64: &str = "REVBREJFRUZERUFEQkVFRg"; const EXAMPLE_SALT_RAW: &[u8] = b"DEADBEEFDEADBEEF"; // Example PHF output encoded as a B64 string. const EXAMPLE_OUTPUT_B64: &str = "REVBREJFRUZERUFEQkVFRkRFQURCRUVGREVBREJFRUZERUFEQkVFRkRFQURCRUVGREVBREJFRUZERUFEQkVFRg"; const EXAMPLE_OUTPUT_RAW: &[u8] = b"DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF"; #[test] fn salt_roundtrip() { let mut buffer = [0u8; 64]; let salt = Salt::from_b64(EXAMPLE_SALT_B64).unwrap(); assert_eq!(salt.as_ref(), EXAMPLE_SALT_B64); let salt_decoded = salt.decode_b64(&mut buffer).unwrap(); assert_eq!(salt_decoded, EXAMPLE_SALT_RAW); } #[test] fn output_roundtrip() { let out = EXAMPLE_OUTPUT_B64.parse::().unwrap(); assert_eq!(out.as_ref(), EXAMPLE_OUTPUT_RAW); assert_eq!(out.to_string(), EXAMPLE_OUTPUT_B64); } password-hash-0.5.0/tests/hashing.rs000064400000000000000000000042531046102023000155460ustar 00000000000000//! Password hashing tests pub use password_hash::{ Decimal, Error, Ident, Output, ParamsString, PasswordHash, PasswordHasher, Result, Salt, }; const ALG: Ident = Ident::new_unwrap("example"); /// Stub password hashing function for testing. pub struct StubPasswordHasher; impl PasswordHasher for StubPasswordHasher { type Params = StubParams; fn hash_password_customized<'a>( &self, password: &[u8], algorithm: Option>, version: Option, params: StubParams, salt: impl Into>, ) -> Result> { let salt = salt.into(); let mut output = Vec::new(); if let Some(alg) = algorithm { if alg != ALG { return Err(Error::Algorithm); } } for slice in &[b"pw", password, b",salt:", salt.as_str().as_bytes()] { output.extend_from_slice(slice); } let hash = Output::new(&output)?; Ok(PasswordHash { algorithm: ALG, version, params: params.try_into()?, salt: Some(salt), hash: Some(hash), }) } } /// Stub parameters #[derive(Clone, Debug, Default)] pub struct StubParams; impl<'a> TryFrom<&PasswordHash<'a>> for StubParams { type Error = Error; fn try_from(_: &PasswordHash<'a>) -> Result { Ok(Self) } } impl<'a> TryFrom for ParamsString { type Error = Error; fn try_from(_: StubParams) -> Result { Ok(Self::default()) } } #[test] fn verify_password_hash() { let valid_password = "test password"; let salt = Salt::from_b64("test-salt").unwrap(); let hash = PasswordHash::generate(StubPasswordHasher, valid_password, salt).unwrap(); // Sanity tests for StubFunction impl above assert_eq!(hash.algorithm, ALG); assert_eq!(hash.salt.unwrap(), salt); // Tests for generic password verification logic assert_eq!( hash.verify_password(&[&StubPasswordHasher], valid_password), Ok(()) ); assert_eq!( hash.verify_password(&[&StubPasswordHasher], "wrong password"), Err(Error::Password) ); } password-hash-0.5.0/tests/password_hash.rs000064400000000000000000000074121046102023000167720ustar 00000000000000//! Tests for `PasswordHash` encoding/decoding. //! //! Each test implements a different permutation of the possible combinations //! of the string encoding, and ensures password hashes round trip under each //! of the conditions. use password_hash::{Ident, ParamsString, PasswordHash, Salt}; const EXAMPLE_ALGORITHM: Ident = Ident::new_unwrap("argon2d"); const EXAMPLE_SALT: &str = "saltsaltsaltsaltsalt"; const EXAMPLE_HASH: &[u8] = &[ 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, 0x21, 0x85, 0xab, ]; /// Example parameters fn example_params() -> ParamsString { let mut params = ParamsString::new(); params.add_decimal("a", 1).unwrap(); params.add_decimal("b", 2).unwrap(); params.add_decimal("c", 3).unwrap(); params } #[test] fn algorithm_alone() { let ph = PasswordHash::new("$argon2d").unwrap(); assert_eq!(ph.algorithm, EXAMPLE_ALGORITHM); let s = ph.to_string(); assert_eq!(s, "$argon2d"); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } #[test] fn params() { let ph = PasswordHash { algorithm: EXAMPLE_ALGORITHM, version: None, params: example_params(), salt: None, hash: None, }; let s = ph.to_string(); assert_eq!(s, "$argon2d$a=1,b=2,c=3"); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } #[test] fn salt() { let ph = PasswordHash { algorithm: EXAMPLE_ALGORITHM, version: None, params: ParamsString::new(), salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), hash: None, }; let s = ph.to_string(); assert_eq!(s, "$argon2d$saltsaltsaltsaltsalt"); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } #[test] fn one_param_and_salt() { let mut params = ParamsString::new(); params.add_decimal("a", 1).unwrap(); let ph = PasswordHash { algorithm: EXAMPLE_ALGORITHM, version: None, params, salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), hash: None, }; let s = ph.to_string(); assert_eq!(s, "$argon2d$a=1$saltsaltsaltsaltsalt"); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } #[test] fn params_and_salt() { let ph = PasswordHash { algorithm: EXAMPLE_ALGORITHM, version: None, params: example_params(), salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), hash: None, }; let s = ph.to_string(); assert_eq!(s, "$argon2d$a=1,b=2,c=3$saltsaltsaltsaltsalt"); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } #[test] fn salt_and_hash() { let ph = PasswordHash { algorithm: EXAMPLE_ALGORITHM, version: None, params: ParamsString::default(), salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), hash: Some(EXAMPLE_HASH.try_into().unwrap()), }; let s = ph.to_string(); assert_eq!( s, "$argon2d$saltsaltsaltsaltsalt$hashhashhashhashhashhashhashhashhashhashhas" ); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } #[test] fn all_fields() { let ph = PasswordHash { algorithm: EXAMPLE_ALGORITHM, version: None, params: example_params(), salt: Some(Salt::from_b64(EXAMPLE_SALT).unwrap()), hash: Some(EXAMPLE_HASH.try_into().unwrap()), }; let s = ph.to_string(); assert_eq!( s, "$argon2d$a=1,b=2,c=3$saltsaltsaltsaltsalt$hashhashhashhashhashhashhashhashhashhashhas" ); let ph2 = PasswordHash::try_from(s.as_str()).unwrap(); assert_eq!(ph, ph2); } password-hash-0.5.0/tests/test_vectors.rs000064400000000000000000000035221046102023000166470ustar 00000000000000//! Test vectors for commonly used password hashing algorithms. use password_hash::{Ident, PasswordHash}; const ARGON2D_HASH: &str = "$argon2d$v=19$m=512,t=3,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ"; const BCRYPT_HASH: &str = "$2b$MTIzNA$i5btSOiulHhaPHPbgNUGdObga/GCAVG/y5HHY1ra7L0C9dpCaw8u"; const SCRYPT_HASH: &str = "$scrypt$epIxT/h6HbbwHaehFnh/bw$7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0"; #[test] fn argon2id() { let ph = PasswordHash::new(ARGON2D_HASH).unwrap(); assert_eq!(ph.algorithm, Ident::new("argon2d").unwrap()); assert_eq!(ph.version, Some(19)); assert_eq!(ph.params.iter().count(), 3); assert_eq!(ph.params.get_decimal("m").unwrap(), 512); assert_eq!(ph.params.get_decimal("t").unwrap(), 3); assert_eq!(ph.params.get_decimal("p").unwrap(), 2); assert_eq!(ph.salt.unwrap().as_ref(), "5VtWOO3cGWYQHEMaYGbsfQ"); assert_eq!(ph.hash.unwrap().to_string(), "AcmqasQgW/wI6wAHAMk4aQ"); assert_eq!(ph.to_string(), ARGON2D_HASH); } #[test] fn bcrypt() { let ph = PasswordHash::new(BCRYPT_HASH).unwrap(); assert_eq!(ph.algorithm, Ident::new("2b").unwrap()); assert_eq!(ph.version, None); assert_eq!(ph.params.len(), 0); assert_eq!(ph.salt.unwrap().to_string(), "MTIzNA"); assert_eq!( ph.hash.unwrap().to_string(), "i5btSOiulHhaPHPbgNUGdObga/GCAVG/y5HHY1ra7L0C9dpCaw8u" ); assert_eq!(ph.to_string(), BCRYPT_HASH); } #[test] fn scrypt() { let ph = PasswordHash::new(SCRYPT_HASH).unwrap(); assert_eq!(ph.algorithm, Ident::new("scrypt").unwrap()); assert_eq!(ph.version, None); assert_eq!(ph.params.len(), 0); assert_eq!(ph.salt.unwrap().to_string(), "epIxT/h6HbbwHaehFnh/bw"); assert_eq!( ph.hash.unwrap().to_string(), "7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0" ); assert_eq!(ph.to_string(), SCRYPT_HASH); }