feat: add AES-128-CBC decryption and clap CLI (M4)
Add session key derivation and payload decryption using AES-128-CBC with well-known XEX2 master keys. Refactor CLI to use clap with inspect/extract subcommands. Extend FileFormatInfo to parse compression metadata (basic blocks, LZX window size/block chain). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use xex2tractor::crypto;
|
||||
use xex2tractor::header::{ModuleFlags, XEX2_MAGIC};
|
||||
use xex2tractor::optional::{CompressionType, EncryptionType, SystemFlags};
|
||||
use xex2tractor::optional::{CompressionInfo, CompressionType, EncryptionType, SystemFlags};
|
||||
use xex2tractor::security::{ImageFlags, MediaFlags, RegionFlags};
|
||||
|
||||
fn sample_data() -> Vec<u8> {
|
||||
@@ -185,17 +186,74 @@ fn test_security_crypto_fields() {
|
||||
assert_eq!(sec.aes_key[0..2], [0xEA, 0xCB]);
|
||||
}
|
||||
|
||||
// ── Compression info tests ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_compression_info_normal() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
let fmt = xex.optional_headers.file_format_info.as_ref().unwrap();
|
||||
|
||||
match &fmt.compression_info {
|
||||
CompressionInfo::Normal {
|
||||
window_size,
|
||||
first_block,
|
||||
} => {
|
||||
// Window size should be a power of 2
|
||||
assert!(window_size.is_power_of_two(), "window_size should be power of 2");
|
||||
assert!(*window_size > 0);
|
||||
// First block should have non-zero size
|
||||
assert!(first_block.block_size > 0);
|
||||
// Block hash should not be all zeros
|
||||
assert!(!first_block.block_hash.iter().all(|&b| b == 0));
|
||||
}
|
||||
other => panic!("expected Normal compression info, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Crypto tests ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_session_key_derivation() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
|
||||
let session_key = crypto::derive_session_key(&xex.security_info.aes_key);
|
||||
// Session key should be non-zero
|
||||
assert!(!session_key.iter().all(|&b| b == 0));
|
||||
// Session key should differ from encrypted key
|
||||
assert_ne!(&session_key[..], &xex.security_info.aes_key[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_payload_decryption() {
|
||||
let data = sample_data();
|
||||
let xex = xex2tractor::parse(&data).unwrap();
|
||||
|
||||
let session_key = crypto::derive_session_key(&xex.security_info.aes_key);
|
||||
|
||||
// Decrypt the first 256 bytes of payload
|
||||
let payload_start = xex.header.header_size as usize;
|
||||
let mut payload_head = data[payload_start..payload_start + 256].to_vec();
|
||||
let original = payload_head.clone();
|
||||
|
||||
crypto::decrypt_in_place(&session_key, &mut payload_head);
|
||||
|
||||
// Decrypted data should differ from encrypted
|
||||
assert_ne!(payload_head, original);
|
||||
}
|
||||
|
||||
// ── CLI tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn test_cli_runs_with_sample() {
|
||||
fn test_cli_inspect_with_sample() {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
||||
.arg(&path)
|
||||
.args(["inspect", &path])
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
assert!(output.status.success(), "CLI should exit successfully");
|
||||
assert!(output.status.success(), "CLI inspect should exit successfully");
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Header section
|
||||
@@ -213,6 +271,8 @@ fn test_cli_runs_with_sample() {
|
||||
assert!(stdout.contains("FILE_FORMAT_INFO"));
|
||||
assert!(stdout.contains("Normal (AES-128-CBC)"));
|
||||
assert!(stdout.contains("Normal (LZX)"));
|
||||
assert!(stdout.contains("Window Size:")); // new compression info
|
||||
assert!(stdout.contains("First Block:")); // new compression info
|
||||
assert!(stdout.contains("STATIC_LIBRARIES"));
|
||||
assert!(stdout.contains("XAPILIB"));
|
||||
assert!(stdout.contains("IMPORT_LIBRARIES"));
|
||||
@@ -242,11 +302,25 @@ fn test_cli_no_args() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_missing_file() {
|
||||
fn test_cli_inspect_missing_file() {
|
||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
||||
.arg("/nonexistent/file.xex")
|
||||
.args(["inspect", "/nonexistent/file.xex"])
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
assert!(!output.status.success());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_extract_not_yet_implemented() {
|
||||
let path = format!("{}/tests/data/default.xex", env!("CARGO_MANIFEST_DIR"));
|
||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_xex2tractor"))
|
||||
.args(["extract", &path])
|
||||
.output()
|
||||
.expect("failed to run xex2tractor");
|
||||
|
||||
// Extract should fail with "not yet implemented" for now
|
||||
assert!(!output.status.success());
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("not yet implemented"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user