ewf-image 0.6.1

Pure-Rust library for reading and writing EWF/E01 forensic disk images.
Documentation
//! X-Ways encrypted EWF1 integration tests.

use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};

use ewf_image::{
    EncryptionMethod, EwfError, EwfPassword, EwfWriter, Image, OpenOptions, WriteFormat,
    WriteOptions,
};
use sha2::{Digest, Sha256};

fn fixture(name: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/data/xways-encrypted")
        .join(name)
}

fn without_password_verifier(name: &str) -> tempfile::NamedTempFile {
    let mut data = std::fs::read(fixture(name)).unwrap();
    let descriptor = data
        .windows(b"x_encryption".len())
        .position(|window| window == b"x_encryption")
        .expect("X-Ways encryption section");
    data[descriptor + 78..descriptor + 80].copy_from_slice(&0x1002_u16.to_le_bytes());
    let file = tempfile::NamedTempFile::new().unwrap();
    std::fs::write(file.path(), data).unwrap();
    file
}

#[test]
fn encrypted_open_requires_a_password() {
    for name in [
        "aes128-compatible.E01",
        "aes128-zstd.E01",
        "aes256-compatible.E01",
        "aes256-zstd.E01",
    ] {
        let error = Image::open(fixture(name)).unwrap_err();
        assert!(
            matches!(error, EwfError::PasswordRequired),
            "fixture {name} returned {error:?}"
        );
    }
}

#[test]
fn encrypted_open_rejects_an_incorrect_password() {
    let password = EwfPassword::utf8("definitely-wrong");
    for name in [
        "aes128-compatible.E01",
        "aes128-zstd.E01",
        "aes256-compatible.E01",
        "aes256-zstd.E01",
    ] {
        let error = Image::open_with_password(fixture(name), &password).unwrap_err();
        assert!(
            matches!(error, EwfError::PasswordRejected),
            "fixture {name} returned {error:?}"
        );
    }
}

#[test]
fn opens_and_reads_known_password_xways_images() {
    let password = EwfPassword::utf8("xways-test");
    for (name, expected_method) in [
        (
            "aes128-known-password.E01",
            EncryptionMethod::XWaysAes128Ctr,
        ),
        (
            "aes256-known-password.E01",
            EncryptionMethod::XWaysAes256Ctr,
        ),
    ] {
        let image = Image::open_with_password(fixture(name), &password).unwrap();
        let info = image.encryption_info().expect("encrypted image info");
        assert_eq!(info.method(), expected_method);
        assert!(info.password_verifier_present());

        let mut data = Vec::new();
        image.cursor().read_to_end(&mut data).unwrap();
        assert_eq!(data.len(), 1_048_576);
        assert_eq!(
            Sha256::digest(&data)[..],
            [
                0xa4, 0xa3, 0xec, 0x30, 0xd6, 0x38, 0x82, 0x44, 0xde, 0xd0, 0x6c, 0x36, 0xc1, 0xc7,
                0xf5, 0x01, 0xee, 0x42, 0xfd, 0x79, 0x40, 0xc0, 0x43, 0x50, 0xc6, 0xe3, 0x67, 0xa6,
                0x44, 0x22, 0x03, 0x13,
            ]
        );

        let offsets = [0_usize, 1, 15, 16, 17, 32_767, 32_768, data.len() - 31];
        for offset in offsets.into_iter().chain(offsets.into_iter().rev()) {
            let expected = &data[offset..data.len().min(offset + 73)];
            let mut actual = vec![0_u8; expected.len()];
            assert_eq!(
                image
                    .read_at(&mut actual, u64::try_from(offset).unwrap())
                    .unwrap(),
                expected.len(),
                "fixture {name}, offset {offset}"
            );
            assert_eq!(actual, expected, "fixture {name}, offset {offset}");
        }
    }
}

