From b5c8e1d34d7a9dda19a105f2d5408d5260c69147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 10:29:26 +0200 Subject: [PATCH 1/8] fix: validate salt before prompting for password --- src/main.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index f03f9b5..5293da3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,6 +86,9 @@ fn main() -> Result<(), Box> { let args = Args::parse_from(new_args); + let salt_string = SaltString::encode_b64(args.salt.as_bytes()) + .map_err(|e| format!("Invalid salt: {}", e))?; + let password = get_input().unwrap_or_else(|e| { eprintln!("Error reading input: {}", e); std::process::exit(1); @@ -114,10 +117,6 @@ fn main() -> Result<(), Box> { Some(args.l as usize), ).map_err(|e| format!("Invalid parameters: {}", e))?; - // Encode salt to PHC string format - let salt_string = SaltString::encode_b64(args.salt.as_bytes()) - .map_err(|e| format!("Invalid salt: {}", e))?; - let argon2 = argon2::Argon2::new( algorithm, argon2::Version::V0x13, From 5f405bcb18d04410d211405ea1014d4662f42a5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 10:30:40 +0200 Subject: [PATCH 2/8] feat: make salt optional, generate random salt by default --- Cargo.lock | 23 ++++++++++++++++++++++- Cargo.toml | 1 + src/main.rs | 11 +++++++---- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da2d39e..55fa10e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,7 @@ dependencies = [ "clap", "hex", "rand", + "rand_core 0.6.4", ] [[package]] @@ -190,6 +191,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -301,6 +313,9 @@ name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] [[package]] name = "rand_core" @@ -308,7 +323,7 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa" dependencies = [ - "getrandom", + "getrandom 0.3.4", ] [[package]] @@ -358,6 +373,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" diff --git a/Cargo.toml b/Cargo.toml index cf435b0..f0c52de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ eula = false [dependencies] argon2 = "0.5.3" clap = { version = "4.5.54", features = ["derive"] } +rand_core = { version = "0.6", features = ["getrandom"] } hex = "0.4.3" [dev-dependencies] diff --git a/src/main.rs b/src/main.rs index 5293da3..0d1bb49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,8 +9,8 @@ use argon2::password_hash::SaltString; #[command(group(ArgGroup::new("memory").args(&["m", "k"])))] #[command(group(ArgGroup::new("output_format").args(&["e", "r"])))] struct Args { - /// The salt to use, at least 8 characters - salt: String, + /// The salt to use (optional, a random salt is generated if omitted) + salt: Option, /// Use Argon2i (this is the default) #[arg(short = 'i', long, default_value_t = false)] @@ -86,8 +86,11 @@ fn main() -> Result<(), Box> { let args = Args::parse_from(new_args); - let salt_string = SaltString::encode_b64(args.salt.as_bytes()) - .map_err(|e| format!("Invalid salt: {}", e))?; + let salt_string = match &args.salt { + Some(salt) => SaltString::encode_b64(salt.as_bytes()) + .map_err(|e| format!("Invalid salt: {}", e))?, + None => SaltString::generate(&mut rand_core::OsRng), + }; let password = get_input().unwrap_or_else(|e| { eprintln!("Error reading input: {}", e); From e6785a37ee7bb696174c883ad3ac437bf82a1743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 10:31:08 +0200 Subject: [PATCH 3/8] chore!: bump version in Cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f0c52de..cb2b6b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "argon2-cli" -version = "0.1.1" +version = "0.2.0" edition = "2024" authors = [ "Joel Schulz-Andres ", From c823342e90b7ae8f1e4b433d93ba73de03e5bf6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 10:34:16 +0200 Subject: [PATCH 4/8] chore: bump dependencies --- Cargo.lock | 86 +++++++++++++++++++++++++++--------------------------- Cargo.toml | 4 +-- 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55fa10e..4cacece 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -19,15 +19,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -66,7 +66,7 @@ dependencies = [ [[package]] name = "argon2-cli" -version = "0.1.1" +version = "0.2.0" dependencies = [ "argon2", "clap", @@ -107,9 +107,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.5.54" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -129,9 +129,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -141,15 +141,15 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.7" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "cpufeatures" @@ -234,9 +234,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "once_cell_polyfill" @@ -266,18 +266,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.105" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.43" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -290,12 +290,12 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -305,7 +305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.4", + "rand_core 0.9.5", ] [[package]] @@ -319,9 +319,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1b3bc831f92381018fd9c6350b917c7b21f1eed35a65a51900e0e55a3d7afa" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -340,9 +340,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -351,15 +351,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "utf8parse" @@ -381,9 +381,9 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen", ] @@ -405,24 +405,24 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "zerocopy" -version = "0.8.33" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.33" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index cb2b6b6..c7ed3f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,12 +21,12 @@ eula = false [dependencies] argon2 = "0.5.3" -clap = { version = "4.5.54", features = ["derive"] } +clap = { version = "4.6.1", features = ["derive"] } rand_core = { version = "0.6", features = ["getrandom"] } hex = "0.4.3" [dev-dependencies] -rand = { version = "0.9.2", features = ["std", "std_rng"] } +rand = { version = "0.9.4", features = ["std", "std_rng"] } # The profile that 'dist' will build with [profile.dist] From 0189540d6d03863cba09af137af5cc841a8e888d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 11:14:18 +0200 Subject: [PATCH 5/8] feat: implement Argon2 version selection via -v flag --- src/main.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0d1bb49..ca5d4c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,9 +52,9 @@ struct Args { #[arg(short = 'r', default_value_t = false)] r: bool, - /// Argon2 version (defaults to the most recent version, currently 13) + /// Argon2 version (10 or 13, default: 13) #[arg(short = 'v', default_value_t = 13)] - v: u32, // Unimplemented: version selection not supported, always uses v13 + v: u32, } fn get_input() -> io::Result { @@ -120,9 +120,15 @@ fn main() -> Result<(), Box> { Some(args.l as usize), ).map_err(|e| format!("Invalid parameters: {}", e))?; + let version = match args.v { + 10 => argon2::Version::V0x10, + 13 => argon2::Version::V0x13, + _ => return Err(format!("Invalid version: {} (expected 10 or 13)", args.v).into()), + }; + let argon2 = argon2::Argon2::new( algorithm, - argon2::Version::V0x13, + version, params, ); From 93dea4276dcd6e7e81e4fef3b6ad2dbf15574c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 11:40:24 +0200 Subject: [PATCH 6/8] docs: add installation methods and compatibility note to README --- README.md | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 595305b..e037083 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,25 @@ Generate Argon2 hashes from command line ## Installation ```bash -cargo install --path . +cargo install argon2-cli +``` + +Or via [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall): + +```bash +cargo binstall argon2-cli ``` ## Usage ```bash -echo -n "password" | argon2 somesalt +echo -n "password" | argon2 ``` ### Options ``` -argon2 [-h] salt [-i|-d|-id] [-t iterations] [-m log2(memory in KiB) | -k memory in KiB] [-p parallelism] [-l hash length] [-e|-r] [-v (10|13)] +argon2 [-h] [salt] [-i|-d|-id] [-t iterations] [-m log2(memory in KiB) | -k memory in KiB] [-p parallelism] [-l hash length] [-e|-r] [-v (10|13)] ``` - `-i` Use Argon2i (default) @@ -35,7 +41,10 @@ argon2 [-h] salt [-i|-d|-id] [-t iterations] [-m log2(memory in KiB) | -k memory ## Examples ```bash -# Basic usage +# Basic usage (random salt generated automatically) +echo -n "password" | argon2 + +# With explicit salt echo -n "password" | argon2 somesalt # Use Argon2id with custom parameters @@ -45,6 +54,10 @@ echo -n "password" | argon2 somesalt -id -t 4 -m 16 -p 4 echo -n "password" | argon2 somesalt -e ``` +## Compatibility + +This CLI strives for interface compatibility with the [C reference implementation](https://github.com/P-H-C/phc-winner-argon2), but does not guarantee it. Deviations are made where the original interface significantly hinders usability. For example, the salt is optional in our implementation and a cryptographically secure random salt is generated by default. + ## License MIT From c29e5095a158dd00ddb7eaba1121f1c0e4b234d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 16:50:25 +0200 Subject: [PATCH 7/8] fix: validate salt length before reading password --- README.md | 1 + src/main.rs | 77 ++++++++++++++++++++++++++----------------------- tests/verify.rs | 64 ++++++++++++++++++++++++++++++---------- 3 files changed, 91 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index e037083..fdda85e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ echo -n "password" | argon2 argon2 [-h] [salt] [-i|-d|-id] [-t iterations] [-m log2(memory in KiB) | -k memory in KiB] [-p parallelism] [-l hash length] [-e|-r] [-v (10|13)] ``` +- `[salt]` Optional explicit salt; must be at least 8 characters long - `-i` Use Argon2i (default) - `-d` Use Argon2d - `-id` Use Argon2id diff --git a/src/main.rs b/src/main.rs index ca5d4c8..48f860e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,19 @@ +use argon2::password_hash::SaltString; use clap::{ArgGroup, Parser}; use std::io::{self, BufRead, IsTerminal, Write}; -use argon2::password_hash::SaltString; // Usage: argon2 [-h] salt [-i|-d|-id] [-t iterations] [-m log2(memory in KiB) | -k memory in KiB] [-p parallelism] [-l hash length] [-e|-r] [-v (10|13)] #[derive(Parser, Debug)] -#[command(name = "argon2", about = "(Rust implementation)", disable_help_flag = false)] +#[command( + name = "argon2", + about = "(Rust implementation)", + disable_help_flag = false +)] #[command(group(ArgGroup::new("variant").args(&["i", "d", "id"])))] #[command(group(ArgGroup::new("memory").args(&["m", "k"])))] #[command(group(ArgGroup::new("output_format").args(&["e", "r"])))] struct Args { - /// The salt to use (optional, a random salt is generated if omitted) + /// The salt to use (optional, a random salt is generated if omitted; minimum 8 characters) salt: Option, /// Use Argon2i (this is the default) @@ -73,22 +77,34 @@ fn get_input() -> io::Result { } } +fn validate_salt(salt: &str) -> Result<(), String> { + if salt.len() < 8 { + return Err("Invalid salt: minimum length is 8 characters".to_string()); + } + + Ok(()) +} + fn main() -> Result<(), Box> { // Handle the non-standard `-id` flag which conflicts with clap's short flag clustering let args_env = std::env::args(); - let new_args: Vec = args_env.map(|arg| { - if arg == "-id" { - "--id".to_string() - } else { - arg - } - }).collect(); + let new_args: Vec = args_env + .map(|arg| { + if arg == "-id" { + "--id".to_string() + } else { + arg + } + }) + .collect(); let args = Args::parse_from(new_args); let salt_string = match &args.salt { - Some(salt) => SaltString::encode_b64(salt.as_bytes()) - .map_err(|e| format!("Invalid salt: {}", e))?, + Some(salt) => { + validate_salt(salt)?; + SaltString::encode_b64(salt.as_bytes()).map_err(|e| format!("Invalid salt: {}", e))? + } None => SaltString::generate(&mut rand_core::OsRng), }; @@ -96,7 +112,7 @@ fn main() -> Result<(), Box> { eprintln!("Error reading input: {}", e); std::process::exit(1); }); - + // Select algorithm variant let algorithm = if args.d { argon2::Algorithm::Argon2d @@ -107,18 +123,10 @@ fn main() -> Result<(), Box> { }; // Calculate memory cost - let memory_kib = if let Some(k) = args.k { - k - } else { - 1 << args.m - }; + let memory_kib = if let Some(k) = args.k { k } else { 1 << args.m }; - let params = argon2::Params::new( - memory_kib, - args.t, - args.p, - Some(args.l as usize), - ).map_err(|e| format!("Invalid parameters: {}", e))?; + let params = argon2::Params::new(memory_kib, args.t, args.p, Some(args.l as usize)) + .map_err(|e| format!("Invalid parameters: {}", e))?; let version = match args.v { 10 => argon2::Version::V0x10, @@ -126,20 +134,17 @@ fn main() -> Result<(), Box> { _ => return Err(format!("Invalid version: {} (expected 10 or 13)", args.v).into()), }; - let argon2 = argon2::Argon2::new( - algorithm, - version, - params, - ); + let argon2 = argon2::Argon2::new(algorithm, version, params); let start = std::time::Instant::now(); - + let password_bytes = password.as_bytes(); - + use argon2::PasswordHasher; - let password_hash = argon2.hash_password(password_bytes, salt_string.as_salt()) + let password_hash = argon2 + .hash_password(password_bytes, salt_string.as_salt()) .map_err(|e| format!("Hashing failed: {}", e))?; - + let duration = start.elapsed(); // Generate output based on flags @@ -147,19 +152,19 @@ fn main() -> Result<(), Box> { println!("{}", password_hash); } else if args.r { if let Some(hash) = password_hash.hash { - io::stdout().write_all(hash.as_bytes())?; + io::stdout().write_all(hash.as_bytes())?; } } else { println!("Type: {:?}", algorithm); println!("Iterations: {}", args.t); println!("Memory: {} KiB", memory_kib); println!("Parallelism: {}", args.p); - + if let Some(hash) = password_hash.hash { println!("Hash: {}", hex::encode(hash.as_bytes())); } println!("Encoded: {}", password_hash); - + println!("{:.3} seconds", duration.as_secs_f64()); println!("Verification ok"); } diff --git a/tests/verify.rs b/tests/verify.rs index 7df2ee3..6a3acea 100644 --- a/tests/verify.rs +++ b/tests/verify.rs @@ -1,7 +1,7 @@ -use std::process::Command; -use std::io::Write; use rand::Rng; use std::collections::HashMap; +use std::io::Write; +use std::process::Command; const REF_BINARY: &str = "argon2"; // We assume the binary is at the standard release location relative to the test runner. @@ -24,19 +24,25 @@ fn run_argon2(binary: &str, salt: &str, password: &str, args: &[String]) -> Resu for arg in args { cmd.arg(arg); } - + // Setup stdin cmd.stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); - let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn {}: {}", binary, e))?; + let mut child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn {}: {}", binary, e))?; if let Some(mut stdin) = child.stdin.take() { - stdin.write_all(password.as_bytes()).map_err(|e| format!("Failed to write to stdin: {}", e))?; + stdin + .write_all(password.as_bytes()) + .map_err(|e| format!("Failed to write to stdin: {}", e))?; } - let output = child.wait_with_output().map_err(|e| format!("Failed to wait: {}", e))?; + let output = child + .wait_with_output() + .map_err(|e| format!("Failed to wait: {}", e))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); @@ -91,14 +97,16 @@ fn verify(i: u32, memory_exp: u32, parallelism: u32, variant: &str) -> bool { let rust_data = parse_output(&rust_out); let keys_to_check = ["Iterations", "Memory", "Parallelism", "Hash", "Encoded"]; - + let mut success = true; for key in keys_to_check { let ref_val = ref_data.get(key); let rust_val = rust_data.get(key); - - if ref_val.is_none() { continue; } // e/-r flags not processed here yet - + + if ref_val.is_none() { + continue; + } // e/-r flags not processed here yet + if rust_val.is_none() { eprintln!("Mismatch: key '{}' missing in Rust output", key); success = false; @@ -106,11 +114,14 @@ fn verify(i: u32, memory_exp: u32, parallelism: u32, variant: &str) -> bool { } if ref_val != rust_val { - eprintln!("Mismatch for '{}':\n Ref: {:?}\n Rust: {:?}", key, ref_val, rust_val); + eprintln!( + "Mismatch for '{}':\n Ref: {:?}\n Rust: {:?}", + key, ref_val, rust_val + ); success = false; } } - + if success { println!("OK"); } @@ -141,7 +152,30 @@ fn test_argon2_compatibility() { let variant = variants[var_idx]; if !verify(iterations, memory_exp, parallelism, variant) { - panic!("Random test failed with: t={}, m={}, p={}, var={}", iterations, memory_exp, parallelism, variant); + panic!( + "Random test failed with: t={}, m={}, p={}, var={}", + iterations, memory_exp, parallelism, variant + ); } } } + +#[test] +fn test_short_salt_fails_before_hashing() { + let output = Command::new("./target/debug/argon2-cli") + .arg("1234567") + .output() + .expect("Failed to run debug binary"); + + assert!(!output.status.success(), "Short salt should fail"); + + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Invalid salt: minimum length is 8 characters"), + "unexpected stderr: {stderr}" + ); + assert!( + !stderr.contains("Hashing failed"), + "salt validation should happen before hashing: {stderr}" + ); +} From 73a2588895fc49135152b449e6ae364fdc3098b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=ABl=20Schulz-Andres?= Date: Wed, 6 May 2026 16:52:27 +0200 Subject: [PATCH 8/8] docs: state that password is taken in from stdin --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fdda85e..7e8b089 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ cargo binstall argon2-cli echo -n "password" | argon2 ``` +This CLI accepts the password via `stdin`, which avoids putting the password in shell history. + ### Options ``` @@ -57,7 +59,7 @@ echo -n "password" | argon2 somesalt -e ## Compatibility -This CLI strives for interface compatibility with the [C reference implementation](https://github.com/P-H-C/phc-winner-argon2), but does not guarantee it. Deviations are made where the original interface significantly hinders usability. For example, the salt is optional in our implementation and a cryptographically secure random salt is generated by default. +This CLI strives for interface compatibility with the [C reference implementation](https://github.com/P-H-C/phc-winner-argon2), but does not guarantee it. Deviations are made where the original interface significantly hinders usability. For example, the salt is optional in our implementation and a cryptographically secure random salt is generated by default. This CLI also accepts the password via `stdin`, unlike the standard Linux `argon2` CLI, which helps avoid leaking passwords into shell history. ## License