#[path = "common/mod.rs"]
mod common;
mod vault_root_safety {
use envseal::vault::Vault;
#[test]
fn rejects_tmp_root() {
let result = Vault::validate_root(std::path::Path::new("/tmp/evil-vault"));
assert!(result.is_err(), "vault should reject /tmp root");
let err = result.unwrap_err().to_string();
assert!(
err.contains("world-writable") || err.contains("hard block"),
"error should explain why: {err}"
);
}
#[test]
fn rejects_var_tmp_root() {
let result = Vault::validate_root(std::path::Path::new("/var/tmp/evil"));
assert!(result.is_err(), "vault should reject /var/tmp root");
}
#[test]
fn rejects_dev_shm_root() {
let result = Vault::validate_root(std::path::Path::new("/dev/shm/vault"));
assert!(result.is_err(), "vault should reject /dev/shm root");
}
#[test]
fn rejects_nested_tmp_root() {
let result = Vault::validate_root(std::path::Path::new("/tmp/deep/nested/path/vault"));
assert!(result.is_err(), "vault should reject nested /tmp paths");
}
#[test]
fn accepts_safe_tmpdir_root() {
let dir = tempfile::tempdir_in(std::env::var("HOME").unwrap_or("/nonexistent".into()));
if let Ok(d) = dir {
let passphrase = zeroize::Zeroizing::new("test-passphrase".to_string());
let result = Vault::open_with_passphrase(d.path(), &passphrase);
assert!(
result.is_ok(),
"vault should accept home-dir paths: {:?}",
result.err()
);
}
}
}
mod secret_name_attacks {
use crate::common::temp_vault;
#[test]
fn rejects_path_traversal() {
let (_dir, vault) = temp_vault();
let result = vault.store("../../../etc/passwd", b"evil", false);
assert!(result.is_err());
}
#[test]
fn rejects_null_byte_in_name() {
let (_dir, vault) = temp_vault();
let result = vault.store("secret\0evil", b"data", false);
assert!(result.is_err());
}
#[test]
fn rejects_hidden_name() {
let (_dir, vault) = temp_vault();
let result = vault.store(".hidden-secret", b"data", false);
assert!(result.is_err());
}
#[test]
fn rejects_slash_name() {
let (_dir, vault) = temp_vault();
assert!(vault.store("path/to/secret", b"data", false).is_err());
assert!(vault.store("/absolute/path", b"data", false).is_err());
}
#[test]
fn rejects_empty_name() {
let (_dir, vault) = temp_vault();
assert!(vault.store("", b"data", false).is_err());
}
#[test]
fn backslash_in_name() {
let (_dir, vault) = temp_vault();
let result = vault.store("test\\evil", b"data", false);
if result.is_ok() {
let dec = vault.decrypt("test\\evil").unwrap();
assert_eq!(&dec[..], b"data");
}
}
#[test]
fn name_with_seal_extension() {
let (_dir, vault) = temp_vault();
vault.store("my-key.seal", b"data", false).unwrap();
let dec = vault.decrypt("my-key.seal").unwrap();
assert_eq!(&dec[..], b"data");
}
#[test]
fn very_long_name_boundary() {
let (_dir, vault) = temp_vault();
let name: String = (0..250).map(|_| 'x').collect();
let result = vault.store(&name, b"data", false);
if result.is_ok() {
assert_eq!(&vault.decrypt(&name).unwrap()[..], b"data");
}
}
#[test]
fn whitespace_only_name() {
let (_dir, vault) = temp_vault();
let result = vault.store(" ", b"data", false);
if result.is_ok() {
assert_eq!(&vault.decrypt(" ").unwrap()[..], b"data");
}
}
}
mod crypto_attacks {
use crate::common::{secret_file_path, temp_vault};
#[test]
fn truncated_ciphertext_detected() {
let (dir, vault) = temp_vault();
vault.store("target", b"secret-data", false).unwrap();
let seal_path = secret_file_path(dir.path(), "target");
let data = std::fs::read(&seal_path).unwrap();
std::fs::write(&seal_path, &data[..data.len() / 2]).unwrap();
let result = vault.decrypt("target");
assert!(result.is_err(), "truncated ciphertext should fail");
}
#[test]
fn bitflip_in_ciphertext_detected() {
let (dir, vault) = temp_vault();
vault.store("target", b"secret-data", false).unwrap();
let seal_path = secret_file_path(dir.path(), "target");
let mut data = std::fs::read(&seal_path).unwrap();
if data.len() > 20 {
data[20] ^= 0xFF; }
std::fs::write(&seal_path, &data).unwrap();
let result = vault.decrypt("target");
assert!(result.is_err(), "bit-flipped ciphertext should fail");
}
#[test]
fn random_garbage_detected() {
let (dir, vault) = temp_vault();
vault.store("target", b"secret-data", false).unwrap();
let seal_path = secret_file_path(dir.path(), "target");
let garbage: Vec<u8> = (0..64).map(|i| (i * 37 % 256) as u8).collect();
std::fs::write(&seal_path, &garbage).unwrap();
let result = vault.decrypt("target");
assert!(result.is_err(), "random garbage should fail");
}
#[test]
fn empty_sealed_file_detected() {
let (dir, vault) = temp_vault();
vault.store("target", b"secret-data", false).unwrap();
let seal_path = secret_file_path(dir.path(), "target");
std::fs::write(&seal_path, b"").unwrap();
let result = vault.decrypt("target");
assert!(result.is_err(), "empty sealed file should fail");
}
#[test]
fn nonce_uniqueness_on_overwrite() {
let (dir, vault) = temp_vault();
vault.store("nonce-test", b"same-value", false).unwrap();
let seal_path = secret_file_path(dir.path(), "nonce-test");
let ct1 = std::fs::read(&seal_path).unwrap();
vault.store("nonce-test", b"same-value", true).unwrap();
let ct2 = std::fs::read(&seal_path).unwrap();
assert_ne!(
ct1, ct2,
"same value encrypted twice must produce different ciphertext (unique nonce)"
);
}
#[test]
fn different_keys_different_ciphertext() {
let (_dir1, vault1) = temp_vault();
let dir2 = tempfile::tempdir_in(std::env::var("HOME").unwrap()).unwrap();
let pass2 = crate::common::test_passphrase_alt();
let vault2 = envseal::vault::Vault::open_with_passphrase(dir2.path(), &pass2).unwrap();
vault1.store("shared", b"same-value", false).unwrap();
vault2.store("shared", b"same-value", false).unwrap();
let ct1 = std::fs::read(secret_file_path(vault1.root(), "shared")).unwrap();
let ct2 = std::fs::read(secret_file_path(dir2.path(), "shared")).unwrap();
assert_ne!(ct1, ct2, "different keys must produce different ciphertext");
}
}
mod policy_tampering {
use crate::common::{TEST_KEY_BYTES, TEST_KEY_BYTES_ALT};
use envseal::policy::Policy;
#[test]
fn detect_modified_policy() {
let dir = tempfile::tempdir_in(std::env::var("HOME").unwrap()).unwrap();
let legacy_path = dir.path().join("policy.toml");
let mut policy = Policy::default();
policy.allow_key("/usr/bin/python3", "openai-key");
policy.save_sealed(&legacy_path, &TEST_KEY_BYTES).unwrap();
let sealed_path = envseal::policy::sealed_path_for(&legacy_path);
let mut bytes = std::fs::read(&sealed_path).unwrap();
let target = bytes.len() - 1;
bytes[target] ^= 0x01;
std::fs::write(&sealed_path, &bytes).unwrap();
let result = Policy::load_sealed(&legacy_path, &TEST_KEY_BYTES);
assert!(result.is_err(), "tampered sealed policy must be rejected");
}
#[test]
fn wrong_key_rejects_policy() {
let dir = tempfile::tempdir_in(std::env::var("HOME").unwrap()).unwrap();
let path = dir.path().join("policy.toml");
let mut policy = Policy::default();
policy.allow_key("/usr/bin/app", "secret");
policy.save_signed(&path, &TEST_KEY_BYTES).unwrap();
let result = Policy::load_verified(&path, &TEST_KEY_BYTES_ALT);
assert!(result.is_err(), "wrong key should reject policy");
}
#[test]
fn empty_policy_roundtrip() {
let dir = tempfile::tempdir_in(std::env::var("HOME").unwrap()).unwrap();
let path = dir.path().join("policy.toml");
let policy = Policy::default();
policy.save_signed(&path, &TEST_KEY_BYTES).unwrap();
let loaded = Policy::load_verified(&path, &TEST_KEY_BYTES).unwrap();
assert!(!loaded.is_authorized("/any/binary", "any-secret"));
}
}
mod entropy_edge_cases {
use envseal::secret_health::{check_entropy, shannon_entropy};
#[test]
fn all_zeros_low_entropy() {
let e = shannon_entropy(&[0u8; 100]);
assert!(e < 0.1, "all zeros should be ~0 entropy, got {e}");
}
#[test]
fn all_unique_high_entropy() {
let data: Vec<u8> = (0..=255u8).collect();
let e = shannon_entropy(&data);
assert!(e > 7.9, "all unique bytes should be ~8 bits, got {e}");
}
#[test]
fn single_byte_zero_entropy() {
let e = shannon_entropy(&[42]);
assert!(
(e - 0.0).abs() < f64::EPSILON,
"single byte has 0 entropy: {e}"
);
}
#[test]
fn two_bytes_one_bit_entropy() {
let data: Vec<u8> = (0..100)
.map(|i| if i % 2 == 0 { b'a' } else { b'b' })
.collect();
let e = shannon_entropy(&data);
assert!(
(e - 1.0).abs() < 0.01,
"50/50 split should be 1 bit, got {e}"
);
}
#[test]
fn detects_changeme() {
let warnings = check_entropy("test", b"changeme");
assert!(!warnings.is_empty());
assert!(warnings
.iter()
.any(|w| w.id.as_str().starts_with("secret.entropy.placeholder.")));
}
#[test]
fn detects_replace_me() {
let warnings = check_entropy("test", b"replace-me");
assert!(!warnings.is_empty());
}
#[test]
fn detects_xxx() {
let warnings = check_entropy("test", b"xxx");
assert!(!warnings.is_empty());
}
#[test]
fn real_openai_key_clean() {
let key = b"sk-proj-abc123defghijklmnopqrstuvwxyz1234567890abcdef";
let warnings = check_entropy("openai-key", key);
let placeholder_warns: Vec<_> = warnings
.iter()
.filter(|w| w.id.as_str().starts_with("secret.entropy.placeholder."))
.collect();
assert!(
placeholder_warns.is_empty(),
"real API key flagged: {placeholder_warns:?}"
);
}
#[test]
fn connection_string_not_flagged() {
let url = b"postgres://user:R4nd0mP@ss!@db.prod.example.com:5432/mydb";
let warnings = check_entropy("db-url", url);
let placeholder_warns: Vec<_> = warnings
.iter()
.filter(|w| w.id.as_str().starts_with("secret.entropy.placeholder."))
.collect();
assert!(placeholder_warns.is_empty());
}
#[test]
fn binary_data_high_entropy() {
let data: Vec<u8> = (0..64).map(|i| (i * 37 + 19) as u8).collect();
let e = shannon_entropy(&data);
assert!(
e > 4.0,
"pseudo-random bytes should have decent entropy: {e}"
);
}
}
mod guard_hardening {
use envseal::guard;
#[test]
fn sanitized_env_removes_ld_preload() {
let clean = guard::sanitized_env();
for k in clean.keys() {
assert_ne!(k, "LD_PRELOAD", "sanitized_env must strip LD_PRELOAD");
assert_ne!(
k, "LD_LIBRARY_PATH",
"sanitized_env must strip LD_LIBRARY_PATH"
);
assert_ne!(k, "DYLD_INSERT_LIBRARIES");
}
}
#[test]
fn sanitized_env_preserves_essentials() {
let clean = guard::sanitized_env();
let keys: Vec<_> = clean.keys().map(|k| k.as_str()).collect();
assert!(keys.contains(&"PATH"), "sanitized_env must preserve PATH");
assert!(keys.contains(&"HOME"), "sanitized_env must preserve HOME");
}
#[test]
fn hash_binary_deterministic() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp.path(), b"test content").unwrap();
let h1 = guard::hash_binary(tmp.path()).unwrap();
let h2 = guard::hash_binary(tmp.path()).unwrap();
assert_eq!(h1, h2, "same file must produce same hash");
}
#[test]
fn hash_binary_different_content() {
let tmp1 = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp1.path(), b"content-a").unwrap();
let tmp2 = tempfile::NamedTempFile::new().unwrap();
std::fs::write(tmp2.path(), b"content-b").unwrap();
let h1 = guard::hash_binary(tmp1.path()).unwrap();
let h2 = guard::hash_binary(tmp2.path()).unwrap();
assert_ne!(h1, h2, "different content must produce different hashes");
}
#[test]
fn startup_audit_no_panic() {
let warnings = guard::startup_audit();
let _ = warnings;
}
}
mod concurrent_safety {
use crate::common::temp_vault;
use std::sync::Arc;
#[test]
fn concurrent_reads_safe() {
let (_dir, vault) = temp_vault();
vault.store("shared", b"shared-value", false).unwrap();
let vault = Arc::new(vault);
let mut handles = vec![];
for _ in 0..10 {
let v = Arc::clone(&vault);
handles.push(std::thread::spawn(move || {
for _ in 0..100 {
let dec = v.decrypt("shared").unwrap();
assert_eq!(&dec[..], b"shared-value");
}
}));
}
for h in handles {
h.join().unwrap();
}
}
#[test]
fn concurrent_lists_safe() {
let (_dir, vault) = temp_vault();
for i in 0..10 {
vault.store(&format!("key-{i}"), b"val", false).unwrap();
}
let vault = Arc::new(vault);
let mut handles = vec![];
for _ in 0..10 {
let v = Arc::clone(&vault);
handles.push(std::thread::spawn(move || {
for _ in 0..50 {
let names = v.list().unwrap();
assert_eq!(names.len(), 10);
}
}));
}
for h in handles {
h.join().unwrap();
}
}
}
mod totp_security {
use envseal::totp;
#[test]
fn rejects_non_numeric_codes() {
let secret = totp::generate_secret();
assert!(!totp::verify_code(&secret, "abcdef").unwrap());
assert!(!totp::verify_code(&secret, "12345a").unwrap());
assert!(!totp::verify_code(&secret, "!@#$%^").unwrap());
}
#[test]
fn accepts_current_valid_code() {
let secret = totp::generate_secret();
let code = totp::generate_code(&secret).unwrap();
assert!(totp::verify_code(&secret, &code).unwrap());
}
#[test]
fn totp_secret_encrypt_decrypt_roundtrip() {
let secret = totp::generate_secret();
let key = [0x42u8; 32];
let encrypted = totp::encrypt_secret(&secret, &key).unwrap();
let decrypted = totp::decrypt_secret(&encrypted, &key).unwrap();
assert_eq!(*decrypted, secret);
}
#[test]
fn different_keys_different_encrypted_secret() {
let secret = totp::generate_secret();
let ct1 = totp::encrypt_secret(&secret, &[1u8; 32]).unwrap();
let ct2 = totp::encrypt_secret(&secret, &[2u8; 32]).unwrap();
assert_ne!(ct1, ct2);
}
}
#[cfg(unix)]
mod file_permissions {
use crate::common::temp_vault;
use std::os::unix::fs::PermissionsExt;
#[test]
fn sealed_files_are_0600() {
let (dir, vault) = temp_vault();
vault.store("perm-test", b"secret", false).unwrap();
let seal_path = dir.path().join("vault").join("perm-test.seal");
let meta = std::fs::metadata(&seal_path).unwrap();
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "seal file should be 0600, got {mode:04o}");
}
#[test]
fn vault_dir_is_0700() {
let (dir, vault) = temp_vault();
vault.store("dir-test", b"x", false).unwrap();
let vault_dir = dir.path().join("vault");
let meta = std::fs::metadata(&vault_dir).unwrap();
let mode = meta.permissions().mode() & 0o777;
assert_eq!(mode, 0o700, "vault dir should be 0700, got {mode:04o}");
}
}