### htauth usage example Source: https://docs.rs/htauth/latest/htauth/index.html Shows how to use the htauth crate to open an htpasswd file, add a user with bcrypt hashing, verify a password, list users, and save changes. Requires the htauth crate and error handling with the ? operator. ```rust use htauth::{Htpasswd, HashAlgorithm}; // Create or open an htpasswd file let mut htpasswd = Htpasswd::open(".htpasswd")?; // Add a new user htpasswd.add_user("alice", "password123", HashAlgorithm::Bcrypt)?; // Verify a user's password if htpasswd.verify_user("alice", "password123")? { println!("Password correct!"); } // List all users for user in htpasswd.list_users() { println!("{}", user); } // Save changes htpasswd.save()?; ``` -------------------------------- ### Get number of users Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Returns the number of users in the htpasswd file. ```rust /// Get the number of users in the htpasswd file. pub fn user_count(&self) -> usize { self.entries.len() } ``` -------------------------------- ### Get file path Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Returns the file path of the htpasswd file. ```rust /// Get the file path. pub fn path(&self) -> &Path { &self.path } ``` -------------------------------- ### Add User and Verify File Content Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Demonstrates adding a user with SHA-256 hashing and verifying the resulting file content. ```rust .add_user("bob", "password456", HashAlgorithm::Sha256) .unwrap(); htpasswd.save().unwrap(); // Verify file exists and has correct content assert!(file_path.exists()); let content = fs::read_to_string(&file_path).unwrap(); assert!(content.contains("alice:")); assert!(content.contains("bob:")); // SHA-256 hashes now use SHA-crypt format: $5$rounds=5000$salt$hash assert!(content.contains("$5$")); } ``` -------------------------------- ### Module Documentation and Imports Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html The module-level documentation provides an overview of the APR1-MD5 algorithm, its source references, and a security warning. Imports include MD5, snafu for error handling, subtle for constant-time comparison, and zeroize for digit zeroing. ```rust //! Apache APR1-MD5 password hashing implementation. //! //! This module implements the APR1-MD5 password hashing algorithm used by //! Apache's htpasswd tool. The format is: `$apr1$salt$hash` //! //! # Algorithm Source //! //! This implementation is based on the Apache Portable Runtime (APR) library's //! `apr_md5_encode()` function from `crypto/apr_md5.c`. The APR implementation //! itself is derived from the FreeBSD 3.0 MD5 crypt() function (Beer-Ware License). //! //! Key references: //! - Apache APR: //! - Apache htpasswd: //! - FreeBSD crypt.c: //! //! The itoa64 alphabet and encoding scheme are defined in APR's `to64()` function: //! ```c //! static unsigned char itoa64[] = //! "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; //! ``` //! //! # Security Warning //! //! MD5 is cryptographically broken. This implementation is provided for //! compatibility with existing Apache htpasswd files only. Do not use //! APR1-MD5 for new password hashes if possible - use bcrypt instead. use md5::{Digest, Md5}; use snafu::{ResultExt, Snafu}; use subtle::ConstantTimeEq; use zeroize::Zeroize; ``` -------------------------------- ### Htpasswd::open Source: https://docs.rs/htauth/latest/index.html Opens an existing htpasswd file or creates a new one. Returns a Htpasswd instance for further operations. ```APIDOC ## Htpasswd::open ### Description Opens an htpasswd file for reading and modification. If the file does not exist, it may be created. ### Method open ### Signature `pub fn open(path: &str) -> Result` ### Parameters - **path** (string) - Required - Path to the htpasswd file. ### Returns - **Result** - The Htpasswd instance or an error. ### Example ```rust let mut htpasswd = Htpasswd::open(".htpasswd")?; ``` ``` -------------------------------- ### Add user with hash algorithm Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Adds a new user with the given password and hash algorithm. Fails with UserAlreadyExistsSnafu if the username already exists. ```rust /// Add a new user with the given password and hash algorithm. pub fn add_user( &mut self, username: &str, password: &str, algorithm: HashAlgorithm, ) -> Result<(), Error> { match self.entries.entry(username.to_string()) { Entry::Occupied(_) => UserAlreadyExistsSnafu { username }.fail(), Entry::Vacant(entry) => { let hash = hash_password(password, algorithm).context(HashSnafu)?; entry.insert(hash); Ok(()) } } } ``` -------------------------------- ### Test create and save htpasswd file Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Test that creates a temporary htpasswd file, adds a user with Bcrypt, and saves it. Uses tempfile for a temporary directory. ```rust #[cfg(test)] mod tests { use super::*; use std::fs; use tempfile::tempdir; #[test] fn test_create_and_save() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); htpasswd .add_user("alice", "password123", HashAlgorithm::Bcrypt) .unwrap(); htpasswd ``` -------------------------------- ### C Byte Ordering for MD5 Encoding (from apr_md5.c) Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html Shows the byte ordering logic used in Apache's to64() calls for encoding a 16-byte MD5 digest into a 22-character string. ```c l = (final[ 0]<<16) | (final[ 6]<<8) | final[12]; to64(p, l, 4); p += 4; l = (final[ 1]<<16) | (final[ 7]<<8) | final[13]; to64(p, l, 4); p += 4; l = (final[ 2]<<16) | (final[ 8]<<8) | final[14]; to64(p, l, 4); p += 4; l = (final[ 3]<<16) | (final[ 9]<<8) | final[15]; to64(p, l, 4); p += 4; l = (final[ 4]<<16) | (final[10]<<8) | final[ 5]; to64(p, l, 4); p += 4; l = final[11]; to64(p, l, 2); p += 2; ``` -------------------------------- ### List all usernames Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Returns a sorted list of all usernames in the htpasswd file. ```rust /// List all usernames in the htpasswd file. pub fn list_users(&self) -> Vec { let mut users: Vec = self.entries.keys().cloned().collect(); users.sort(); users } ``` -------------------------------- ### Tests for hashing, verification, and detection Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Unit tests for hash_password, verify_password, and detect_algorithm. Covers bcrypt variants ($2b$, $2y$, $2a$), SHA-256 ($5$), SHA-512 ($6$), APR1-MD5 ($apr1$), and unknown input returning None. ```rust #[cfg(test)] mod tests { use super::*; #[test] fn test_bcrypt_hash_and_verify() { let password = "test_password_123"; let hash = hash_password(password, HashAlgorithm::Bcrypt).unwrap(); assert!(hash.starts_with("$2b$")); assert!(verify_password(password, &hash).unwrap()); assert!(!verify_password("wrong_password", &hash).unwrap()); } #[test] fn test_sha256_hash_and_verify() { let password = "test_password_123"; let hash = hash_password(password, HashAlgorithm::Sha256).unwrap(); // SHA-crypt format: $5$rounds=5000$salt$hash assert!(hash.starts_with("$5$")); println!("Generated hash: {}", hash); // Try direct library verification use sha_crypt::{PasswordVerifier, ShaCrypt}; let sha_crypt = ShaCrypt::default(); match sha_crypt.verify_password(password.as_bytes(), hash.as_str()) { Ok(_) => println!("Direct verification SUCCESS"), Err(e) => println!("Direct verification FAILED: {:?}", e), } assert!(verify_password(password, &hash).unwrap()); assert!(!verify_password("wrong_password", &hash).unwrap()); } #[test] fn test_sha512_hash_and_verify() { let password = "test_password_123"; let hash = hash_password(password, HashAlgorithm::Sha512).unwrap(); // SHA-crypt format: $6$rounds=5000$salt$hash assert!(hash.starts_with("$6$")); assert!(verify_password(password, &hash).unwrap()); assert!(!verify_password("wrong_password", &hash).unwrap()); } #[test] fn test_detect_algorithm() { assert_eq!(detect_algorithm("$2b$12$abc"), Some(HashAlgorithm::Bcrypt)); assert_eq!(detect_algorithm("$2y$12$abc"), Some(HashAlgorithm::Bcrypt)); assert_eq!(detect_algorithm("$2a$12$abc"), Some(HashAlgorithm::Bcrypt)); assert_eq!( detect_algorithm("$5$rounds=5000$salt$hash"), Some(HashAlgorithm::Sha256) ); assert_eq!( detect_algorithm("$5$salt$hash"), Some(HashAlgorithm::Sha256) ); assert_eq!( detect_algorithm("$6$rounds=5000$salt$hash"), Some(HashAlgorithm::Sha512) ); assert_eq!( detect_algorithm("$6$salt$hash"), Some(HashAlgorithm::Sha512) ); assert_eq!( detect_algorithm("$apr1$salt$hash"), Some(HashAlgorithm::Apr1Md5) ); assert_eq!(detect_algorithm("unknown"), None); } } ``` -------------------------------- ### Verify password against hash Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Verifies a password against a hash string by detecting the algorithm and dispatching to the appropriate verifier. For SHA-256/SHA-512, sha-crypt returns Ok(true) for valid and Ok(false) for invalid, not Err. ```rust pub fn verify_password(password: &str, hash_str: &str) -> Result { let algorithm = detect_algorithm(hash_str).context(InvalidHashFormatSnafu { hash: hash_str })?; match algorithm { HashAlgorithm::Bcrypt => { // All bcrypt variants ($2a$, $2b$, $2y$) are compatible for verification verify(password, hash_str).context(BcryptVerifySnafu) } HashAlgorithm::Sha256 | HashAlgorithm::Sha512 => { // sha-crypt handles both SHA-256 and SHA-512 verification // Returns Ok(true) if valid, Ok(false) if invalid (not Err) let sha_crypt = ShaCrypt::default(); Ok(sha_crypt .verify_password(password.as_bytes(), hash_str) .is_ok()) } HashAlgorithm::Apr1Md5 => { // APR1-MD5 verification Ok(crate::apr1_md5::verify(password, hash_str)) } } } ``` -------------------------------- ### Test List Users Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Verifies that the list of users is returned correctly, sorted alphabetically. ```rust #[test] fn test_list_users() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); htpasswd .add_user("charlie", "password123", HashAlgorithm::Bcrypt) .unwrap(); htpasswd .add_user("alice", "password456", HashAlgorithm::Bcrypt) .unwrap(); htpasswd .add_user("bob", "password789", HashAlgorithm::Bcrypt) .unwrap(); let users = htpasswd.list_users(); assert_eq!(users, vec!["alice", "bob", "charlie"]); } ``` -------------------------------- ### Save htpasswd file with permissions and comments Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Saves the htpasswd file, creating parent directories if needed, setting restrictive permissions (0o600) on Unix when creating a new file, writing comments and entries, and flushing. Uses snafu error contexts. ```rust /// Save the htpasswd file. pub fn save(&self) -> Result<(), Error> { // Create parent directories if needed if let Some(parent) = self.path.parent() && !parent.exists() { fs::create_dir_all(parent).context(CreateDirSnafu { path: parent.to_path_buf(), })?; } // Store reference to avoid repeated clones let path = self.path.as_path(); // Set restrictive permissions when creating new file #[cfg(unix)] let mut file = if self.file_existed { OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&self.path) .context(FileSaveSnafu { path })? } else { OpenOptions::new() .write(true) .create(true) .truncate(true) .mode(0o600) .open(&self.path) .context(FileSaveSnafu { path })? }; #[cfg(not(unix))] let mut file = OpenOptions::new() .write(true) .create(true) .truncate(true) .open(&self.path) .context(FileSaveSnafu { path })?; // Write comments first for comment in &self.comments { writeln!(file, "{}", comment).context(FileSaveSnafu { path })?; } // Write entries for (username, hash) in &self.entries { writeln!(file, "{}:{}", username, hash).context(FileSaveSnafu { path })?; } file.flush().context(FileSaveSnafu { path })?; Ok(()) } ``` -------------------------------- ### Htpasswd::add_user Source: https://docs.rs/htauth/latest/index.html Adds a new user to the htpasswd file with the specified password and hashing algorithm. ```APIDOC ## Htpasswd::add_user ### Description Adds a new user with the given username and password, hashed using the specified algorithm. ### Method add_user ### Signature `pub fn add_user(&mut self, username: &str, password: &str, algorithm: HashAlgorithm) -> Result<(), HtpasswdError>` ### Parameters - **username** (string) - Required - The username to add. - **password** (string) - Required - The plaintext password. - **algorithm** (HashAlgorithm) - Required - The hashing algorithm to use. ### Returns - **Result<(), HtpasswdError>** - Ok on success, or an error. ### Example ```rust htpasswd.add_user("alice", "password123", HashAlgorithm::Bcrypt)?; ``` ``` -------------------------------- ### Hash password with SHA-512 and APR1-MD5 Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Handles SHA-512 and APR1-MD5 hashing in the hash_password function. Uses ShaCrypt with Algorithm::Sha512Crypt and DEFAULT_ROUNDS, generates a random salt via getrandom, and calls apr1_md5::generate_salt for Apache htpasswd compatibility. ```rust .hash_password_with_salt(password.as_bytes(), &salt_bytes) .context(ShaCryptHashSnafu)?; Ok(hash.to_string()) } HashAlgorithm::Sha512 => { let params = Params::new(DEFAULT_ROUNDS).context(ShaCryptParamsSnafu)?; let sha_crypt = ShaCrypt::new(Algorithm::Sha512Crypt, params); let mut salt_bytes = [0u8; SALT_BYTE_LEN]; getrandom::fill(&mut salt_bytes).context(SaltGenerationSnafu)?; let hash = sha_crypt .hash_password_with_salt(password.as_bytes(), &salt_bytes) .context(ShaCryptHashSnafu)?; Ok(hash.to_string()) } HashAlgorithm::Apr1Md5 => { // APR1-MD5 for compatibility with Apache htpasswd let salt = crate::apr1_md5::generate_salt().context(Apr1SaltSnafu)?; Ok(crate::apr1_md5::hash(password, &salt)) } } } ``` -------------------------------- ### Struct Htpasswd Source: https://docs.rs/htauth/latest/htauth/struct.Htpasswd.html The Htpasswd struct provides methods to manage an htpasswd file, including opening, saving, reloading, adding, updating, deleting, verifying, listing, and checking users. ```APIDOC ## Struct Htpasswd ### Description Represents an htpasswd file with user credentials. ### Methods #### open(path: impl AsRef) -> Result Open an htpasswd file, creating it if it doesn't exist. #### save(&self) -> Result<(), Error> Save the htpasswd file. #### reload(&mut self) -> Result<(), Error> Reload the htpasswd file from disk. #### add_user(&mut self, username: &str, password: &str, algorithm: HashAlgorithm) -> Result<(), Error> Add a new user with the given password and hash algorithm. #### update_user(&mut self, username: &str, password: &str, algorithm: HashAlgorithm) -> Result<(), Error> Update an existing user's password with the specified algorithm. #### delete_user(&mut self, username: &str) -> Result<(), Error> Delete a user from the htpasswd file. #### verify_user(&self, username: &str, password: &str) -> Result Verify a user's password. #### list_users(&self) -> Vec List all usernames in the htpasswd file. #### user_exists(&self, username: &str) -> bool Check if a user exists in the htpasswd file. #### user_count(&self) -> usize Get the number of users in the htpasswd file. #### path(&self) -> &Path Get the file path. ``` -------------------------------- ### Test Verify User Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Tests credential verification for existing, incorrect, and non-existent users. ```rust #[test] fn test_verify_user() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); htpasswd .add_user("alice", "password123", HashAlgorithm::Bcrypt) .unwrap(); htpasswd.save().unwrap(); // Reload and verify let htpasswd = Htpasswd::open(&file_path).unwrap(); assert!(htpasswd.verify_user("alice", "password123").unwrap()); assert!(!htpasswd.verify_user("alice", "wrongpassword").unwrap()); assert!(htpasswd.verify_user("nonexistent", "password").is_err()); } ``` -------------------------------- ### Htpasswd::verify_user Source: https://docs.rs/htauth/latest/index.html Verifies a user's password against the stored hash in the htpasswd file. ```APIDOC ## Htpasswd::verify_user ### Description Checks if the provided password matches the stored hash for the given username. ### Method verify_user ### Signature `pub fn verify_user(&self, username: &str, password: &str) -> Result` ### Parameters - **username** (string) - Required - The username to verify. - **password** (string) - Required - The plaintext password to check. ### Returns - **Result** - Ok(true) if password matches, Ok(false) otherwise, or an error. ### Example ```rust if htpasswd.verify_user("alice", "password123")? { println!("Password correct!"); } ``` ``` -------------------------------- ### APR1-MD5 Constants and Salt Generation Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html Defines the custom ITOA64 alphabet, APR1_PREFIX, APR1_SALT_LEN, and APR1_ROUNDS constants used by the module. ```rust /// Custom base64 alphabet (itoa64) used by APR1-MD5. /// /// This is the same alphabet used in Apache APR's `to64()` function and /// SHA-crypt. It differs from standard base64 by starting with `./` instead /// of `A-Za-z`. /// /// Source: Apache apr-util/crypto/apr_md5.c const ITOA64: &[u8; 64] = b"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; /// APR1-MD5 hash prefix pub const APR1_PREFIX: &str = "$apr1$"; /// Salt length in characters for APR1-MD5 pub const APR1_SALT_LEN: usize = 8; /// Number of MD5 rounds in APR1 algorithm const APR1_ROUNDS: u32 = 1000; ``` -------------------------------- ### Detect hash algorithm from prefix Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Detects the hash algorithm by checking the hash string prefix against BCRYPT_PREFIX, SHA256_PREFIX, SHA512_PREFIX, and APR1_PREFIX. Returns None if no prefix matches. ```rust pub fn detect_algorithm(hash: &str) -> Option { if hash.starts_with(BCRYPT_PREFIX) { Some(HashAlgorithm::Bcrypt) } else if hash.starts_with(SHA256_PREFIX) { Some(HashAlgorithm::Sha256) } else if hash.starts_with(SHA512_PREFIX) { Some(HashAlgorithm::Sha512) } else if hash.starts_with(APR1_PREFIX) { Some(HashAlgorithm::Apr1Md5) } else { None } } ``` -------------------------------- ### APR1 Roundtrip and Salt Utility Tests Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html Tests for full hash-and-verify cycles, salt length, character validity, and salt truncation behavior. ```rust #[test] fn test_apr1_hash_and_verify_roundtrip() { let password = "my_secure_password_123"; let salt = generate_salt().unwrap(); let hash_str = hash(password, &salt); assert!(verify(password, &hash_str)); assert!(!verify("wrong_password", &hash_str)); } #[test] fn test_generate_salt_length() { let salt = generate_salt().unwrap(); assert_eq!(salt.len(), APR1_SALT_LEN); } #[test] fn test_generate_salt_valid_chars() { let salt = generate_salt().unwrap(); // All characters should be from the itoa64 alphabet for ch in salt.chars() { assert!( ITOA64.contains(&(ch as u8)), "Invalid salt character: {}", ch ); } } #[test] fn test_salt_truncation() { // Salt longer than 8 chars should be truncated let password = "test"; let long_salt = "abcdefgh12345678"; let hash1 = hash(password, long_salt); let hash2 = hash(password, "abcdefgh"); assert_eq!(hash1, hash2); } ``` -------------------------------- ### verify_password Source: https://docs.rs/htauth/latest/htauth/fn.verify_password.html Verifies a password against a hash. ```APIDOC ## verify_password ### Description Verifies a password against a hash. ### Function Signature ```rust pub fn verify_password(password: &str, hash_str: &str) -> Result ``` ### Parameters - **password** (`&str`) - The password to verify. - **hash_str** (`&str`) - The hash to verify against. ### Returns - `Result` - Returns `Ok(true)` if the password matches the hash, `Ok(false)` if it does not, or an `Error` if verification fails. ``` -------------------------------- ### generate_salt Function (Partial) Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html Generates an 8-character salt using the itoa64 alphabet, based on 6 random bytes. The code is truncated in the documentation. ```rust pub fn generate_salt() -> Result { let mut salt_bytes = [0u8; 6]; // 6 bytes * 8 bits = 48 bits = 8 chars * 6 bits getrandom::fill(&mut salt_bytes).context(SaltGenerationSnafu)?; // Encode using the same technique as the C code's generate_salt() let mut salt = String::with_capacity(APR1_SALT_LEN); let mut val: u32 = 0; let mut bits: u32 = 0; let mut n = 0; ``` -------------------------------- ### Test Delete Nonexistent User Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Verifies that attempting to delete a user that does not exist returns an Error::UserNotFound error. ```rust #[test] fn test_delete_nonexistent_user() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); let result = htpasswd.delete_user("nonexistent"); assert!(matches!(result, Err(Error::UserNotFound { .. }))); } ``` -------------------------------- ### APR1 Hash Generation Tests Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html Unit tests verifying hash generation against known OpenSSL test vectors for specific passwords and salts. ```rust #[test] fn test_apr1_hash_hello() { // Test vector from openssl: openssl passwd -apr1 -salt xlWep/gn hello // Expected: $apr1$xlWep/gn$6UNiHq3WE714EKfeH2X5c. let password = "hello"; let salt = "xlWep/gn"; let result = hash(password, salt); println!("Generated hash for 'hello': {}", result); assert!(result.starts_with("$apr1$xlWep/gn$")); assert_eq!(result, "$apr1$xlWep/gn$6UNiHq3WE714EKfeH2X5c."); } #[test] fn test_apr1_hash_password() { // Test vector from openssl: openssl passwd -apr1 -salt lZL6V/ci password // Expected: $apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00 let password = "password"; let salt = "lZL6V/ci"; let result = hash(password, salt); println!("Generated hash for 'password': {}", result); assert!(result.starts_with("$apr1$lZL6V/ci$")); assert_eq!(result, "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00"); } ``` -------------------------------- ### Htpasswd::list_users Source: https://docs.rs/htauth/latest/index.html Returns a list of all usernames in the htpasswd file. ```APIDOC ## Htpasswd::list_users ### Description Retrieves a list of all usernames stored in the htpasswd file. ### Method list_users ### Signature `pub fn list_users(&self) -> Vec` ### Parameters None. ### Returns - **Vec** - A vector of usernames. ### Example ```rust for user in htpasswd.list_users() { println!("{}", user); } ``` ``` -------------------------------- ### Test Apache SHA-256 hash verification Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Verifies that the library can correctly identify and validate hashes generated by Apache htpasswd using the SHA-256 algorithm. ```rust #[test] fn test_apache_sha256_format() { // Test that we can verify a hash generated by Apache htpasswd // This hash was generated with: htpasswd -nb2 testuser testpass123 let apache_hash = "$5$6.O43dJ8bymsjcrp$QzfN4hZywImpvc6uE0O8TR.Xe87tgyvATwyV61rQbR5"; assert!(detect_algorithm(apache_hash) == Some(HashAlgorithm::Sha256)); // Debug: try direct library call use sha_crypt::{PasswordVerifier, ShaCrypt}; let sha_crypt = ShaCrypt::default(); match sha_crypt.verify_password(b"testpass123", apache_hash) { Ok(_) => println!("Direct Apache verification SUCCESS"), Err(e) => println!("Direct Apache verification FAILED: {:?}", e), } assert!(verify_password("testpass123", apache_hash).unwrap()); assert!(!verify_password("wrongpass", apache_hash).unwrap()); } ``` -------------------------------- ### Htpasswd::save Source: https://docs.rs/htauth/latest/index.html Saves the current state of the htpasswd file back to disk. ```APIDOC ## Htpasswd::save ### Description Writes the current in-memory htpasswd data to the file. ### Method save ### Signature `pub fn save(&self) -> Result<(), HtpasswdError>` ### Parameters None. ### Returns - **Result<(), HtpasswdError>** - Ok on success, or an error. ### Example ```rust htpasswd.save()?; ``` ``` -------------------------------- ### Test HashAlgorithm detection and string conversion Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Tests for detecting hash algorithms from strings and converting string identifiers to HashAlgorithm variants. ```rust Some(HashAlgorithm::Sha512) ); assert_eq!( detect_algorithm("$6$salt$hash"), Some(HashAlgorithm::Sha512) ); assert_eq!(detect_algorithm("invalid"), None); } #[test] fn test_algorithm_from_str() { assert_eq!( HashAlgorithm::from_str("bcrypt").unwrap(), HashAlgorithm::Bcrypt ); assert_eq!( HashAlgorithm::from_str("BCRYPT").unwrap(), HashAlgorithm::Bcrypt ); assert_eq!( HashAlgorithm::from_str("sha256").unwrap(), HashAlgorithm::Sha256 ); assert_eq!( HashAlgorithm::from_str("sha512").unwrap(), HashAlgorithm::Sha512 ); assert!(HashAlgorithm::from_str("invalid").is_err()); } ``` -------------------------------- ### Test User Exists Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Checks the user_exists method before and after adding a user. ```rust #[test] fn test_user_exists() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); assert!(!htpasswd.user_exists("alice")); htpasswd .add_user("alice", "password123", HashAlgorithm::Bcrypt) .unwrap(); assert!(htpasswd.user_exists("alice")); assert!(!htpasswd.user_exists("bob")); } ``` -------------------------------- ### Test Update User Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Tests updating an existing user's password and hash algorithm. ```rust #[test] fn test_update_user() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); htpasswd .add_user("alice", "password123", HashAlgorithm::Bcrypt) .unwrap(); htpasswd .update_user("alice", "newpassword", HashAlgorithm::Sha512) .unwrap(); htpasswd.save().unwrap(); let htpasswd = Htpasswd::open(&file_path).unwrap(); assert!(!htpasswd.verify_user("alice", "password123").unwrap()); assert!(htpasswd.verify_user("alice", "newpassword").unwrap()); } ``` -------------------------------- ### Test Add Duplicate User Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Verifies that attempting to add a user that already exists returns an Error::UserAlreadyExists error. ```rust #[test] fn test_add_duplicate_user() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); htpasswd .add_user("alice", "password123", HashAlgorithm::Bcrypt) .unwrap(); let result = htpasswd.add_user("alice", "password456", HashAlgorithm::Bcrypt); assert!(matches!(result, Err(Error::UserAlreadyExists { .. }))); } ``` -------------------------------- ### Reload htpasswd file from disk Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Reloads the htpasswd file by reopening it from the stored path, replacing the current state. ```rust /// Reload the htpasswd file from disk. pub fn reload(&mut self) -> Result<(), Error> { *self = Self::open(&self.path)?; Ok(()) } ``` -------------------------------- ### Test Apache SHA-512 hash verification Source: https://docs.rs/htauth/latest/src/htauth/hash.rs.html Verifies that the library can correctly identify and validate hashes generated by Apache htpasswd using the SHA-512 algorithm. ```rust #[test] fn test_apache_sha512_format() { // Test that we can verify a hash generated by Apache htpasswd // This hash was generated with: htpasswd -nb5 testuser testpass123 let apache_hash = "$6$On58VpHvsS95KvxU$lbtz65RFxG2fcos.C3vi2wllW82efo8fDcO2PRVEnHwKpvS4tGE4OJ6He98QevZYUrenCYl9QiG0woAexDYkZ/"; assert!(detect_algorithm(apache_hash) == Some(HashAlgorithm::Sha512)); assert!(verify_password("testpass123", apache_hash).unwrap()); assert!(!verify_password("wrongpass", apache_hash).unwrap()); } ``` -------------------------------- ### APR1-MD5 Password Verification Function Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html The verify function checks a password against an existing APR1-MD5 hash string. ```rust pub fn verify(password: &str, hash_str: &str) -> bool { // Parse the hash to extract the salt if !hash_str.starts_with(APR1_PREFIX) { return false; } // Find the salt part (between $apr1$ and the next $) ``` -------------------------------- ### Verify user password Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Verifies a user's password against the stored hash. Fails with UserNotFoundSnafu if the username does not exist, and returns a VerifySnafu error if verification fails. ```rust /// Verify a user's password. pub fn verify_user(&self, username: &str, password: &str) -> Result { let hash = self .entries .get(username) .context(UserNotFoundSnafu { username })?; verify_password(password, hash).context(VerifySnafu) } ``` -------------------------------- ### APR1-MD5 Password Hashing Function Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html The hash function implements the multi-step APR1-MD5 algorithm, including 1000 rounds of MD5 hashing with alternating inputs. ```rust pub fn hash(password: &str, salt: &str) -> String { let password_bytes = password.as_bytes(); let salt = &salt[..salt.len().min(8)]; // Truncate to 8 chars // Step 1: hash1 = MD5(password + salt + password) let mut hasher1 = Md5::new(); hasher1.update(password_bytes); hasher1.update(salt.as_bytes()); hasher1.update(password_bytes); let hash1 = hasher1.finalize(); // Step 2: Build context = password + "$apr1$" + salt + hash1[0:16] repeated let mut context = Vec::new(); context.extend_from_slice(password_bytes); context.extend_from_slice(APR1_PREFIX.as_bytes()); context.extend_from_slice(salt.as_bytes()); // Add hash1 repeatedly for each 16-byte chunk of password length let mut i = password_bytes.len(); while i > 0 { let chunk_len = hash1.len().min(i); context.extend_from_slice(&hash1[..chunk_len]); i -= chunk_len; } // Step 3: For each bit of password length, add null byte or first char i = password_bytes.len(); while i > 0 { if (i & 1) == 1 { context.push(0); // Odd: add null byte } else { context.push(password_bytes[0]); // Even: add first password char } i >>= 1; } // Step 4: hash2 = MD5(context) let mut hasher2 = Md5::new(); hasher2.update(&context); let mut hash2 = hasher2.finalize(); context.zeroize(); // Step 5: 1000 rounds of MD5 with alternating input // Pre-calculate max input size to avoid reallocations in the hot loop let max_input_size = password_bytes.len() + 16 + 8 + password_bytes.len() + 16; let mut input = Vec::with_capacity(max_input_size); for i in 0..APR1_ROUNDS { input.clear(); // Alternate between password and hash2 if (i & 1) == 1 { input.extend_from_slice(password_bytes); } else { input.extend_from_slice(&hash2); } // Add salt every 3rd round (not 0, 3, 6, ...) if i % 3 != 0 { input.extend_from_slice(salt.as_bytes()); } // Add password every 7th round (not 0, 7, 14, ...) if i % 7 != 0 { input.extend_from_slice(password_bytes); } // Alternate between hash2 and password if (i & 1) == 1 { input.extend_from_slice(&hash2); } else { input.extend_from_slice(password_bytes); } // Compute MD5 for next round let mut hasher = Md5::new(); hasher.update(&input); hash2 = hasher.finalize(); } input.zeroize(); // Step 6: Encode the final hash let encoded_hash = encode_apr1_hash(&hash2.into()); format!("{}{}${}", APR1_PREFIX, salt, encoded_hash) } ``` -------------------------------- ### Delete user from htpasswd file Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Deletes a user from the htpasswd file. Fails with UserNotFoundSnafu if the username does not exist. ```rust /// Delete a user from the htpasswd file. pub fn delete_user(&mut self, username: &str) -> Result<(), Error> { self.entries .remove(username) .context(UserNotFoundSnafu { username })?; Ok(()) } ``` -------------------------------- ### hash_password Source: https://docs.rs/htauth/latest/htauth/fn.hash_password.html Documentation for the `hash_password` function in the `htauth` crate, which hashes a password using a specified algorithm. ```APIDOC ## hash_password ### Description Hash a password using the specified algorithm. For SHA-256 and SHA-512, uses the SHA-crypt format compatible with Apache htpasswd. ### Function Signature ```rust pub fn hash_password( password: &str, algorithm: HashAlgorithm, ) -> Result ``` ### Parameters - **password** (`&str`) - Required - The password to hash. - **algorithm** (`HashAlgorithm`) - Required - The hashing algorithm to use. ### Returns - **Result** - On success, returns the hashed password as a `String`. On failure, returns an `Error`. ### Details - For SHA-256, the format is `$5$rounds=N$salt$hash`. - For SHA-512, the format is `$6$rounds=N$salt$hash`. ``` -------------------------------- ### detect_algorithm Source: https://docs.rs/htauth/latest/htauth/fn.detect_algorithm.html Detects the hash algorithm from a hash string. ```APIDOC ## detect_algorithm ### Description Detects the hash algorithm from a hash string. ### Signature ```rust pub fn detect_algorithm(hash: &str) -> Option ``` ### Parameters - **hash** (`&str`) - The hash string to analyze. ### Returns - `Option` - The detected hash algorithm, or `None` if it cannot be determined. ``` -------------------------------- ### ItOA64 Alphabet in C (from APR) Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html The C array definition of the itoa64 alphabet, referenced as the source of the encoding scheme. ```c static unsigned char itoa64[] = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; ``` -------------------------------- ### APR1-MD5 Salt Generation Loop Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html This loop processes salt bytes to generate the encoded salt string. ```rust for _ in 0..APR1_SALT_LEN { if bits < 6 { val |= u32::from(salt_bytes[n]) << bits; n += 1; bits += 8; } salt.push(ITOA64[(val & 0x3f) as usize] as char); val >>= 6; bits -= 6; } Ok(salt) } ``` -------------------------------- ### APR1 Hash Verification Tests Source: https://docs.rs/htauth/latest/src/htauth/apr1_md5.rs.html Unit tests verifying that the verify function correctly validates passwords against known hash strings. ```rust #[test] fn test_apr1_verify_hello() { // Known hash for "hello" with salt "xlWep/gn" (from openssl) let hash_str = "$apr1$xlWep/gn$6UNiHq3WE714EKfeH2X5c."; assert!(verify("hello", hash_str)); assert!(!verify("wrong", hash_str)); } #[test] fn test_apr1_verify_password() { // Known hash for "password" with salt "lZL6V/ci" (from openssl) let hash_str = "$apr1$lZL6V/ci$eIMz/iKDkbtys/uU7LEK00"; assert!(verify("password", hash_str)); assert!(!verify("wrong", hash_str)); } #[test] fn test_apr1_verify_testpass123() { // Test vector from openssl: openssl passwd -apr1 -salt WxrZ8P3I testpass123 // Expected: $apr1$WxrZ8P3I$XD2BykvOa82I1l5jCMtbW0 let hash_str = "$apr1$WxrZ8P3I$XD2BykvOa82I1l5jCMtbW0"; assert!(verify("testpass123", hash_str)); assert!(!verify("wrongpass", hash_str)); } ``` -------------------------------- ### Update user password Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Updates an existing user's password with the specified algorithm. Fails with UserNotFoundSnafu if the username does not exist. ```rust /// Update an existing user's password with the specified algorithm. pub fn update_user( &mut self, username: &str, password: &str, algorithm: HashAlgorithm, ) -> Result<(), Error> { let entry = self .entries .get_mut(username) .context(UserNotFoundSnafu { username })?; *entry = hash_password(password, algorithm).context(HashSnafu)?; Ok(()) } ``` -------------------------------- ### Check if user exists Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Returns true if the given username exists in the htpasswd file. ```rust /// Check if a user exists in the htpasswd file. pub fn user_exists(&self, username: &str) -> bool { self.entries.contains_key(username) } ``` -------------------------------- ### Test Delete User Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Verifies that a user can be successfully removed from the htpasswd file. ```rust #[test] fn test_delete_user() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); let mut htpasswd = Htpasswd::open(&file_path).unwrap(); htpasswd .add_user("alice", "password123", HashAlgorithm::Bcrypt) .unwrap(); htpasswd.delete_user("alice").unwrap(); htpasswd.save().unwrap(); let htpasswd = Htpasswd::open(&file_path).unwrap(); assert!(!htpasswd.user_exists("alice")); } ``` -------------------------------- ### Test Comment Preservation Source: https://docs.rs/htauth/latest/src/htauth/htpasswd.rs.html Verifies that comments in the htpasswd file are preserved during operations. ```rust #[test] fn test_comment_preservation() { let dir = tempdir().unwrap(); let file_path = dir.path().join("test.htpasswd"); // Create file with comments { let mut file = File::create(&file_path).unwrap(); writeln!(file, "# This is a comment").unwrap(); ```