use crate::crypt::constants::{
AES_256_KEY_SIZE, AES_GCM_NONCE_SIZE, DIGIT_POOL_SIZE, LOWERCASE_POOL_SIZE,
MAX_CONSECUTIVE_IDENTICAL_CHARS, MAX_SEQUENTIAL_CHARS, MIN_ENCRYPTED_HEADER_SIZE,
MIN_ENTROPY_BITS, MIN_PASSWORD_LENGTH, MODERATE_UNIQUENESS_PENALTY,
MODERATE_UNIQUENESS_THRESHOLD, PBKDF2_ITERATIONS, PBKDF2_ITERATIONS_LEGACY, PBKDF2_SALT_SIZE,
SEVERE_UNIQUENESS_PENALTY, SEVERE_UNIQUENESS_THRESHOLD, SINGLE_CLASS_MIN_ENTROPY_BITS,
SPECIAL_CHAR_POOL_SIZE, UPPERCASE_POOL_SIZE,
};
use crate::crypt::private_key::ZeroizingVec;
use crate::error::JacsError;
use crate::storage::jenv::get_env_var;
use aes_gcm::AeadCore;
use aes_gcm::{
Aes256Gcm, Key, Nonce,
aead::{Aead, KeyInit, OsRng},
};
use pbkdf2::pbkdf2_hmac;
use rand::Rng;
use sha2::Sha256;
use tracing::warn;
use zeroize::Zeroize;
const WEAK_PASSWORDS: &[&str] = &[
"password",
"12345678",
"123456789",
"1234567890",
"qwerty123",
"letmein123",
"welcome123",
"admin123",
"password1",
"password123",
"iloveyou1",
"sunshine1",
"princess1",
"football1",
"monkey123",
"shadow123",
"master123",
"dragon123",
"trustno1",
"abc12345",
"abcd1234",
"qwertyuiop",
"asdfghjkl",
"zxcvbnm123",
];
pub fn password_requirements() -> String {
format!(
"Password Requirements:\n\
- At least {} characters long\n\
- Not empty or whitespace-only\n\
- Not a common/easily-guessed password\n\
- No 4+ identical characters in a row (e.g., 'aaaa')\n\
- No 5+ sequential characters (e.g., '12345', 'abcde')\n\
- Minimum {:.0} bits of entropy\n\
- Recommended: use at least 2 character types (uppercase, lowercase, digits, symbols)\n\
\n\
Tip: Set the password via the JACS_PRIVATE_KEY_PASSWORD environment variable.",
MIN_PASSWORD_LENGTH, MIN_ENTROPY_BITS
)
}
fn calculate_entropy(password: &str) -> f64 {
if password.is_empty() {
return 0.0;
}
let has_lower = password.chars().any(|c| c.is_ascii_lowercase());
let has_upper = password.chars().any(|c| c.is_ascii_uppercase());
let has_digit = password.chars().any(|c| c.is_ascii_digit());
let has_special = password.chars().any(|c| !c.is_alphanumeric());
let mut pool_size = 0;
if has_lower {
pool_size += LOWERCASE_POOL_SIZE;
}
if has_upper {
pool_size += UPPERCASE_POOL_SIZE;
}
if has_digit {
pool_size += DIGIT_POOL_SIZE;
}
if has_special {
pool_size += SPECIAL_CHAR_POOL_SIZE;
}
let unique_chars: std::collections::HashSet<char> = password.chars().collect();
pool_size = pool_size.max(unique_chars.len());
if pool_size == 0 {
return 0.0;
}
let bits_per_char = (pool_size as f64).log2();
let len = password.len() as f64;
let base_entropy = bits_per_char * len;
let uniqueness_ratio = unique_chars.len() as f64 / len;
let uniqueness_penalty = if uniqueness_ratio < SEVERE_UNIQUENESS_THRESHOLD {
SEVERE_UNIQUENESS_PENALTY
} else if uniqueness_ratio < MODERATE_UNIQUENESS_THRESHOLD {
MODERATE_UNIQUENESS_PENALTY
} else {
1.0 };
base_entropy * uniqueness_penalty
}
fn has_excessive_repetition(password: &str) -> bool {
let chars: Vec<char> = password.chars().collect();
let mut consecutive = 1;
for i in 1..chars.len() {
if chars[i] == chars[i - 1] {
consecutive += 1;
if consecutive >= MAX_CONSECUTIVE_IDENTICAL_CHARS {
return true;
}
} else {
consecutive = 1;
}
}
false
}
fn has_sequential_pattern(password: &str) -> bool {
let chars: Vec<char> = password.chars().collect();
let mut ascending = 1;
let mut descending = 1;
for i in 1..chars.len() {
let prev = chars[i - 1] as i32;
let curr = chars[i] as i32;
if curr == prev + 1 {
ascending += 1;
descending = 1;
if ascending >= MAX_SEQUENTIAL_CHARS {
return true;
}
} else if curr == prev - 1 {
descending += 1;
ascending = 1;
if descending >= MAX_SEQUENTIAL_CHARS {
return true;
}
} else {
ascending = 1;
descending = 1;
}
}
false
}
fn count_character_classes(password: &str) -> usize {
let has_lower = password.chars().any(|c| c.is_ascii_lowercase());
let has_upper = password.chars().any(|c| c.is_ascii_uppercase());
let has_digit = password.chars().any(|c| c.is_ascii_digit());
let has_special = password.chars().any(|c| !c.is_alphanumeric());
[has_lower, has_upper, has_digit, has_special]
.iter()
.filter(|&&b| b)
.count()
}
fn validate_password(password: &str) -> Result<(), JacsError> {
let trimmed = password.trim();
if trimmed.is_empty() {
return Err(format!(
"Password cannot be empty or whitespace-only.\n\n{}",
password_requirements()
)
.into());
}
if trimmed.len() < MIN_PASSWORD_LENGTH {
return Err(JacsError::CryptoError(format!(
"Password must be at least {} characters long (got {} characters).\n\nRequirements: use at least {} characters with mixed character types. \
Set JACS_PRIVATE_KEY_PASSWORD to a secure password.",
MIN_PASSWORD_LENGTH,
trimmed.len(),
MIN_PASSWORD_LENGTH
)).into());
}
let lower = trimmed.to_lowercase();
if WEAK_PASSWORDS.contains(&lower.as_str()) {
return Err(format!(
"Password is too common and easily guessable. Please use a unique password.\n\n{}",
password_requirements()
)
.into());
}
if has_excessive_repetition(trimmed) {
return Err(format!(
"Password contains too many repeated characters (4+ in a row). Use more variety.\n\n{}",
password_requirements()
)
.into());
}
if has_sequential_pattern(trimmed) {
return Err(format!(
"Password contains sequential characters (like '12345' or 'abcde'). Use a less predictable pattern.\n\n{}",
password_requirements()
)
.into());
}
let entropy = calculate_entropy(trimmed);
if entropy < MIN_ENTROPY_BITS {
let char_classes = count_character_classes(trimmed);
let suggestion = if char_classes < 2 {
"Try mixing uppercase, lowercase, numbers, and symbols."
} else {
"Try using a longer password with more varied characters."
};
return Err(JacsError::CryptoError(format!(
"Password entropy too low ({:.1} bits, minimum is {:.0} bits). {}\n\nRequirements: {}",
entropy, MIN_ENTROPY_BITS, suggestion,
"use at least 8 characters with mixed character types (uppercase, lowercase, digits, symbols)."
))
.into());
}
let char_classes = count_character_classes(trimmed);
if char_classes < 2 && entropy < SINGLE_CLASS_MIN_ENTROPY_BITS {
return Err(JacsError::CryptoError(format!(
"Password uses only {} character class(es) with insufficient length. Use at least 2 character types (uppercase, lowercase, digits, symbols) or use a longer password.\n\n{}",
char_classes,
password_requirements()
)).into());
}
Ok(())
}
pub fn check_password_strength(password: &str) -> Result<(), JacsError> {
validate_password(password)
}
fn derive_key_with_iterations(
password: &str,
salt: &[u8],
iterations: u32,
) -> [u8; AES_256_KEY_SIZE] {
let mut key = [0u8; AES_256_KEY_SIZE];
pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut key);
key
}
fn derive_key_from_password(password: &str, salt: &[u8]) -> [u8; AES_256_KEY_SIZE] {
derive_key_with_iterations(password, salt, PBKDF2_ITERATIONS)
}
pub fn resolve_private_key_password(
explicit_password: Option<&str>,
agent_id: Option<&str>,
) -> Result<String, JacsError> {
if let Some(pw) = explicit_password {
if pw.trim().is_empty() {
return Err(JacsError::ConfigError(
"Explicit password provided but empty or whitespace-only.".to_string(),
));
}
return Ok(pw.to_string());
}
if let Ok(Some(pw)) = get_env_var("JACS_PRIVATE_KEY_PASSWORD", false) {
if pw.trim().is_empty() {
return Err(JacsError::ConfigError(
"JACS_PRIVATE_KEY_PASSWORD is set but empty or whitespace-only. \
Provide a non-empty password."
.to_string(),
));
}
return Ok(pw);
}
if let Ok(Some(path_str)) = get_env_var("JACS_PASSWORD_FILE", false) {
let path = std::path::Path::new(path_str.trim());
if path.exists() {
if let Ok(contents) = std::fs::read_to_string(path) {
let pw = contents.trim_end_matches(|c| c == '\n' || c == '\r');
if !pw.is_empty() {
tracing::debug!("Using password from JACS_PASSWORD_FILE");
return Ok(pw.to_string());
}
}
}
}
let legacy_path = std::path::Path::new("./jacs_keys/.jacs_password");
if legacy_path.exists() {
if let Ok(contents) = std::fs::read_to_string(legacy_path) {
let pw = contents.trim_end_matches(|c| c == '\n' || c == '\r');
if !pw.is_empty() {
tracing::debug!("Using password from legacy .jacs_password file");
if !is_keychain_disabled() && crate::keystore::keychain::is_available() {
tracing::warn!(
"A plaintext .jacs_password file was found at '{}'. \
Consider migrating to the OS keychain with `jacs keychain set` \
and then deleting the password file.",
legacy_path.display()
);
}
return Ok(pw.to_string());
}
}
}
if !is_keychain_disabled() {
if let Some(id) = agent_id {
if let Ok(Some(pw)) = crate::keystore::keychain::get_password(id) {
tracing::debug!("Using password from OS keychain for agent {}", id);
return Ok(pw);
}
}
}
Err(JacsError::ConfigError(
"No private key password available. Options:\n\
1. Set JACS_PRIVATE_KEY_PASSWORD environment variable\n\
2. Set JACS_PASSWORD_FILE to a file path containing the password\n\
3. Use `jacs keychain set --agent-id <ID>` to store in OS keychain"
.to_string(),
))
}
fn is_keychain_disabled() -> bool {
if let Ok(Some(val)) = get_env_var("JACS_KEYCHAIN_BACKEND", false) {
return val.eq_ignore_ascii_case("disabled");
}
false
}
pub fn encrypt_private_key(private_key: &[u8]) -> Result<Vec<u8>, JacsError> {
let password = resolve_private_key_password(None, None)?;
encrypt_private_key_with_password(private_key, &password)
}
pub fn encrypt_private_key_with_password(
private_key: &[u8],
password: &str,
) -> Result<Vec<u8>, JacsError> {
validate_password(password)?;
let mut salt = [0u8; PBKDF2_SALT_SIZE];
rand::rng().fill(&mut salt[..]);
let key = derive_key_from_password(&password, &salt);
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let encrypted_data = cipher
.encrypt(&nonce, private_key)
.map_err(|e| format!("AES-GCM encryption failed: {}", e))?;
let mut encrypted_key_with_salt_and_nonce = salt.to_vec();
encrypted_key_with_salt_and_nonce.extend_from_slice(nonce.as_slice());
encrypted_key_with_salt_and_nonce.extend_from_slice(&encrypted_data);
Ok(encrypted_key_with_salt_and_nonce)
}
#[deprecated(
since = "0.6.0",
note = "Use decrypt_private_key_secure() which returns ZeroizingVec for automatic memory zeroization"
)]
pub fn decrypt_private_key(encrypted_key_with_salt_and_nonce: &[u8]) -> Result<Vec<u8>, JacsError> {
let secure = decrypt_private_key_secure(encrypted_key_with_salt_and_nonce)?;
Ok(secure.as_slice().to_vec())
}
pub fn decrypt_private_key_secure(
encrypted_key_with_salt_and_nonce: &[u8],
) -> Result<ZeroizingVec, JacsError> {
let password = resolve_private_key_password(None, None)?;
decrypt_private_key_secure_with_password(encrypted_key_with_salt_and_nonce, &password)
}
pub fn decrypt_private_key_secure_with_password(
encrypted_key_with_salt_and_nonce: &[u8],
password: &str,
) -> Result<ZeroizingVec, JacsError> {
if encrypted_key_with_salt_and_nonce.len() < MIN_ENCRYPTED_HEADER_SIZE {
return Err(JacsError::CryptoError(format!(
"Encrypted private key file is corrupted or truncated: expected at least {} bytes, got {} bytes. \
The key file may have been damaged during transfer or storage. \
Try regenerating your keys with 'jacs keygen' or restore from a backup.",
MIN_ENCRYPTED_HEADER_SIZE,
encrypted_key_with_salt_and_nonce.len()
)).into());
}
let (salt, rest) = encrypted_key_with_salt_and_nonce.split_at(PBKDF2_SALT_SIZE);
let (nonce, encrypted_data) = rest.split_at(AES_GCM_NONCE_SIZE);
let nonce_slice = Nonce::from_slice(nonce);
let mut key = derive_key_from_password(&password, salt);
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
key.zeroize();
if let Ok(decrypted_data) = cipher.decrypt(nonce_slice, encrypted_data) {
return Ok(ZeroizingVec::new(decrypted_data));
}
let mut legacy_key = derive_key_with_iterations(&password, salt, PBKDF2_ITERATIONS_LEGACY);
let legacy_cipher_key = Key::<Aes256Gcm>::from_slice(&legacy_key);
let legacy_cipher = Aes256Gcm::new(legacy_cipher_key);
legacy_key.zeroize();
let decrypted_data = legacy_cipher
.decrypt(nonce_slice, encrypted_data)
.map_err(|_| {
"Private key decryption failed: incorrect password or corrupted key file. \
Check that JACS_PRIVATE_KEY_PASSWORD matches the password used during key generation. \
If the key file is corrupted, you may need to regenerate your keys."
.to_string()
})?;
warn!(
"MIGRATION: Private key was decrypted using legacy PBKDF2 iteration count ({}). \
Re-encrypt your private key to upgrade to the current iteration count ({}) \
for improved security. Run 'jacs keygen' to regenerate keys.",
PBKDF2_ITERATIONS_LEGACY, PBKDF2_ITERATIONS
);
Ok(ZeroizingVec::new(decrypted_data))
}
pub fn decrypt_with_password(encrypted_data: &[u8], password: &str) -> Result<Vec<u8>, JacsError> {
if encrypted_data.len() < MIN_ENCRYPTED_HEADER_SIZE {
return Err(JacsError::CryptoError(format!(
"Encrypted data too short: expected at least {} bytes, got {} bytes.",
MIN_ENCRYPTED_HEADER_SIZE,
encrypted_data.len()
))
.into());
}
let (salt, rest) = encrypted_data.split_at(PBKDF2_SALT_SIZE);
let (nonce, ciphertext) = rest.split_at(AES_GCM_NONCE_SIZE);
let nonce_slice = Nonce::from_slice(nonce);
let mut key = derive_key_from_password(password, salt);
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
key.zeroize();
if let Ok(decrypted) = cipher.decrypt(nonce_slice, ciphertext) {
return Ok(decrypted);
}
let mut legacy_key = derive_key_with_iterations(password, salt, PBKDF2_ITERATIONS_LEGACY);
let legacy_cipher_key = Key::<Aes256Gcm>::from_slice(&legacy_key);
let legacy_cipher = Aes256Gcm::new(legacy_cipher_key);
legacy_key.zeroize();
legacy_cipher.decrypt(nonce_slice, ciphertext).map_err(|_| {
"Decryption failed: incorrect password or corrupted data."
.to_string()
.into()
})
}
pub fn encrypt_with_password(data: &[u8], password: &str) -> Result<Vec<u8>, JacsError> {
validate_password(password)?;
let mut salt = [0u8; PBKDF2_SALT_SIZE];
rand::rng().fill(&mut salt[..]);
let key = derive_key_from_password(password, &salt);
let cipher_key = Key::<Aes256Gcm>::from_slice(&key);
let cipher = Aes256Gcm::new(cipher_key);
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let encrypted = cipher
.encrypt(&nonce, data)
.map_err(|e| format!("AES-GCM encryption failed: {}", e))?;
let mut result = salt.to_vec();
result.extend_from_slice(nonce.as_slice());
result.extend_from_slice(&encrypted);
Ok(result)
}
pub fn reencrypt_private_key(
encrypted_data: &[u8],
old_password: &str,
new_password: &str,
) -> Result<Vec<u8>, JacsError> {
let plaintext = decrypt_with_password(encrypted_data, old_password)?;
let re_encrypted = encrypt_with_password(&plaintext, new_password)?;
Ok(re_encrypted)
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
fn set_test_password(password: &str) {
unsafe {
env::set_var("JACS_PRIVATE_KEY_PASSWORD", password);
}
}
fn remove_test_password() {
unsafe {
env::remove_var("JACS_PRIVATE_KEY_PASSWORD");
}
}
#[test]
#[serial(jacs_env)]
fn test_encrypt_decrypt_roundtrip() {
set_test_password("test_password_123");
let original_key = b"this is a test private key data that should be encrypted";
let encrypted = encrypt_private_key(original_key).expect("encryption should succeed");
assert!(encrypted.len() > original_key.len());
let decrypted = decrypt_private_key(&encrypted).expect("decryption should succeed");
assert_eq!(original_key.as_slice(), decrypted.as_slice());
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_wrong_password_fails() {
set_test_password("correct_password");
let original_key = b"secret data";
let encrypted = encrypt_private_key(original_key).expect("encryption should succeed");
set_test_password("wrong_password");
let result = decrypt_private_key(&encrypted);
assert!(result.is_err());
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_truncated_data_fails() {
set_test_password("test_password");
let short_data = vec![0u8; 20];
let result = decrypt_private_key(&short_data);
assert!(result.is_err());
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_different_salts_produce_different_ciphertexts() {
set_test_password("test_password");
let original_key = b"test data";
let encrypted1 = encrypt_private_key(original_key).expect("encryption should succeed");
let encrypted2 = encrypt_private_key(original_key).expect("encryption should succeed");
assert_ne!(encrypted1, encrypted2);
let decrypted1 = decrypt_private_key(&encrypted1).expect("decryption should succeed");
let decrypted2 = decrypt_private_key(&encrypted2).expect("decryption should succeed");
assert_eq!(decrypted1, decrypted2);
assert_eq!(original_key.as_slice(), decrypted1.as_slice());
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_empty_password_rejected() {
set_test_password("");
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("empty") || err_msg.contains("whitespace"),
"Expected error about empty/whitespace password, got: {}",
err_msg
);
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_whitespace_only_password_rejected() {
set_test_password(" \t\n ");
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("empty") || err_msg.contains("whitespace"),
"Expected error about empty/whitespace password, got: {}",
err_msg
);
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_short_password_rejected() {
set_test_password("short");
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("8 characters"));
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_minimum_length_password_accepted() {
set_test_password("xK9m$pL2");
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
assert!(
result.is_ok(),
"8-character varied password should be accepted: {:?}",
result.err()
);
remove_test_password();
}
#[test]
fn test_validate_password_unit() {
assert!(validate_password("").is_err());
assert!(validate_password(" ").is_err());
assert!(validate_password("\t\n").is_err());
assert!(validate_password("short").is_err());
assert!(validate_password("1234567").is_err()); assert!(validate_password("12345678").is_err());
assert!(validate_password("xK9m$pL2").is_ok());
assert!(validate_password("MyP@ssw0rd!").is_ok()); }
#[test]
fn test_entropy_calculation() {
let low_entropy = calculate_entropy("aaaaaaaa");
assert!(
low_entropy < 20.0,
"Repeated chars should have low entropy due to uniqueness penalty: {}",
low_entropy
);
let medium_entropy = calculate_entropy("abcdefgh");
assert!(
medium_entropy > 30.0,
"8 unique lowercase chars should have decent entropy: {}",
medium_entropy
);
let high_entropy = calculate_entropy("aB3$xY9@kL");
assert!(
high_entropy > 50.0,
"Complex password should have high entropy: {}",
high_entropy
);
let lowercase_only = calculate_entropy("abcdefgh");
let mixed_case = calculate_entropy("aBcDeFgH");
let with_numbers = calculate_entropy("aBcD1234");
let with_special = calculate_entropy("aB1$cD2@");
assert!(
mixed_case > lowercase_only,
"Mixed case should have higher entropy than lowercase only"
);
assert!(
with_numbers > mixed_case,
"Adding numbers should increase entropy"
);
assert!(
with_special > with_numbers,
"Adding special chars should increase entropy"
);
}
#[test]
fn test_common_weak_passwords_rejected() {
let weak_passwords = [
"password",
"Password",
"PASSWORD",
"12345678",
"qwerty123",
"letmein123",
"password123",
"trustno1",
];
for pwd in weak_passwords {
let result = validate_password(pwd);
assert!(
result.is_err(),
"Common password '{}' should be rejected",
pwd
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("common") || err_msg.contains("guessable"),
"Error for '{}' should mention common/guessable: {}",
pwd,
err_msg
);
}
}
#[test]
fn test_repetition_rejected() {
let repetitive_passwords = ["aaaa1234", "pass1111", "xxxx5678", "ab@@@@cd"];
for pwd in repetitive_passwords {
let result = validate_password(pwd);
assert!(
result.is_err(),
"Repetitive password '{}' should be rejected",
pwd
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("repeated"),
"Error for '{}' should mention repetition: {}",
pwd,
err_msg
);
}
}
#[test]
fn test_sequential_patterns_rejected() {
let sequential_passwords = ["12345abc", "abcdefgh", "98765xyz", "zyxwvuts"];
for pwd in sequential_passwords {
let result = validate_password(pwd);
assert!(
result.is_err(),
"Sequential password '{}' should be rejected",
pwd
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("sequential") || err_msg.contains("entropy"),
"Error for '{}' should mention sequential or entropy: {}",
pwd,
err_msg
);
}
}
#[test]
fn test_low_entropy_single_class_rejected() {
let low_entropy_passwords = ["aaaaabbb", "zzzzzzzz", "qqqqqqqq"];
for pwd in low_entropy_passwords {
let result = validate_password(pwd);
assert!(
result.is_err(),
"Low entropy password '{}' should be rejected",
pwd
);
}
}
#[test]
fn test_strong_passwords_accepted() {
let strong_passwords = [
"MyP@ssw0rd!",
"Tr0ub4dor&3",
"correct-horse-battery",
"xK9$mN2@pL5!",
"SecurePass#2024",
"n0t-a-w3ak-p@ss",
];
for pwd in strong_passwords {
let result = validate_password(pwd);
assert!(
result.is_ok(),
"Strong password '{}' should be accepted: {:?}",
pwd,
result.err()
);
}
}
#[test]
fn test_character_class_counting() {
assert_eq!(count_character_classes("abcdefgh"), 1); assert_eq!(count_character_classes("ABCDEFGH"), 1); assert_eq!(count_character_classes("12345678"), 1); assert_eq!(count_character_classes("!@#$%^&*"), 1); assert_eq!(count_character_classes("abcABC12"), 3); assert_eq!(count_character_classes("aB1!"), 4); }
#[test]
fn test_has_excessive_repetition_detection() {
assert!(!has_excessive_repetition("abc123")); assert!(!has_excessive_repetition("aabcc")); assert!(!has_excessive_repetition("aaabbb")); assert!(has_excessive_repetition("aaaab")); assert!(has_excessive_repetition("x1111y")); }
#[test]
fn test_has_sequential_pattern_detection() {
assert!(!has_sequential_pattern("abc12")); assert!(!has_sequential_pattern("1234x")); assert!(has_sequential_pattern("12345")); assert!(has_sequential_pattern("abcde")); assert!(has_sequential_pattern("54321")); assert!(has_sequential_pattern("edcba")); }
#[test]
#[serial(jacs_env)]
fn test_encryption_with_weak_password_fails() {
set_test_password("password");
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("common") || err_msg.contains("guessable"),
"Should reject common password: {}",
err_msg
);
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_encryption_with_strong_password_succeeds() {
set_test_password("MyStr0ng!Pass#2024");
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
assert!(result.is_ok(), "Strong password should work: {:?}", result);
let encrypted = result.unwrap();
let decrypted = decrypt_private_key(&encrypted);
assert!(decrypted.is_ok());
assert_eq!(decrypted.unwrap().as_slice(), original_key);
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_very_long_password_works_or_fails_gracefully() {
let long_password = "A".repeat(100_000);
set_test_password(&long_password);
let original_key = b"secret data";
let result = encrypt_private_key(original_key);
if let Ok(encrypted) = result {
let decrypted = decrypt_private_key(&encrypted);
assert!(
decrypted.is_ok(),
"If encryption with long password succeeds, decryption should too"
);
assert_eq!(decrypted.unwrap().as_slice(), original_key);
}
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_corrupted_encrypted_data_fails_gracefully() {
set_test_password("MyStr0ng!Pass#2024");
let original_key = b"secret data";
let encrypted = encrypt_private_key(original_key).expect("encryption should succeed");
let test_cases = vec![
("corrupted salt", {
let mut data = encrypted.clone();
data[0] ^= 0xFF;
data[8] ^= 0xFF;
data
}),
("corrupted nonce", {
let mut data = encrypted.clone();
data[16] ^= 0xFF; data[20] ^= 0xFF;
data
}),
("corrupted ciphertext", {
let mut data = encrypted.clone();
let mid = data.len() / 2;
data[mid] ^= 0xFF;
data
}),
("corrupted auth tag", {
let mut data = encrypted.clone();
let last = data.len() - 1;
data[last] ^= 0xFF;
data[last - 8] ^= 0xFF;
data
}),
];
for (description, corrupted_data) in test_cases {
let result = decrypt_private_key(&corrupted_data);
assert!(
result.is_err(),
"Decryption with {} should fail",
description
);
}
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_all_zeros_encrypted_data_rejected() {
set_test_password("MyStr0ng!Pass#2024");
let zeros = vec![0u8; 100];
let result = decrypt_private_key(&zeros);
assert!(
result.is_err(),
"All-zeros encrypted data should be rejected"
);
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_all_ones_encrypted_data_rejected() {
set_test_password("MyStr0ng!Pass#2024");
let ones = vec![0xFF; 100];
let result = decrypt_private_key(&ones);
assert!(
result.is_err(),
"All-ones encrypted data should be rejected"
);
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_empty_plaintext_encryption() {
set_test_password("MyStr0ng!Pass#2024");
let empty_data = b"";
let encrypted = encrypt_private_key(empty_data);
assert!(encrypted.is_ok(), "Empty data encryption should succeed");
let decrypted = decrypt_private_key(&encrypted.unwrap());
assert!(decrypted.is_ok(), "Empty data decryption should succeed");
assert_eq!(decrypted.unwrap().as_slice(), empty_data.as_slice());
remove_test_password();
}
#[test]
#[serial(jacs_env)]
fn test_large_plaintext_encryption() {
set_test_password("MyStr0ng!Pass#2024");
let large_data = vec![0x42u8; 1_000_000];
let encrypted = encrypt_private_key(&large_data);
assert!(encrypted.is_ok(), "Large data encryption should succeed");
let decrypted = decrypt_private_key(&encrypted.unwrap());
assert!(decrypted.is_ok(), "Large data decryption should succeed");
assert_eq!(decrypted.unwrap().as_slice(), large_data.as_slice());
remove_test_password();
}
#[test]
fn test_unicode_password_validation() {
let unicode_passwords = [
("P@ssw0rd\u{1F600}", true), ("密码Tr0ng!Pass", true), ("\u{0391}\u{0392}Str0ng!P@ss", true), ("\u{1F600}\u{1F600}\u{1F600}\u{1F600}", false), ];
for (password, should_pass) in unicode_passwords {
let result = validate_password(password);
if should_pass {
assert!(
result.is_ok(),
"Unicode password '{}' should be accepted: {:?}",
password,
result.err()
);
} else {
assert!(
result.is_err(),
"Low entropy unicode password '{}' should be rejected",
password
);
}
}
}
#[test]
fn test_password_with_null_bytes_handled() {
let passwords_with_nulls = ["pass\0word12!@AB", "\0passwordAB12!@", "passwordAB12!@\0"];
for password in passwords_with_nulls {
let result = validate_password(password);
let _ = result;
}
}
#[test]
fn test_password_boundary_lengths() {
let seven_chars = "aB1$xY9";
assert!(
validate_password(seven_chars).is_err(),
"7-character password should be rejected"
);
let eight_chars = "aB1$xY90";
assert!(
validate_password(eight_chars).is_ok(),
"8-character varied password should be accepted: {:?}",
validate_password(eight_chars).err()
);
}
#[test]
fn test_keyboard_pattern_passwords() {
let keyboard_patterns = [
"qwertyuiop", "asdfghjkl", "zxcvbnm123", ];
for pattern in keyboard_patterns {
let result = validate_password(pattern);
assert!(
result.is_err(),
"Keyboard pattern '{}' should be rejected",
pattern
);
}
}
#[test]
fn test_leet_speak_common_passwords() {
let result = validate_password("trustno1");
assert!(
result.is_err(),
"trustno1 should be rejected as a common weak password"
);
}
#[test]
fn test_password_requirements_returns_string() {
let reqs = password_requirements();
assert!(
reqs.contains("8 characters"),
"Should mention minimum character count: {}",
reqs
);
assert!(
reqs.contains("JACS_PRIVATE_KEY_PASSWORD"),
"Should mention env var: {}",
reqs
);
assert!(reqs.contains("entropy"), "Should mention entropy: {}", reqs);
}
#[test]
fn test_empty_password_error_contains_requirements() {
let result = validate_password("");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("Requirements") || err_msg.contains("Password Requirements"),
"Empty password error should include requirements text: {}",
err_msg
);
}
#[test]
#[serial(jacs_env)]
fn test_decrypt_with_missing_password_env_var() {
remove_test_password();
let dummy_encrypted = vec![0u8; 50];
let result = decrypt_private_key(&dummy_encrypted);
assert!(
result.is_err(),
"Decryption without password env var should fail"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("JACS_PRIVATE_KEY_PASSWORD")
|| err_msg.contains("password")
|| err_msg.contains("environment"),
"Error should mention missing password: {}",
err_msg
);
}
#[test]
fn test_reencrypt_roundtrip() {
let old_password = "OldP@ssw0rd!2024";
let new_password = "NewStr0ng!Pass#2025";
let original_data = b"this is a secret private key for testing re-encryption";
let encrypted =
encrypt_with_password(original_data, old_password).expect("encryption should succeed");
let re_encrypted = reencrypt_private_key(&encrypted, old_password, new_password)
.expect("re-encryption should succeed");
let decrypted = decrypt_with_password(&re_encrypted, new_password)
.expect("decryption with new password should succeed");
assert_eq!(original_data.as_slice(), decrypted.as_slice());
let old_result = decrypt_with_password(&re_encrypted, old_password);
assert!(
old_result.is_err(),
"Old password should not decrypt re-encrypted data"
);
}
#[test]
fn test_reencrypt_wrong_old_password_fails() {
let correct_password = "CorrectP@ss!2024";
let wrong_password = "WrongP@ssw0rd!99";
let new_password = "NewStr0ng!Pass#2025";
let original_data = b"secret key data";
let encrypted = encrypt_with_password(original_data, correct_password)
.expect("encryption should succeed");
let result = reencrypt_private_key(&encrypted, wrong_password, new_password);
assert!(
result.is_err(),
"Re-encryption with wrong old password should fail"
);
}
#[test]
fn test_reencrypt_weak_new_password_fails() {
let old_password = "OldP@ssw0rd!2024";
let weak_new_password = "password";
let original_data = b"secret key data";
let encrypted =
encrypt_with_password(original_data, old_password).expect("encryption should succeed");
let result = reencrypt_private_key(&encrypted, old_password, weak_new_password);
assert!(
result.is_err(),
"Re-encryption with weak new password should fail"
);
}
#[test]
fn test_encrypt_decrypt_with_password_roundtrip() {
let password = "TestP@ssw0rd!2024";
let data = b"test data for explicit password functions";
let encrypted = encrypt_with_password(data, password).expect("encryption should succeed");
let decrypted =
decrypt_with_password(&encrypted, password).expect("decryption should succeed");
assert_eq!(data.as_slice(), decrypted.as_slice());
}
#[test]
fn test_resolve_password_explicit_override() {
let result = resolve_private_key_password(Some("explicit_pass"), None).unwrap();
assert_eq!(result, "explicit_pass");
}
#[test]
#[serial(jacs_env)]
fn test_resolve_password_explicit_overrides_env() {
use crate::storage::jenv::set_env_var;
set_env_var("JACS_PRIVATE_KEY_PASSWORD", "env_pass").unwrap();
let result = resolve_private_key_password(Some("explicit_pass"), None).unwrap();
assert_eq!(result, "explicit_pass", "explicit should win over env");
let _ = crate::storage::jenv::clear_env_var("JACS_PRIVATE_KEY_PASSWORD");
}
#[test]
#[serial(jacs_env)]
fn test_resolve_password_none_falls_back_to_env() {
use crate::storage::jenv::set_env_var;
set_env_var("JACS_PRIVATE_KEY_PASSWORD", "env_pass").unwrap();
let result = resolve_private_key_password(None, None).unwrap();
assert_eq!(result, "env_pass", "None should fall back to env");
let _ = crate::storage::jenv::clear_env_var("JACS_PRIVATE_KEY_PASSWORD");
}
#[test]
fn test_resolve_password_explicit_empty_fails() {
let result = resolve_private_key_password(Some(""), None);
assert!(result.is_err(), "empty explicit password should fail");
let err = result.unwrap_err().to_string();
assert!(
err.contains("empty or whitespace"),
"error should mention empty: {}",
err
);
}
#[test]
fn test_resolve_password_explicit_whitespace_fails() {
let result = resolve_private_key_password(Some(" "), None);
assert!(
result.is_err(),
"whitespace-only explicit password should fail"
);
}
}