Skip to content

Commit c29e509

Browse files
committed
fix: validate salt length before reading password
1 parent 93dea42 commit c29e509

3 files changed

Lines changed: 91 additions & 51 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ echo -n "password" | argon2
2626
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)]
2727
```
2828

29+
- `[salt]` Optional explicit salt; must be at least 8 characters long
2930
- `-i` Use Argon2i (default)
3031
- `-d` Use Argon2d
3132
- `-id` Use Argon2id

src/main.rs

Lines changed: 41 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1+
use argon2::password_hash::SaltString;
12
use clap::{ArgGroup, Parser};
23
use std::io::{self, BufRead, IsTerminal, Write};
3-
use argon2::password_hash::SaltString;
44

55
// 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)]
66
#[derive(Parser, Debug)]
7-
#[command(name = "argon2", about = "(Rust implementation)", disable_help_flag = false)]
7+
#[command(
8+
name = "argon2",
9+
about = "(Rust implementation)",
10+
disable_help_flag = false
11+
)]
812
#[command(group(ArgGroup::new("variant").args(&["i", "d", "id"])))]
913
#[command(group(ArgGroup::new("memory").args(&["m", "k"])))]
1014
#[command(group(ArgGroup::new("output_format").args(&["e", "r"])))]
1115
struct Args {
12-
/// The salt to use (optional, a random salt is generated if omitted)
16+
/// The salt to use (optional, a random salt is generated if omitted; minimum 8 characters)
1317
salt: Option<String>,
1418

1519
/// Use Argon2i (this is the default)
@@ -73,30 +77,42 @@ fn get_input() -> io::Result<String> {
7377
}
7478
}
7579

80+
fn validate_salt(salt: &str) -> Result<(), String> {
81+
if salt.len() < 8 {
82+
return Err("Invalid salt: minimum length is 8 characters".to_string());
83+
}
84+
85+
Ok(())
86+
}
87+
7688
fn main() -> Result<(), Box<dyn std::error::Error>> {
7789
// Handle the non-standard `-id` flag which conflicts with clap's short flag clustering
7890
let args_env = std::env::args();
79-
let new_args: Vec<String> = args_env.map(|arg| {
80-
if arg == "-id" {
81-
"--id".to_string()
82-
} else {
83-
arg
84-
}
85-
}).collect();
91+
let new_args: Vec<String> = args_env
92+
.map(|arg| {
93+
if arg == "-id" {
94+
"--id".to_string()
95+
} else {
96+
arg
97+
}
98+
})
99+
.collect();
86100

87101
let args = Args::parse_from(new_args);
88102

89103
let salt_string = match &args.salt {
90-
Some(salt) => SaltString::encode_b64(salt.as_bytes())
91-
.map_err(|e| format!("Invalid salt: {}", e))?,
104+
Some(salt) => {
105+
validate_salt(salt)?;
106+
SaltString::encode_b64(salt.as_bytes()).map_err(|e| format!("Invalid salt: {}", e))?
107+
}
92108
None => SaltString::generate(&mut rand_core::OsRng),
93109
};
94110

95111
let password = get_input().unwrap_or_else(|e| {
96112
eprintln!("Error reading input: {}", e);
97113
std::process::exit(1);
98114
});
99-
115+
100116
// Select algorithm variant
101117
let algorithm = if args.d {
102118
argon2::Algorithm::Argon2d
@@ -107,59 +123,48 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
107123
};
108124

109125
// Calculate memory cost
110-
let memory_kib = if let Some(k) = args.k {
111-
k
112-
} else {
113-
1 << args.m
114-
};
126+
let memory_kib = if let Some(k) = args.k { k } else { 1 << args.m };
115127

116-
let params = argon2::Params::new(
117-
memory_kib,
118-
args.t,
119-
args.p,
120-
Some(args.l as usize),
121-
).map_err(|e| format!("Invalid parameters: {}", e))?;
128+
let params = argon2::Params::new(memory_kib, args.t, args.p, Some(args.l as usize))
129+
.map_err(|e| format!("Invalid parameters: {}", e))?;
122130

123131
let version = match args.v {
124132
10 => argon2::Version::V0x10,
125133
13 => argon2::Version::V0x13,
126134
_ => return Err(format!("Invalid version: {} (expected 10 or 13)", args.v).into()),
127135
};
128136

129-
let argon2 = argon2::Argon2::new(
130-
algorithm,
131-
version,
132-
params,
133-
);
137+
let argon2 = argon2::Argon2::new(algorithm, version, params);
134138

135139
let start = std::time::Instant::now();
136-
140+
137141
let password_bytes = password.as_bytes();
138-
142+
139143
use argon2::PasswordHasher;
140-
let password_hash = argon2.hash_password(password_bytes, salt_string.as_salt())
144+
let password_hash = argon2
145+
.hash_password(password_bytes, salt_string.as_salt())
141146
.map_err(|e| format!("Hashing failed: {}", e))?;
142-
147+
143148
let duration = start.elapsed();
144149

145150
// Generate output based on flags
146151
if args.e {
147152
println!("{}", password_hash);
148153
} else if args.r {
149154
if let Some(hash) = password_hash.hash {
150-
io::stdout().write_all(hash.as_bytes())?;
155+
io::stdout().write_all(hash.as_bytes())?;
151156
}
152157
} else {
153158
println!("Type: {:?}", algorithm);
154159
println!("Iterations: {}", args.t);
155160
println!("Memory: {} KiB", memory_kib);
156161
println!("Parallelism: {}", args.p);
157-
162+
158163
if let Some(hash) = password_hash.hash {
159164
println!("Hash: {}", hex::encode(hash.as_bytes()));
160165
}
161166
println!("Encoded: {}", password_hash);
162-
167+
163168
println!("{:.3} seconds", duration.as_secs_f64());
164169
println!("Verification ok");
165170
}

tests/verify.rs

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
use std::process::Command;
2-
use std::io::Write;
31
use rand::Rng;
42
use std::collections::HashMap;
3+
use std::io::Write;
4+
use std::process::Command;
55

66
const REF_BINARY: &str = "argon2";
77
// 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
2424
for arg in args {
2525
cmd.arg(arg);
2626
}
27-
27+
2828
// Setup stdin
2929
cmd.stdin(std::process::Stdio::piped())
30-
.stdout(std::process::Stdio::piped())
31-
.stderr(std::process::Stdio::piped());
30+
.stdout(std::process::Stdio::piped())
31+
.stderr(std::process::Stdio::piped());
3232

33-
let mut child = cmd.spawn().map_err(|e| format!("Failed to spawn {}: {}", binary, e))?;
33+
let mut child = cmd
34+
.spawn()
35+
.map_err(|e| format!("Failed to spawn {}: {}", binary, e))?;
3436

3537
if let Some(mut stdin) = child.stdin.take() {
36-
stdin.write_all(password.as_bytes()).map_err(|e| format!("Failed to write to stdin: {}", e))?;
38+
stdin
39+
.write_all(password.as_bytes())
40+
.map_err(|e| format!("Failed to write to stdin: {}", e))?;
3741
}
3842

39-
let output = child.wait_with_output().map_err(|e| format!("Failed to wait: {}", e))?;
43+
let output = child
44+
.wait_with_output()
45+
.map_err(|e| format!("Failed to wait: {}", e))?;
4046

4147
if !output.status.success() {
4248
let stderr = String::from_utf8_lossy(&output.stderr);
@@ -91,26 +97,31 @@ fn verify(i: u32, memory_exp: u32, parallelism: u32, variant: &str) -> bool {
9197
let rust_data = parse_output(&rust_out);
9298

9399
let keys_to_check = ["Iterations", "Memory", "Parallelism", "Hash", "Encoded"];
94-
100+
95101
let mut success = true;
96102
for key in keys_to_check {
97103
let ref_val = ref_data.get(key);
98104
let rust_val = rust_data.get(key);
99-
100-
if ref_val.is_none() { continue; } // e/-r flags not processed here yet
101-
105+
106+
if ref_val.is_none() {
107+
continue;
108+
} // e/-r flags not processed here yet
109+
102110
if rust_val.is_none() {
103111
eprintln!("Mismatch: key '{}' missing in Rust output", key);
104112
success = false;
105113
continue;
106114
}
107115

108116
if ref_val != rust_val {
109-
eprintln!("Mismatch for '{}':\n Ref: {:?}\n Rust: {:?}", key, ref_val, rust_val);
117+
eprintln!(
118+
"Mismatch for '{}':\n Ref: {:?}\n Rust: {:?}",
119+
key, ref_val, rust_val
120+
);
110121
success = false;
111122
}
112123
}
113-
124+
114125
if success {
115126
println!("OK");
116127
}
@@ -141,7 +152,30 @@ fn test_argon2_compatibility() {
141152
let variant = variants[var_idx];
142153

143154
if !verify(iterations, memory_exp, parallelism, variant) {
144-
panic!("Random test failed with: t={}, m={}, p={}, var={}", iterations, memory_exp, parallelism, variant);
155+
panic!(
156+
"Random test failed with: t={}, m={}, p={}, var={}",
157+
iterations, memory_exp, parallelism, variant
158+
);
145159
}
146160
}
147161
}
162+
163+
#[test]
164+
fn test_short_salt_fails_before_hashing() {
165+
let output = Command::new("./target/debug/argon2-cli")
166+
.arg("1234567")
167+
.output()
168+
.expect("Failed to run debug binary");
169+
170+
assert!(!output.status.success(), "Short salt should fail");
171+
172+
let stderr = String::from_utf8_lossy(&output.stderr);
173+
assert!(
174+
stderr.contains("Invalid salt: minimum length is 8 characters"),
175+
"unexpected stderr: {stderr}"
176+
);
177+
assert!(
178+
!stderr.contains("Hashing failed"),
179+
"salt validation should happen before hashing: {stderr}"
180+
);
181+
}

0 commit comments

Comments
 (0)