diff --git a/Cargo.lock b/Cargo.lock index bc57c21..e327ee9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ [[package]] name = "rs-random" -version = "0.1.0" +version = "0.2.1" dependencies = [ "rand", ] diff --git a/Cargo.toml b/Cargo.toml index 6c91fd0..c19fe84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rs-random" -version = "0.1.0" +version = "0.2.1" edition = "2024" [dependencies] diff --git a/README.md b/README.md index 7e4e3ab..739587a 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,30 @@ **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 -- Very small binary size (optimized release profile) -- Minimal dependencies (only the `rand` crate with `std_rng` and `getrandom` features) -- Flexible selection of character sets, including alphabets, numbers, symbols, and more +- **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 +### Windows + +Download the latest Windows executable from the [releases page](https://git.caileb.com/Caileb/rs-random/releases). + ### From Source ```bash @@ -30,14 +39,25 @@ cargo build --release ```bash rs-random [LENGTH] rs-random -l [-s ] [-c ] -rs-random -h | --help +rs-random -h ``` - `LENGTH` <number>: Length of each string (default: `16`) -- `-l, --length `: Specify the string length -- `-s, --sets `: Comma-separated list of character sets to include (default: `lowercase,uppercase,numbers,special-safe`) -- `-c, --count `: Number of strings to generate (default: `1`) -- `-h, --help`: Show help information +- `-l `: Specify the string length +- `-s `: Comma-separated list of character sets to include (default: `lowercase,uppercase,numbers,special-safe`) +- `-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 @@ -46,6 +66,7 @@ rs-random -h | --help | 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 | @@ -62,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 5c1e3b2..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,20 +17,23 @@ static CHARACTER_SETS: [(&str, &str, &str); 10] = [ ("symbols", "©®™€£¥§¶†‡•…‰′″‹›\"\'–—", "Extended symbols"), ]; +#[inline(never)] fn print_help() { println!("rs-random: Secure String Generator\n"); println!("Usage:"); println!(" rs-random [LENGTH] (uses safe defaults)"); println!(" rs-random -l [-s ] [-c ]"); - println!(" rs-random -h | --help\n"); + println!(" rs-random -h\n"); println!("Available sets (for -s, comma-separated):"); for &(name, _, desc) in &CHARACTER_SETS { println!(" {:<15} - {}", name, desc); } } +#[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,29 +50,55 @@ 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() { let args: Vec = env::args().collect(); - if args.len() < 2 || args.contains(&"-h".to_string()) || args.contains(&"--help".to_string()) { + if args.len() < 2 || args.contains(&"-h".to_string()) { print_help(); return; } @@ -80,13 +110,14 @@ fn main() { let mut i = 1; while i < args.len() { match args[i].as_str() { - "-l" | "--length" => { + "-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 { @@ -94,7 +125,7 @@ fn main() { process::exit(1); } } - "-s" | "--sets" => { + "-s" => { if i + 1 < args.len() { sets = &args[i + 1]; i += 2; @@ -103,13 +134,14 @@ fn main() { process::exit(1); } } - "-c" | "--count" => { + "-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; }