#[test]
fn supplied_reader_opens_and_reads_a_known_password_xways_image() {
    let bytes = std::fs::read(fixture("aes256-known-password.E01")).unwrap();
    let password = EwfPassword::utf8("xways-test");
    let image = Image::open_readers_with_options_and_password(
        [("fixture.E01", Cursor::new(bytes))],
        OpenOptions::default(),
        &password,
    )
    .unwrap();

    let mut data = Vec::new();
    image.cursor().read_to_end(&mut data).unwrap();
    assert_eq!(data.len(), 1_048_576);
    assert_eq!(
        Sha256::digest(data)[..],
        [
            0xa4, 0xa3, 0xec, 0x30, 0xd6, 0x38, 0x82, 0x44, 0xde, 0xd0, 0x6c, 0x36, 0xc1, 0xc7,
            0xf5, 0x01, 0xee, 0x42, 0xfd, 0x79, 0x40, 0xc0, 0x43, 0x50, 0xc6, 0xe3, 0x67, 0xa6,
            0x44, 0x22, 0x03, 0x13,
        ]
    );
}

#[test]
fn verifier_less_image_uses_decrypted_structure_validation() {
    let file = without_password_verifier("aes256-known-password.E01");
    let password = EwfPassword::utf8("xways-test");
    let image = Image::open_segments_with_options_and_password(
        [file.path()],
        OpenOptions::default(),
        &password,
    )
    .unwrap();
    assert!(
        !image
            .encryption_info()
            .expect("encrypted image info")
            .password_verifier_present()
    );
    image.read_data_chunk(0).unwrap();

    let wrong_password = EwfPassword::utf8("definitely-wrong");
    let error = Image::open_segments_with_options_and_password(
        [file.path()],
        OpenOptions::default(),
        &wrong_password,
    )
    .unwrap_err();
    assert!(matches!(error, EwfError::DecryptionValidationFailed));
}

#[test]
fn authentic_xways_fixtures_read_with_operator_supplied_password() {
    let Ok(password) = std::env::var("EWF_IMAGE_XWAYS_TEST_PASSWORD") else {
        return;
    };
    let password = EwfPassword::utf8(&password);
    for (name, expected_method) in [
        ("aes128-compatible.E01", EncryptionMethod::XWaysAes128Ctr),
        ("aes128-zstd.E01", EncryptionMethod::XWaysAes128Ctr),
        ("aes256-compatible.E01", EncryptionMethod::XWaysAes256Ctr),
        ("aes256-zstd.E01", EncryptionMethod::XWaysAes256Ctr),
    ] {
        let image = Image::open_with_password(fixture(name), &password).unwrap();
        assert_eq!(
            image
                .encryption_info()
                .expect("encrypted image info")
                .method(),
            expected_method
        );
        let mut data = Vec::new();
        image.cursor().read_to_end(&mut data).unwrap();
        assert_eq!(
            Sha256::digest(data)[..],
            [
                0xa4, 0xa3, 0xec, 0x30, 0xd6, 0x38, 0x82, 0x44, 0xde, 0xd0, 0x6c, 0x36, 0xc1, 0xc7,
                0xf5, 0x01, 0xee, 0x42, 0xfd, 0x79, 0x40, 0xc0, 0x43, 0x50, 0xc6, 0xe3, 0x67, 0xa6,
                0x44, 0x22, 0x03, 0x13,
            ],
            "fixture {name}"
        );
    }
}

#[test]
fn password_open_methods_preserve_plain_image_behavior() {
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("plain.E01");
    let options = WriteOptions {
        format: WriteFormat::Ewf1Physical,
        ..WriteOptions::default()
    };
    let mut writer = EwfWriter::create(&path, options).unwrap();
    writer.write_all(b"plain image data").unwrap();
    writer.finish().unwrap();
    let password = EwfPassword::utf8("unused-public-test-password");

    let image = Image::open_with_password(&path, &password).unwrap();
    assert!(image.encryption_info().is_none());

    let image =
        Image::open_with_options_and_password(&path, OpenOptions::default(), &password).unwrap();
    assert!(image.encryption_info().is_none());

    let image =
        Image::open_segments_with_options_and_password([&path], OpenOptions::default(), &password)
            .unwrap();
    assert!(image.encryption_info().is_none());

    let bytes = std::fs::read(&path).unwrap();
    let image = Image::open_readers_with_options_and_password(
        [("plain.E01", Cursor::new(bytes))],
        OpenOptions::default(),
        &password,
    )
    .unwrap();
    assert!(image.encryption_info().is_none());
}