shadow-crypt 3.0.0

Secure password-based file encryption with filename obfuscation
Documentation
mod common;

use std::fs;
use std::path::{Path, PathBuf};

use common::{TEST_PASSWORD, decrypt_files, encrypt_files, find_single_shadow_file};
use tempfile::TempDir;

fn fixture_path(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests")
        .join("fixtures")
        .join(name)
}

/// Decrypts a committed fixture file and checks filename and content.
/// Guards against changes that would break decryption of existing files.
fn assert_fixture_decrypts(fixture_name: &str, expected_filename: &str, expected_content: &[u8]) {
    let temp_dir = TempDir::new().unwrap();
    let fixture = fixture_path(fixture_name);
    assert!(fixture.exists(), "Fixture not found at {:?}", fixture);

    let temp_shadow_file = temp_dir.path().join(fixture_name);
    fs::copy(&fixture, &temp_shadow_file).unwrap();

    decrypt_files(&[&temp_shadow_file], TEST_PASSWORD, temp_dir.path()).unwrap();

    let decrypted_file = temp_dir.path().join(expected_filename);
    assert!(decrypted_file.exists(), "Decrypted file was not created");

    let decrypted_content = fs::read(&decrypted_file).unwrap();
    assert_eq!(
        decrypted_content, expected_content,
        "Decrypted content does not match expected content"
    );
}

#[test]
fn test_v1_backward_compatibility() {
    assert_fixture_decrypts(
        "test_v1.shadow",
        "test_v1_plaintext.txt",
        b"This is a test file for backward compatibility testing. It contains known content that should be decrypted correctly.\n",
    );
}

#[test]
fn test_v2_backward_compatibility() {
    assert_fixture_decrypts(
        "test_v2.shadow",
        "test_v2_plaintext.txt",
        b"This is a test file for v2 backward compatibility testing. It contains known content that should be decrypted correctly.\n",
    );
}

#[test]
fn test_v3_backward_compatibility() {
    assert_fixture_decrypts(
        "test_v3.shadow",
        "test_v3_plaintext.txt",
        b"This is a test file for v3 backward compatibility testing. It contains known content that should be decrypted correctly.\n",
    );
}

/// Regenerates the v2 fixture through the v2 core API (the workflow writes
/// the current format, not v2). Run manually after an intentional format
/// change: `cargo test regenerate_v2_fixture -- --ignored`
#[test]
#[ignore]
fn regenerate_v2_fixture() {
    use shadow_crypt::core::{
        file::PlaintextFile,
        memory::{SecureBytes, SecureString},
        profile::SecurityProfile,
        v2::{file::EncryptedFile, key::KeyDerivationParams},
    };

    let params = KeyDerivationParams::from(SecurityProfile::Test);
    let salt = [11u8; 16];
    let (key, _) = params.derive_key(TEST_PASSWORD.as_bytes(), &salt).unwrap();

    let plaintext = PlaintextFile::new(
        SecureString::new("test_v2_plaintext.txt".to_string()),
        SecureBytes::new(
            b"This is a test file for v2 backward compatibility testing. It contains known content that should be decrypted correctly.\n".to_vec(),
        ),
    );
    let sealed =
        EncryptedFile::seal(&plaintext, &key, params, salt, [12u8; 24], [13u8; 24]).unwrap();

    fs::write(fixture_path("test_v2.shadow"), sealed.to_bytes()).unwrap();
}

/// Regenerates the v3 fixture through the regular encryption workflow (v3 is
/// the current write format). Run manually after an intentional format
/// change: `cargo test regenerate_v3_fixture -- --ignored`
#[test]
#[ignore]
fn regenerate_v3_fixture() {
    let temp_dir = TempDir::new().unwrap();
    let input_file = temp_dir.path().join("test_v3_plaintext.txt");
    fs::write(
        &input_file,
        b"This is a test file for v3 backward compatibility testing. It contains known content that should be decrypted correctly.\n",
    )
    .unwrap();

    encrypt_files(&[&input_file], TEST_PASSWORD, temp_dir.path()).unwrap();
    let encrypted = find_single_shadow_file(temp_dir.path());

    fs::copy(&encrypted, fixture_path("test_v3.shadow")).unwrap();
}