diff --git a/.github/workflows/balloon.yml b/.github/workflows/balloon.yml new file mode 100644 index 00000000..a68787af --- /dev/null +++ b/.github/workflows/balloon.yml @@ -0,0 +1,57 @@ +name: balloon + +on: + pull_request: + paths: + - "balloon/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: balloon + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.56.1 # MSRV + - stable + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + override: true + - run: cargo build --target ${{ matrix.target }} --release --no-default-features + - run: cargo build --target ${{ matrix.target }} --release --no-default-features --features password-hash + - run: cargo build --target ${{ matrix.target }} --release + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.56.1 # MSRV + - stable + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - run: cargo test --release + - run: cargo test --release --all-features diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index 90c59925..0350ef95 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.51.0 # MSRV + toolchain: 1.56.1 # MSRV components: clippy override: true profile: minimal diff --git a/Cargo.lock b/Cargo.lock index 96688d49..8194d55f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "balloon-hash" +version = "0.1.0" +dependencies = [ + "crypto-bigint", + "digest", + "hex-literal", + "password-hash", + "rayon", + "sha2", +] + [[package]] name = "base64ct" version = "1.0.1" @@ -140,6 +152,16 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-bigint" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "476ecdba12db8402a1664482de8c37fff7dc96241258bd0de1f0d70e760a45fd" +dependencies = [ + "generic-array", + "subtle", +] + [[package]] name = "crypto-common" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index ae8c916d..e3f4a849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "argon2", + "balloon", "bcrypt-pbkdf", "pbkdf2", "scrypt", diff --git a/README.md b/README.md index 7bcffe4f..5ae41e02 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Collection of password hashing algorithms, otherwise known as password-based key | Algorithm | Crate | Crates.io | Documentation | MSRV | |-----------|-------|:----------:|:-------------:|:----:| | [Argon2] | [`argon2`] | [![crates.io](https://img.shields.io/crates/v/argon2.svg)](https://crates.io/crates/argon2) | [![Documentation](https://docs.rs/argon2/badge.svg)](https://docs.rs/argon2) | ![MSRV 1.51][msrv-1.51] | +| [Balloon] | [`balloon`] | [![crates.io](https://img.shields.io/crates/v/balloon-hash.svg)](https://crates.io/crates/balloon-hash) | [![Documentation](https://docs.rs/balloon-hash/badge.svg)](https://docs.rs/balloon-hash) | ![MSRV 1.56][msrv-1.56] | | [bcrypt-pbkdf] | [`bcrypt-pbkdf`] |[![crates.io](https://img.shields.io/crates/v/bcrypt-pbkdf.svg)](https://crates.io/crates/bcrypt-pbkdf) | [![Documentation](https://docs.rs/bcrypt-pbkdf/badge.svg)](https://docs.rs/bcrypt-pbkdf) | ![MSRV 1.51][msrv-1.51] | | [PBKDF2] | [`pbkdf2`] | [![crates.io](https://img.shields.io/crates/v/pbkdf2.svg)](https://crates.io/crates/pbkdf2) | [![Documentation](https://docs.rs/pbkdf2/badge.svg)](https://docs.rs/pbkdf2) | ![MSRV 1.51][msrv-1.51] | | [scrypt] | [`scrypt`] | [![crates.io](https://img.shields.io/crates/v/scrypt.svg)](https://crates.io/crates/scrypt) | [![Documentation](https://docs.rs/scrypt/badge.svg)](https://docs.rs/scrypt) | ![MSRV 1.51][msrv-1.51] | @@ -41,10 +42,12 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [deps-image]: https://deps.rs/repo/github/RustCrypto/password-hashes/status.svg [deps-link]: https://deps.rs/repo/github/RustCrypto/password-hashes [msrv-1.51]: https://img.shields.io/badge/rustc-1.51.0+-blue.svg +[msrv-1.56]: https://img.shields.io/badge/rustc-1.56.0+-blue.svg [//]: # (crates) [`argon2`]: ./argon2 +[`balloon`]: ./balloon [`bcrypt-pbkdf`]: ./bcrypt-pbkdf [`pbkdf2`]: ./pbkdf2 [`scrypt`]: ./scrypt @@ -53,6 +56,7 @@ Unless you explicitly state otherwise, any contribution intentionally submitted [//]: # (general links) [Argon2]: https://en.wikipedia.org/wiki/Argon2 +[Balloon]: https://en.wikipedia.org/wiki/Balloon_hashing [bcrypt-pbkdf]: https://flak.tedunangst.com/post/bcrypt-pbkdf [PBKDF2]: https://en.wikipedia.org/wiki/PBKDF2 [scrypt]: https://en.wikipedia.org/wiki/Scrypt diff --git a/balloon/CHANGELOG.md b/balloon/CHANGELOG.md new file mode 100644 index 00000000..425b5f82 --- /dev/null +++ b/balloon/CHANGELOG.md @@ -0,0 +1,9 @@ +# 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.1.0 (2021-09-01) +- Initial release diff --git a/balloon/Cargo.toml b/balloon/Cargo.toml new file mode 100644 index 00000000..299aa6b6 --- /dev/null +++ b/balloon/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "balloon-hash" +version = "0.1.0" # Also update html_root_url in lib.rs when bumping this +description = "Pure Rust implementation of the Balloon password hashing function" +authors = ["RustCrypto Developers"] +license = "MIT OR Apache-2.0" +documentation = "https://docs.rs/balloon-hash" +repository = "https://github.com/RustCrypto/password-hashes/tree/master/balloon" +keywords = ["crypto", "password", "hashing"] +categories = ["cryptography", "no-std"] +edition = "2021" +rust-version = "1.56.1" +readme = "README.md" + +[dependencies] +digest = { version = "0.10", default-features = false } +crypto-bigint = { version = "0.3", default-features = false, features = ["generic-array"] } + +# optional dependencies +password-hash = { version = "0.3", default-features = false, optional = true } +rayon = { version = "1", optional = true } + +[dev-dependencies] +hex-literal = "0.3" +sha2 = "0.10" + +[features] +default = ["alloc", "password-hash", "rand"] +alloc = [] +parallel = ["rayon", "std"] +rand = ["password-hash/rand_core"] +std = ["alloc", "password-hash/std"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/balloon/LICENSE-APACHE b/balloon/LICENSE-APACHE new file mode 100644 index 00000000..78173fa2 --- /dev/null +++ b/balloon/LICENSE-APACHE @@ -0,0 +1,201 @@ + 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. diff --git a/balloon/LICENSE-MIT b/balloon/LICENSE-MIT new file mode 100644 index 00000000..c869ada5 --- /dev/null +++ b/balloon/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2021 The RustCrypto Project 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. diff --git a/balloon/README.md b/balloon/README.md new file mode 100644 index 00000000..8902b5e6 --- /dev/null +++ b/balloon/README.md @@ -0,0 +1,56 @@ +# RustCrypto: Balloon + +[![crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] +![Apache2/MIT licensed][license-image] +![Rust Version][rustc-image] +[![Project Chat][chat-image]][chat-link] +[![Build Status][build-image]][build-link] + +Pure Rust implementation of the [Balloon] password hashing function. + +[Documentation][docs-link] + +## Minimum Supported Rust Version + +Rust **1.56** or higher. + +Minimum supported Rust version can be changed in the future, but it will be +done with 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](http://www.apache.org/licenses/LICENSE-2.0) + * [MIT license](http://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://img.shields.io/crates/v/balloon-hash.svg +[crate-link]: https://crates.io/crates/balloon-hash +[docs-image]: https://docs.rs/balloon-hash/badge.svg +[docs-link]: https://docs.rs/balloon-hash/ +[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.56+-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 +[build-image]: https://github.com/RustCrypto/password-hashes/workflows/balloon/badge.svg?branch=master&event=push +[build-link]: https://github.com/RustCrypto/password-hashes/actions?query=workflow%3Aballoon + +[//]: # (general links) + +[Balloon]: https://en.wikipedia.org/wiki/Balloon_hashing diff --git a/balloon/src/algorithm.rs b/balloon/src/algorithm.rs new file mode 100644 index 00000000..ebea21a1 --- /dev/null +++ b/balloon/src/algorithm.rs @@ -0,0 +1,110 @@ +//! Balloon algorithms (e.g. Balloon, BalloonM). + +use crate::{Error, Result}; +use core::{ + fmt::{self, Display}, + str::FromStr, +}; + +#[cfg(feature = "password-hash")] +use {core::convert::TryFrom, password_hash::Ident}; + +/// Balloon primitive type: variants of the algorithm. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] +pub enum Algorithm { + /// Standard Balloon hashing algorithm. + Balloon, + + /// M-core variant of the Balloon hashing algorithm. + /// + /// Supports parallelism by computing M instances of the + /// single-core Balloon function and XORing all the outputs. + BalloonM, +} + +impl Default for Algorithm { + fn default() -> Algorithm { + Algorithm::BalloonM + } +} + +impl Algorithm { + /// Balloon algorithm identifier + #[cfg(feature = "password-hash")] + #[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] + pub const BALLOON_IDENT: Ident<'static> = Ident::new("balloon"); + + /// BalloonM algorithm identifier + #[cfg(feature = "password-hash")] + #[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] + pub const BALLOON_M_IDENT: Ident<'static> = Ident::new("balloon-m"); + + /// Parse an [`Algorithm`] from the provided string. + pub fn new(id: impl AsRef) -> Result { + id.as_ref().parse() + } + + /// Get the identifier string for this Balloon [`Algorithm`]. + pub fn as_str(&self) -> &str { + match self { + Algorithm::Balloon => "balloon", + Algorithm::BalloonM => "balloon-m", + } + } + + /// Get the [`Ident`] that corresponds to this Balloon [`Algorithm`]. + #[cfg(feature = "password-hash")] + #[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] + pub fn ident(&self) -> Ident<'static> { + match self { + Algorithm::Balloon => Self::BALLOON_IDENT, + Algorithm::BalloonM => Self::BALLOON_M_IDENT, + } + } +} + +impl AsRef for Algorithm { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl Display for Algorithm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.as_str()) + } +} + +impl FromStr for Algorithm { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "balloon" => Ok(Algorithm::Balloon), + "balloon-m" => Ok(Algorithm::BalloonM), + _ => Err(Error::AlgorithmInvalid), + } + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl From for Ident<'static> { + fn from(alg: Algorithm) -> Ident<'static> { + alg.ident() + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl<'a> TryFrom> for Algorithm { + type Error = password_hash::Error; + + fn try_from(ident: Ident<'a>) -> password_hash::Result { + match ident { + Self::BALLOON_IDENT => Ok(Algorithm::Balloon), + Self::BALLOON_M_IDENT => Ok(Algorithm::BalloonM), + _ => Err(password_hash::Error::Algorithm), + } + } +} diff --git a/balloon/src/balloon.rs b/balloon/src/balloon.rs new file mode 100644 index 00000000..7fe3dbb5 --- /dev/null +++ b/balloon/src/balloon.rs @@ -0,0 +1,203 @@ +use crate::error::{Error, Result}; +use crate::Params; +use core::mem; +use crypto_bigint::{ArrayDecoding, ArrayEncoding, NonZero}; +use digest::generic_array::GenericArray; +use digest::{Digest, FixedOutputReset}; + +pub fn balloon( + pwd: &[u8], + salt: &[u8], + secret: Option<&[u8]>, + params: Params, + memory_blocks: &mut [GenericArray], +) -> Result> +where + GenericArray: ArrayDecoding, +{ + if params.p_cost.get() == 1 { + hash_internal::(pwd, salt, secret, params, memory_blocks, None) + } else { + Err(Error::ThreadsTooMany) + } +} + +pub fn balloon_m( + pwd: &[u8], + salt: &[u8], + secret: Option<&[u8]>, + params: Params, + memory_blocks: &mut [GenericArray], +) -> Result> +where + GenericArray: ArrayDecoding, +{ + #[cfg(not(feature = "parallel"))] + let output = { + let mut output = GenericArray::<_, D::OutputSize>::default(); + + for thread in 1..=u64::from(params.p_cost.get()) { + let hash = hash_internal::(pwd, salt, secret, params, memory_blocks, Some(thread))?; + output = output.into_iter().zip(hash).map(|(a, b)| a ^ b).collect(); + } + + output + }; + + #[cfg(feature = "parallel")] + let output = { + use rayon::iter::{ParallelBridge, ParallelIterator}; + + if memory_blocks.len() < (params.s_cost.get() * params.p_cost.get()) as usize { + return Err(Error::MemoryTooLittle); + } + + // Shortcut if p_cost is one. + if params.p_cost.get() == 1 { + hash_internal::(pwd, salt, secret, params, memory_blocks, Some(1)) + } else { + (1..=u64::from(params.p_cost.get())) + .zip(memory_blocks.chunks_exact_mut(params.s_cost.get() as usize)) + .par_bridge() + .map_with((params, secret), |(params, secret), (thread, memory)| { + hash_internal::(pwd, salt, *secret, *params, memory, Some(thread)) + }) + .try_reduce(GenericArray::default, |a, b| { + Ok(a.into_iter().zip(b).map(|(a, b)| a ^ b).collect()) + }) + }? + }; + + let mut digest = D::new(); + Digest::update(&mut digest, pwd); + Digest::update(&mut digest, salt); + + if let Some(secret) = secret { + Digest::update(&mut digest, secret); + } + + Digest::update(&mut digest, output); + Ok(digest.finalize_reset()) +} + +fn hash_internal( + pwd: &[u8], + salt: &[u8], + secret: Option<&[u8]>, + params: Params, + memory_blocks: &mut [GenericArray], + thread_id: Option, +) -> Result> +where + GenericArray: ArrayDecoding, +{ + // we will use `s_cost` to index arrays regularly + let s_cost = params.s_cost.get() as usize; + let s_cost_bigint = { + let mut s_cost = GenericArray::::default(); + s_cost[..mem::size_of::()].copy_from_slice(¶ms.s_cost.get().to_le_bytes()); + NonZero::new(s_cost.into_uint_le()).unwrap() + }; + + let mut digest = D::new(); + + // This is a direct translation of the `Balloon` from chapter 3.1. + // int delta = 3 // Number of dependencies per block + const DELTA: u64 = 3; + // int cnt = 0 // A counter (used in security proof) + let mut cnt: u64 = 0; + // block_t buf[s_cost]): // The main buffer + let buf = memory_blocks + .get_mut(..s_cost) + .ok_or(Error::MemoryTooLittle)?; + + // Step 1. Expand input into buffer. + // buf[0] = hash(cnt++, passwd, salt) + Digest::update(&mut digest, cnt.to_le_bytes()); + cnt += 1; + Digest::update(&mut digest, pwd); + Digest::update(&mut digest, salt); + + if let Some(secret) = secret { + Digest::update(&mut digest, secret); + } + + if let Some(thread_id) = thread_id { + Digest::update(&mut digest, thread_id.to_le_bytes()); + } + + buf[0] = digest.finalize_reset(); + + // for m from 1 to s_cost-1: + for m in 1..s_cost { + // buf[m] = hash(cnt++, buf[m-1]) + Digest::update(&mut digest, &cnt.to_le_bytes()); + cnt += 1; + Digest::update(&mut digest, &buf[m - 1]); + buf[m] = digest.finalize_reset(); + } + + // Step 2. Mix buffer contents. + // for t from 0 to t_cost-1: + for t in 0..u64::from(params.t_cost.get()) { + // for m from 0 to s_cost-1: + for m in 0..s_cost { + // Step 2a. Hash last and current blocks. + // block_t prev = buf[(m-1) mod s_cost] + let prev = if m == 0 { + buf.last().unwrap() + } else { + &buf[m - 1] + }; + + // buf[m] = hash(cnt++, prev, buf[m]) + Digest::update(&mut digest, &cnt.to_le_bytes()); + cnt += 1; + Digest::update(&mut digest, prev); + Digest::update(&mut digest, &buf[m]); + buf[m] = digest.finalize_reset(); + + // Step 2b. Hash in pseudorandomly chosen blocks. + // for i from 0 to delta-1: + for i in 0..DELTA { + // block_t idx_block = ints_to_block(t, m, i) + Digest::update(&mut digest, &t.to_le_bytes()); + Digest::update(&mut digest, &(m as u64).to_le_bytes()); + Digest::update(&mut digest, &i.to_le_bytes()); + let idx_block = digest.finalize_reset(); + + // int other = to_int(hash(cnt++, salt, idx_block)) mod s_cost + Digest::update(&mut digest, &cnt.to_le_bytes()); + cnt += 1; + Digest::update(&mut digest, salt); + + if let Some(secret) = secret { + Digest::update(&mut digest, secret); + } + + if let Some(thread_id) = thread_id { + Digest::update(&mut digest, thread_id.to_le_bytes()); + } + + Digest::update(&mut digest, idx_block); + let other = digest.finalize_reset().into_uint_le() % s_cost_bigint; + let other = usize::from_le_bytes( + other.to_le_byte_array()[..mem::size_of::()] + .try_into() + .unwrap(), + ); + + // buf[m] = hash(cnt++, buf[m], buf[other]) + Digest::update(&mut digest, &cnt.to_le_bytes()); + cnt += 1; + Digest::update(&mut digest, &buf[m]); + Digest::update(&mut digest, &buf[other]); + buf[m] = digest.finalize_reset(); + } + } + } + + // Step 3. Extract output from buffer. + // return buf[s_cost-1] + Ok(buf.last().unwrap().clone()) +} diff --git a/balloon/src/error.rs b/balloon/src/error.rs new file mode 100644 index 00000000..771996e6 --- /dev/null +++ b/balloon/src/error.rs @@ -0,0 +1,53 @@ +//! Error type + +use core::fmt; + +#[cfg(feature = "password-hash")] +use password_hash::errors::InvalidValue; + +/// Result with balloon's [`Error`] type. +pub type Result = core::result::Result; + +/// Error type. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Error { + /// Algorithm identifier invalid. + AlgorithmInvalid, + /// Memory cost is too small. + MemoryTooLittle, + /// Not enough threads. + ThreadsTooFew, + /// Too many threads. + ThreadsTooMany, + /// Time cost is too small. + TimeTooSmall, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Error::AlgorithmInvalid => "algorithm identifier invalid", + Error::MemoryTooLittle => "memory cost is too small", + Error::ThreadsTooFew => "not enough threads", + Error::ThreadsTooMany => "too many threads", + Error::TimeTooSmall => "time cost is too small", + }) + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl From for password_hash::Error { + fn from(err: Error) -> password_hash::Error { + match err { + Error::AlgorithmInvalid => password_hash::Error::Algorithm, + Error::MemoryTooLittle => InvalidValue::TooShort.param_error(), + Error::ThreadsTooFew => InvalidValue::TooShort.param_error(), + Error::ThreadsTooMany => InvalidValue::TooLong.param_error(), + Error::TimeTooSmall => InvalidValue::TooShort.param_error(), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} diff --git a/balloon/src/lib.rs b/balloon/src/lib.rs new file mode 100644 index 00000000..155cb795 --- /dev/null +++ b/balloon/src/lib.rs @@ -0,0 +1,263 @@ +//! Pure Rust implementation of the [Balloon] password hashing function as +//! specified in [this paper](https://eprint.iacr.org/2016/027.pdf). +//! +//! # Usage (simple with default params) +//! +//! Note: this example requires the `rand_core` crate with the `std` feature +//! enabled for `rand_core::OsRng` (embedded platforms can substitute their +//! own RNG) +//! +//! Add the following to your crate's `Cargo.toml` to import it: +//! +//! ```toml +//! [dependencies] +//! balloon-hash = "0.1" +//! rand_core = { version = "0.6", features = ["std"] } +//! sha2 = "0.9" +//! ``` +//! +//! The following example demonstrates the high-level password hashing API: +//! +//! ``` +//! # fn main() -> Result<(), Box> { +//! # #[cfg(all(feature = "password-hash", feature = "std"))] +//! # { +//! use balloon_hash::{ +//! password_hash::{ +//! rand_core::OsRng, +//! PasswordHash, PasswordHasher, PasswordVerifier, SaltString +//! }, +//! Balloon +//! }; +//! use sha2::Sha256; +//! +//! let password = b"hunter42"; // Bad password; don't actually use! +//! let salt = SaltString::generate(&mut OsRng); +//! +//! // Balloon with default params +//! let balloon = Balloon::::default(); +//! +//! // Hash password to PHC string ($balloon$v=1$...) +//! let password_hash = balloon.hash_password(password, &salt)?.to_string(); +//! +//! // Verify password against PHC string +//! let parsed_hash = PasswordHash::new(&password_hash)?; +//! assert!(balloon.verify_password(password, &parsed_hash).is_ok()); +//! # } +//! # Ok(()) +//! # } +//! ``` +//! +//! [Balloon]: https://en.wikipedia.org/wiki/Balloon_hashing + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![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", + html_root_url = "https://docs.rs/balloon-hash/0.1.0" +)] +#![warn(rust_2018_idioms, missing_docs)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +mod algorithm; +mod balloon; +mod error; +mod params; + +pub use crate::{ + algorithm::Algorithm, + error::{Error, Result}, + params::Params, +}; +use core::marker::PhantomData; +use crypto_bigint::ArrayDecoding; +use digest::generic_array::GenericArray; +use digest::{Digest, FixedOutputReset}; +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +pub use password_hash::{self, PasswordHash, PasswordHasher, PasswordVerifier}; +#[cfg(all(feature = "alloc", feature = "password-hash"))] +use { + core::convert::TryFrom, + password_hash::{Decimal, Ident, ParamsString, Salt}, +}; + +/// Balloon context. +/// +/// This is the primary type of this crate's API, and contains the following: +/// +/// - Default set of [`Params`] to be used +/// - (Optional) Secret key a.k.a. "pepper" to be used +#[derive(Clone, Default)] +pub struct Balloon<'key, D: Digest + FixedOutputReset> +where + GenericArray: ArrayDecoding, +{ + /// Storing which hash function is used + pub digest: PhantomData, + /// Algorithm to use + pub algorithm: Algorithm, + /// Algorithm parameters + pub params: Params, + /// Key array + pub secret: Option<&'key [u8]>, +} + +impl<'key, D: Digest + FixedOutputReset> Balloon<'key, D> +where + GenericArray: ArrayDecoding, +{ + /// Create a new Balloon context. + pub fn new(algorithm: Algorithm, params: Params, secret: Option<&'key [u8]>) -> Self { + Self { + digest: PhantomData, + algorithm, + params, + secret, + } + } + + /// Hash a password and associated parameters. + #[cfg(feature = "alloc")] + #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] + pub fn hash(&self, pwd: &[u8], salt: &[u8]) -> Result> { + #[cfg(not(feature = "parallel"))] + let mut memory = alloc::vec![GenericArray::default(); self.params.s_cost.get() as usize]; + #[cfg(feature = "parallel")] + let mut memory = alloc::vec![GenericArray::default(); (self.params.s_cost.get() * self.params.p_cost.get()) as usize]; + self.hash_with_memory(pwd, salt, &mut memory) + } + + /// Hash a password and associated parameters. + /// + /// This method takes an explicit `memory_blocks` parameter which allows + /// the caller to provide the backing storage for the algorithm's state: + /// + /// - Users with the `alloc` feature enabled can use [`Balloon::hash`] + /// to have it allocated for them. + /// - `no_std` users on "heapless" targets can use an array of the [`GenericArray`] type + /// to stack allocate this buffer. It needs a minimum size of `s_cost` or `s_cost * p_cost` + /// with the `parallel` feature enabled. + pub fn hash_with_memory( + &self, + pwd: &[u8], + salt: &[u8], + memory_blocks: &mut [GenericArray], + ) -> Result> { + match self.algorithm { + Algorithm::Balloon => { + balloon::balloon::(pwd, salt, self.secret, self.params, memory_blocks) + } + Algorithm::BalloonM => { + balloon::balloon_m::(pwd, salt, self.secret, self.params, memory_blocks) + } + } + } +} + +#[cfg(all(feature = "alloc", feature = "password-hash"))] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl PasswordHasher for Balloon<'_, D> +where + GenericArray: ArrayDecoding, +{ + type Params = Params; + + fn hash_password<'a, S>( + &self, + password: &[u8], + salt: &'a S, + ) -> password_hash::Result> + where + S: AsRef + ?Sized, + { + let salt = Salt::try_from(salt.as_ref())?; + let mut salt_arr = [0u8; 64]; + let salt_bytes = salt.b64_decode(&mut salt_arr)?; + + let output = password_hash::Output::new(&self.hash(password, salt_bytes)?)?; + + Ok(PasswordHash { + algorithm: self.algorithm.ident(), + version: Some(1), + params: ParamsString::try_from(&self.params)?, + salt: Some(salt), + hash: Some(output), + }) + } + + fn hash_password_customized<'a>( + &self, + password: &[u8], + alg_id: Option>, + version: Option, + params: Params, + salt: impl Into>, + ) -> password_hash::Result> { + let algorithm = alg_id + .map(Algorithm::try_from) + .transpose()? + .unwrap_or_default(); + + if let Some(version) = version { + if version != 1 { + return Err(password_hash::Error::Version); + } + } + + let salt = salt.into(); + + Self::new(algorithm, params, self.secret).hash_password(password, salt.as_str()) + } +} + +impl<'key, D: Digest + FixedOutputReset> From for Balloon<'key, D> +where + GenericArray: ArrayDecoding, +{ + fn from(params: Params) -> Self { + Self::new(Algorithm::default(), params, None) + } +} + +#[cfg(feature = "password-hash")] +#[test] +fn hash_simple_retains_configured_params() { + use sha2::Sha256; + + /// Example password only: don't use this as a real password!!! + const EXAMPLE_PASSWORD: &[u8] = b"hunter42"; + + /// Example salt value. Don't use a static salt value!!! + const EXAMPLE_SALT: &str = "examplesalt"; + + // Non-default but valid parameters + let t_cost = 4; + let s_cost = 2048; + let p_cost = 2; + + let params = Params::new(s_cost, t_cost, p_cost).unwrap(); + let hasher = Balloon::::new(Algorithm::default(), params, None); + let hash = hasher + .hash_password(EXAMPLE_PASSWORD, EXAMPLE_SALT) + .unwrap(); + + assert_eq!(hash.version.unwrap(), 1); + + for &(param, value) in &[("t", t_cost), ("s", s_cost), ("p", p_cost)] { + assert_eq!( + hash.params + .get(param) + .and_then(|p| p.decimal().ok()) + .unwrap(), + value + ); + } +} diff --git a/balloon/src/params.rs b/balloon/src/params.rs new file mode 100644 index 00000000..ef471efc --- /dev/null +++ b/balloon/src/params.rs @@ -0,0 +1,110 @@ +//! Balloon password hash parameters. + +use crate::{Error, Result}; +use core::num::NonZeroU32; +#[cfg(feature = "password-hash")] +use { + core::convert::TryFrom, + password_hash::{errors::InvalidValue, ParamsString, PasswordHash}, +}; + +/// Balloon password hash parameters. +/// +/// These are parameters which can be encoded into a PHC hash string. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Params { + /// Space cost, expressed in number of blocks. + pub s_cost: NonZeroU32, + /// Time cost, expressed in number of rounds. + pub t_cost: NonZeroU32, + /// Degree of parallelism, expressed in number of threads. + /// Only allowed to be higher than 1 when used in combination + /// with [`Algorithm::BalloonM`](crate::Algorithm::BalloonM). + pub p_cost: NonZeroU32, +} + +impl Params { + /// Default memory cost. + pub const DEFAULT_S_COST: u32 = 1024; + + /// Default number of iterations (i.e. "time"). + pub const DEFAULT_T_COST: u32 = 3; + + /// Default degree of parallelism. + pub const DEFAULT_P_COST: u32 = 1; + + /// Create new parameters. + pub fn new(s_cost: u32, t_cost: u32, p_cost: u32) -> Result { + Ok(Self { + s_cost: NonZeroU32::new(s_cost).ok_or(Error::MemoryTooLittle)?, + t_cost: NonZeroU32::new(t_cost).ok_or(Error::TimeTooSmall)?, + p_cost: NonZeroU32::new(p_cost).ok_or(Error::ThreadsTooFew)?, + }) + } +} + +impl Default for Params { + fn default() -> Self { + Self::new( + Self::DEFAULT_S_COST, + Self::DEFAULT_T_COST, + Self::DEFAULT_P_COST, + ) + .unwrap() + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl<'a> TryFrom<&'a PasswordHash<'a>> for Params { + type Error = password_hash::Error; + + fn try_from(hash: &'a PasswordHash<'a>) -> password_hash::Result { + let mut params = Self::default(); + + for (ident, value) in hash.params.iter() { + match ident.as_str() { + "s" => { + params.s_cost = NonZeroU32::new(value.decimal()?) + .ok_or_else(|| InvalidValue::TooShort.param_error())?; + } + "t" => { + params.t_cost = NonZeroU32::new(value.decimal()?) + .ok_or_else(|| InvalidValue::TooShort.param_error())?; + } + "p" => { + params.p_cost = NonZeroU32::new(value.decimal()?) + .ok_or_else(|| InvalidValue::TooShort.param_error())?; + } + _ => return Err(password_hash::Error::ParamNameInvalid), + } + } + + Ok(params) + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl<'a> TryFrom for ParamsString { + type Error = password_hash::Error; + + fn try_from(params: Params) -> password_hash::Result { + ParamsString::try_from(¶ms) + } +} + +#[cfg(feature = "password-hash")] +#[cfg_attr(docsrs, doc(cfg(feature = "password-hash")))] +impl<'a> TryFrom<&Params> for ParamsString { + type Error = password_hash::Error; + + fn try_from(params: &Params) -> password_hash::Result { + let mut output = ParamsString::new(); + output.add_decimal("s", params.s_cost.get())?; + output.add_decimal("t", params.t_cost.get())?; + output.add_decimal("p", params.p_cost.get())?; + + Ok(output) + } +} diff --git a/balloon/tests/balloon.rs b/balloon/tests/balloon.rs new file mode 100644 index 00000000..e7cf98d7 --- /dev/null +++ b/balloon/tests/balloon.rs @@ -0,0 +1,73 @@ +use balloon_hash::{Algorithm, Balloon, Params}; +use digest::generic_array::GenericArray; +use hex_literal::hex; + +struct TestVector { + password: &'static [u8], + salt: &'static [u8], + s_cost: u32, + t_cost: u32, + output: [u8; 32], +} + +/// Tested with the following implementations: +/// - +/// - +const TEST_VECTORS: &[TestVector] = &[ + TestVector { + password: b"hunter42", + salt: b"examplesalt", + s_cost: 1024, + t_cost: 3, + output: hex!("716043dff777b44aa7b88dcbab12c078abecfac9d289c5b5195967aa63440dfb"), + }, + TestVector { + password: b"", + salt: b"salt", + s_cost: 3, + t_cost: 3, + output: hex!("5f02f8206f9cd212485c6bdf85527b698956701ad0852106f94b94ee94577378"), + }, + TestVector { + password: b"password", + salt: b"", + s_cost: 3, + t_cost: 3, + output: hex!("20aa99d7fe3f4df4bd98c655c5480ec98b143107a331fd491deda885c4d6a6cc"), + }, + TestVector { + password: b"\0", + salt: b"\0", + s_cost: 3, + t_cost: 3, + output: hex!("4fc7e302ffa29ae0eac31166cee7a552d1d71135f4e0da66486fb68a749b73a4"), + }, + TestVector { + password: b"password", + salt: b"salt", + s_cost: 1, + t_cost: 1, + output: hex!("eefda4a8a75b461fa389c1dcfaf3e9dfacbc26f81f22e6f280d15cc18c417545"), + }, +]; + +#[test] +fn test_vectors() { + for test_vector in TEST_VECTORS { + let balloon = Balloon::::new( + Algorithm::Balloon, + Params::new(test_vector.s_cost, test_vector.t_cost, 1).unwrap(), + None, + ); + + let mut memory = vec![GenericArray::default(); balloon.params.s_cost.get() as usize]; + + assert_eq!( + balloon + .hash_with_memory(test_vector.password, test_vector.salt, &mut memory) + .unwrap() + .as_slice(), + test_vector.output, + ); + } +} diff --git a/balloon/tests/balloon_m.rs b/balloon/tests/balloon_m.rs new file mode 100644 index 00000000..09bb81c5 --- /dev/null +++ b/balloon/tests/balloon_m.rs @@ -0,0 +1,109 @@ +use balloon_hash::{Algorithm, Balloon, Params}; +use digest::generic_array::GenericArray; +use hex_literal::hex; + +struct TestVector { + password: &'static [u8], + salt: &'static [u8], + s_cost: u32, + t_cost: u32, + p_cost: u32, + output: [u8; 32], +} + +/// Tested with the following implementations: +/// - +/// - +const TEST_VECTORS: &[TestVector] = &[ + TestVector { + password: b"hunter42", + salt: b"examplesalt", + s_cost: 1024, + t_cost: 3, + p_cost: 4, + output: hex!("1832bd8e5cbeba1cb174a13838095e7e66508e9bf04c40178990adbc8ba9eb6f"), + }, + TestVector { + password: b"", + salt: b"salt", + s_cost: 3, + t_cost: 3, + p_cost: 2, + output: hex!("f8767fe04059cef67b4427cda99bf8bcdd983959dbd399a5e63ea04523716c23"), + }, + TestVector { + password: b"password", + salt: b"", + s_cost: 3, + t_cost: 3, + p_cost: 3, + output: hex!("bcad257eff3d1090b50276514857e60db5d0ec484129013ef3c88f7d36e438d6"), + }, + TestVector { + password: b"password", + salt: b"", + s_cost: 3, + t_cost: 3, + p_cost: 1, + output: hex!("498344ee9d31baf82cc93ebb3874fe0b76e164302c1cefa1b63a90a69afb9b4d"), + }, + TestVector { + password: b"\0", + salt: b"\0", + s_cost: 3, + t_cost: 3, + p_cost: 4, + output: hex!("8a665611e40710ba1fd78c181549c750f17c12e423c11930ce997f04c7153e0c"), + }, + TestVector { + password: b"\0", + salt: b"\0", + s_cost: 3, + t_cost: 3, + p_cost: 1, + output: hex!("d9e33c683451b21fb3720afbd78bf12518c1d4401fa39f054b052a145c968bb1"), + }, + TestVector { + password: b"password", + salt: b"salt", + s_cost: 1, + t_cost: 1, + p_cost: 16, + output: hex!("a67b383bb88a282aef595d98697f90820adf64582a4b3627c76b7da3d8bae915"), + }, + TestVector { + password: b"password", + salt: b"salt", + s_cost: 1, + t_cost: 1, + p_cost: 1, + output: hex!("97a11df9382a788c781929831d409d3599e0b67ab452ef834718114efdcd1c6d"), + }, +]; + +#[test] +fn test_vectors() { + for test_vector in TEST_VECTORS { + let balloon = Balloon::::new( + Algorithm::BalloonM, + Params::new(test_vector.s_cost, test_vector.t_cost, test_vector.p_cost).unwrap(), + None, + ); + + #[cfg(not(feature = "parallel"))] + let mut memory = vec![GenericArray::default(); balloon.params.s_cost.get() as usize]; + #[cfg(feature = "parallel")] + let mut memory = vec![ + GenericArray::default(); + (balloon.params.s_cost.get() * balloon.params.p_cost.get()) as usize + ]; + + assert_eq!( + balloon + .hash_with_memory(test_vector.password, test_vector.salt, &mut memory) + .unwrap() + .as_slice(), + test_vector.output, + ); + } +}