diff --git a/Cargo.lock b/Cargo.lock index 3395bf6..e327ee9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ [[package]] name = "rs-random" -version = "0.1.1" +version = "0.2.1" dependencies = [ "rand", ] diff --git a/Cargo.toml b/Cargo.toml index e4d11da..c19fe84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rs-random" -version = "0.1.1" +version = "0.2.1" edition = "2024" [dependencies] diff --git a/README.md b/README.md index 3045164..739587a 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,23 @@ **Secure String Generator** -A minimal, blazing-fast Rust command-line tool for generating secure random strings with the tiniest possible footprint. Perfect for standalone use or integration into other applications. +A blazing-fast Rust command-line tool for generating secure random strings with the tiniest possible footprint. ## Features -- Uses operating system entropy (`OsRng`) for cryptographically secure randomness -- Flexible selection of character sets, including alphabets, numbers, symbols, and more -- Very small binary size -- Minimal dependencies +- **True cryptographic security**: Uses operating system entropy (`OsRng`) for cryptographically secure randomness +- **Flexible character sets**: Includes alphabets, numbers, symbols, and more +- **Tiny binary size**: Aggressively optimized for size +- **Zero dependencies**: Only uses Rust's `rand` crate with minimal features -## About +## Security -I created `rs-random` to have a super simple, minimal way to generate secure strings that I could build into other applications, ensuring security, speed, and a tiny footprint. +`rs-random` uses `OsRng` from Rust's `rand` crate, which is cryptographically secure and implements the `CryptoRng` trait. It pulls entropy directly from: +- Linux/Unix: `/dev/urandom` (via `getrandom` syscall when available) +- Windows: `CryptGenRandom` +- macOS: `SecRandomCopyBytes` + +This is more secure than OpenSSL's `RAND_bytes()` which uses a userland PRNG. ## Installation @@ -43,6 +48,17 @@ rs-random -h - `-c `: Number of strings to generate (default: `1`) - `-h`: Show help information +### Default Character Sets + +When you run `rs-random` without specifying sets, it uses: **lowercase + uppercase + numbers + special-safe** + +This gives you a secure password with: +- 26 lowercase letters (a-z) +- 26 uppercase letters (A-Z) +- 10 numbers (0-9) +- 15 safe special characters: `!@#$%^&*_+-=<>?` +- **Total: 77 possible characters per position** + ### Available Character Sets | Name | Description | @@ -50,6 +66,7 @@ rs-random -h | lowercase | English lowercase letters (a-z) | | uppercase | English uppercase letters (A-Z) | | numbers | Numbers (0-9) | +| hex | Hexadecimal (0-9, a-f) | | special | Special characters | | special-safe | Safe special chars (no pipes/brackets) | | cyrillic-lower | Cyrillic lowercase letters | @@ -66,8 +83,14 @@ Generate a single 32-character string (default sets): rs-random 32 ``` +Generate a 32-character hex string (like `openssl rand -hex 16`): + +```bash +rs-random -l 32 -s hex +``` + Generate five 12-character strings using only lowercase and numbers: ```bash rs-random -l 12 -s lowercase,numbers -c 5 -``` +``` \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 50a2bbd..fa8f63e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,11 @@ use std::collections::HashSet; use std::env; use std::process; -static CHARACTER_SETS: [(&str, &str, &str); 10] = [ +static CHARACTER_SETS: [(&str, &str, &str); 11] = [ ("lowercase", "abcdefghijklmnopqrstuvwxyz", "English lowercase letters (a-z)"), ("uppercase", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "English uppercase letters (A-Z)"), ("numbers", "0123456789", "Numbers (0-9)"), + ("hex", "0123456789abcdef", "Hexadecimal (0-9, a-f)"), ("special", "!@#$%^&*()_+-=[]{}|;:,.<>?", "Special characters"), ("special-safe", "!@#$%^&*_+-=<>?", "Safe special chars (no pipes/brackets)"), ("cyrillic-lower", "абвгдежзийклмнопрстуфхцчшщъыьэюя", "Cyrillic lowercase"), @@ -16,6 +17,7 @@ static CHARACTER_SETS: [(&str, &str, &str); 10] = [ ("symbols", "©®™€£¥§¶†‡•…‰′″‹›\"\'–—", "Extended symbols"), ]; +#[inline(never)] fn print_help() { println!("rs-random: Secure String Generator\n"); println!("Usage:"); @@ -28,8 +30,10 @@ fn print_help() { } } +#[inline] fn get_chars(selected_sets: &[&str]) -> Result, String> { - let mut all_chars = Vec::new(); + // Pre-allocate with estimated capacity + let mut all_chars = Vec::with_capacity(selected_sets.len() * 32); let mut unknown = Vec::new(); for &set_name in selected_sets { @@ -46,23 +50,49 @@ fn get_chars(selected_sets: &[&str]) -> Result, String> { return Err("Empty character pool".to_string()); } - // Remove duplicates - let mut unique_chars = Vec::new(); - let mut seen = HashSet::new(); + // Optimize for common case where no duplicates exist + if selected_sets.len() == 1 || + (selected_sets.len() <= 4 && !selected_sets.iter().any(|&s| + s == "numbers" && (selected_sets.contains(&"hex") || + selected_sets.contains(&"lowercase") || + selected_sets.contains(&"uppercase")))) { + return Ok(all_chars); + } + + // Remove duplicates only when necessary + let mut unique_chars = Vec::with_capacity(all_chars.len()); + let mut seen = HashSet::with_capacity(all_chars.len()); for ch in all_chars { if seen.insert(ch) { unique_chars.push(ch); } } + unique_chars.shrink_to_fit(); Ok(unique_chars) } +#[inline] fn generate_random_string(chars: &[char], length: usize) -> String { let mut rng = OsRng; - (0..length) - .map(|_| *chars.choose(&mut rng).unwrap()) - .collect() + let mut result = String::with_capacity(length); + + // Unroll loop for better performance on small strings + let chunks = length / 4; + let remainder = length % 4; + + for _ in 0..chunks { + result.push(*chars.choose(&mut rng).unwrap()); + result.push(*chars.choose(&mut rng).unwrap()); + result.push(*chars.choose(&mut rng).unwrap()); + result.push(*chars.choose(&mut rng).unwrap()); + } + + for _ in 0..remainder { + result.push(*chars.choose(&mut rng).unwrap()); + } + + result } fn main() { @@ -82,11 +112,12 @@ fn main() { match args[i].as_str() { "-l" => { if i + 1 < args.len() { - if let Ok(n) = args[i + 1].parse::() { - length = n; - } else { - eprintln!("Error: Invalid length"); - process::exit(1); + match args[i + 1].parse::() { + Ok(n) => length = n, + Err(_) => { + eprintln!("Error: Invalid length"); + process::exit(1); + } } i += 2; } else { @@ -105,11 +136,12 @@ fn main() { } "-c" => { if i + 1 < args.len() { - if let Ok(n) = args[i + 1].parse::() { - count = n; - } else { - eprintln!("Error: Invalid count"); - process::exit(1); + match args[i + 1].parse::() { + Ok(n) => count = n, + Err(_) => { + eprintln!("Error: Invalid count"); + process::exit(1); + } } i += 2; } else { @@ -117,12 +149,13 @@ fn main() { process::exit(1); } } - arg if i == 1 && !arg.starts_with("-") => { - if let Ok(n) = arg.parse::() { - length = n; - } else { - eprintln!("Error: Invalid length"); - process::exit(1); + arg if i == 1 && !arg.starts_with('-') => { + match arg.parse::() { + Ok(n) => length = n, + Err(_) => { + eprintln!("Error: Invalid length"); + process::exit(1); + } } i += 1; }