Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ argon2 = "0.5.3"
clap = { version = "4.6.1", features = ["derive"] }
rand_core = { version = "0.6", features = ["getrandom"] }
hex = "0.4.3"
zeroize = "1"

[dev-dependencies]
rand = { version = "0.9.4", features = ["std", "std_rng"] }
Expand Down
36 changes: 21 additions & 15 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use argon2::password_hash::SaltString;
use clap::{ArgGroup, Parser};
use std::io::{self, BufRead, IsTerminal, Write};
use std::io::{self, IsTerminal, Read, Write};
use zeroize::Zeroizing;

// 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)]
Expand Down Expand Up @@ -61,19 +62,25 @@ struct Args {
v: u32,
}

fn get_input() -> io::Result<String> {
fn get_input() -> io::Result<Zeroizing<Vec<u8>>> {
let stdin = io::stdin();

if stdin.is_terminal() {
print!("Enter password: ");
io::stdout().flush()?;

let mut input = String::new();
let mut input = Zeroizing::new(String::new());
stdin.read_line(&mut input)?;
Ok(input.trim().to_string())
// Strip only the newline from pressing Enter, keep other whitespace
while input.ends_with('\n') || input.ends_with('\r') {
input.pop();
}
Ok(Zeroizing::new(input.as_bytes().to_vec()))
} else {
let lines: Vec<String> = stdin.lock().lines().collect::<Result<_, _>>()?;
Ok(lines.join("\n"))
// Read stdin verbatim (including any trailing newline) to match the reference implementation
let mut input = Zeroizing::new(Vec::new());
stdin.lock().read_to_end(&mut input)?;
Ok(input)
}
}

Expand Down Expand Up @@ -108,10 +115,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
None => SaltString::generate(&mut rand_core::OsRng),
};

let password = get_input().unwrap_or_else(|e| {
eprintln!("Error reading input: {}", e);
std::process::exit(1);
});
let password = get_input().map_err(|e| format!("Error reading input: {}", e))?;

// Select algorithm variant
let algorithm = if args.d {
Expand All @@ -138,11 +142,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

let start = std::time::Instant::now();

let password_bytes = password.as_bytes();

use argon2::PasswordHasher;
use argon2::{PasswordHasher, PasswordVerifier};
let password_hash = argon2
.hash_password(password_bytes, salt_string.as_salt())
.hash_password(&password, salt_string.as_salt())
.map_err(|e| format!("Hashing failed: {}", e))?;

let duration = start.elapsed();
Expand All @@ -152,7 +154,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", password_hash);
} else if args.r {
if let Some(hash) = password_hash.hash {
io::stdout().write_all(hash.as_bytes())?;
println!("{}", hex::encode(hash.as_bytes()));
}
} else {
println!("Type: {:?}", algorithm);
Expand All @@ -166,6 +168,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Encoded: {}", password_hash);

println!("{:.3} seconds", duration.as_secs_f64());

argon2
.verify_password(&password, &password_hash)
.map_err(|e| format!("Verification failed: {}", e))?;
println!("Verification ok");
}

Expand Down
49 changes: 47 additions & 2 deletions tests/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fn verify(i: u32, memory_exp: u32, parallelism: u32, variant: &str) -> bool {
fn test_argon2_compatibility() {
// Build release binary first
let status = Command::new("cargo")
.args(&["build", "--release", "--bin", "argon2-cli"])
.args(["build", "--release", "--bin", "argon2-cli"])
.status()
.expect("Failed to build binary");
assert!(status.success(), "Construction of binary failed");
Expand Down Expand Up @@ -160,9 +160,54 @@ fn test_argon2_compatibility() {
}
}

// Debug binary is always built by `cargo test` for integration tests
const RUST_DEBUG_BINARY: &str = "./target/debug/argon2-cli";

#[test]
fn test_encoded_output_matches_reference() {
let salt = generate_random_string(8);
let password = generate_random_string(12);
let args = vec!["-e".to_string()];

let ref_out = run_argon2(REF_BINARY, &salt, &password, &args).expect("reference failed");
let rust_out = run_argon2(RUST_DEBUG_BINARY, &salt, &password, &args).expect("rust failed");

assert_eq!(ref_out, rust_out, "-e output differs from reference");
}

#[test]
fn test_raw_output_matches_reference() {
let salt = generate_random_string(8);
let password = generate_random_string(12);
let args = vec!["-r".to_string()];

let ref_out = run_argon2(REF_BINARY, &salt, &password, &args).expect("reference failed");
let rust_out = run_argon2(RUST_DEBUG_BINARY, &salt, &password, &args).expect("rust failed");

assert_eq!(ref_out, rust_out, "-r output differs from reference");
}

#[test]
fn test_stdin_read_verbatim() {
let salt = generate_random_string(8);
let args = vec!["-e".to_string()];

// Trailing newlines and inner newlines are part of the password, as in the reference
for password in ["password\n", "pass\nword", " password "] {
let ref_out = run_argon2(REF_BINARY, &salt, password, &args).expect("reference failed");
let rust_out = run_argon2(RUST_DEBUG_BINARY, &salt, password, &args).expect("rust failed");

assert_eq!(
ref_out, rust_out,
"stdin handling differs from reference for {:?}",
password
);
}
}

#[test]
fn test_short_salt_fails_before_hashing() {
let output = Command::new("./target/debug/argon2-cli")
let output = Command::new(RUST_DEBUG_BINARY)
.arg("1234567")
.output()
.expect("Failed to run debug binary");
Expand Down
Loading