diff --git a/Cargo.lock b/Cargo.lock index dccc676..259049a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -697,6 +697,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "smallvec" version = "1.15.1" @@ -781,6 +787,7 @@ dependencies = [ "md5", "rsa", "sha1", + "simd-adler32", "tar", ] diff --git a/Cargo.toml b/Cargo.toml index b49622a..36d52f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ rsa = { version = "0.9", features = ["hazmat"] } hex = "0.4" ecb = "0.1" tar = "0.4.44" -binrw = "0.15" \ No newline at end of file +binrw = "0.15" +simd-adler32 = "*" \ No newline at end of file diff --git a/src/formats.rs b/src/formats.rs index 4d150b7..6d3394e 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -11,6 +11,7 @@ pub mod roku; pub mod sony_bdp; pub mod rvp; pub mod funai_upg; +pub mod pana_dvd; pub mod pup; diff --git a/src/formats/pana_dvd.rs b/src/formats/pana_dvd.rs new file mode 100644 index 0000000..2d1af24 --- /dev/null +++ b/src/formats/pana_dvd.rs @@ -0,0 +1,266 @@ +use std::fs::File; +use std::path::Path; +use std::fs::{self, OpenOptions}; +use std::io::{Write, Read, Cursor, Seek}; + +use simd_adler32::adler32; +use flate2::read::GzDecoder; +use binrw::{BinRead, BinReaderExt}; + +use crate::keys; +use crate::common; +use crate::utils::pana_dvd_crypto::{decrypt_data}; +use crate::utils::lzss::{decompress_lzss}; + +#[derive(BinRead)] +struct ModuleEntry { + #[br(count = 4)] name_bytes: Vec, + #[br(count = 4)] version_bytes: Vec, + _unk: u32, + offset: u32, + #[br(count = 8)] platform_bytes: Vec, + _unk1: u16, + #[br(count = 6)] id_bytes: Vec, + size: u32, + data_checksum: u32, + _unk2: u32, + _entry_checksum: u32, +} +impl ModuleEntry { + fn name(&self) -> String { + common::string_from_bytes(&self.name_bytes) + } + fn version(&self) -> String { + common::string_from_bytes(&self.version_bytes) + } + fn platform(&self) -> String { + common::string_from_bytes(&self.platform_bytes) + } + fn id(&self) -> String { + common::string_from_bytes(&self.id_bytes) + } + fn is_valid(&self) -> bool { + self.name().is_ascii() && self.platform().is_ascii() + } +} + +#[derive(BinRead)] +struct MainListHeader { + _checksum: u32, + _unk: u32, + list_size: u32, + _unk2: u64, +} +impl MainListHeader { + fn entry_count(&self) -> u32 { + (&self.list_size - 20) / 8 + } +} + +#[derive(BinRead)] +struct MainListEntry { + size: u32, + checksum: u32, +} + +#[derive(BinRead)] +struct MainEntryHeader { + #[br(count = 14)] _header_string: Vec, //EXTRHEADDRV + compression_type_byte: u16, + decompressed_size: u32, + _destination_address: u32, + compressed_size: u32, + _unk: u32, + _footer_offset: u32, + _base_address: u32, + _checksum: u32, + _checksum_flag: u8, + #[br(count = 19)] _unused: Vec, +} +impl MainEntryHeader { + fn compression_type(&self) -> &str { + if self.compression_type_byte == 0 { + return "Uncompressed" + } else if self.compression_type_byte == 1 { + return "GZIP + LZSS" + } else if self.compression_type_byte == 2 { + return "LZSS" + } else { + return "Unknown" + } + } +} + +pub fn find_key<'a>(key_array: &'a [&'a str], data: &[u8], expected_magic: &[u8]) -> Result, Box> { + for key_hex in key_array { + let key_bytes = hex::decode(key_hex)?; + let key_array: [u8; 8] = match key_bytes.as_slice().try_into() { + Ok(k) => k, + Err(_) => continue, + }; + let decrypted = decrypt_data(data, &key_array); + + if decrypted.starts_with(expected_magic) { + return Ok(Some(key_array)); + } + } + Ok(None) +} + +pub fn is_pana_dvd_file(mut file: &File) -> bool { + let header = common::read_file(&file, 0, 16).expect("Failed to read from file."); + if header.starts_with(b"PANASONIC\x00\x00\x00") { + file.seek(std::io::SeekFrom::Start(48)).expect("Failed to seek"); //skip rest of header + true + } else if find_key(&keys::PANA_DVD_KEYONLY, &header, b"PROG").expect("Failed to decrypt header.").is_some() { + true + } else { + false + } +} + +fn decompress_gzip(data: &[u8]) -> std::io::Result> { + let mut decoder = GzDecoder::new(data); + let mut decompressed = Vec::new(); + decoder.read_to_end(&mut decompressed)?; + Ok(decompressed) +} + +pub fn extract_pana_dvd(mut file: &File, output_folder: &str) -> Result<(), Box> { + let enc_header = common::read_exact(&mut file, 8192)?; + let matching_key; + let header; + // find the key, knowing that the first entry is always "PROG" + if let Some(key_array) = find_key(&keys::PANA_DVD_KEYONLY, &enc_header, b"PROG")? { + println!("Found valid key: {}\n", hex::encode_upper(key_array)); + matching_key = Some(key_array); + header = decrypt_data(&enc_header, &key_array); + } else { + println!("No valid key found!\n"); + return Ok(()); + } + + let matching_key_array = matching_key.as_ref().unwrap(); + + let mut hdr_reader = Cursor::new(header); + let mut modules: Vec = Vec::new(); + + for i in 0..100 { + let entry: ModuleEntry = hdr_reader.read_le()?; + if !entry.is_valid() {break}; + println!("Module {} - Name: {}, Version: {}, Platform: {}, ID: {}, Offset: {}, Size: {}", + i + 1, entry.name(), entry.version(), entry.platform(), entry.id(), entry.offset, entry.size); + if modules.iter().any(|m| m.name() == entry.name()){ + println!("- Duplicate module, skipping!"); + continue + } + modules.push(entry); + } + + let mut main_data: Option> = None; + for module in modules { + println!("\nSave module {}, Offset: {}, Size: {}, Expected checksum: {:#010x}", + module.name(), module.offset, module.size, module.data_checksum); + + let mut data = common::read_file(&file, module.offset as u64, module.size as usize)?; + let checksum = adler32(&data.as_slice()); + println!("Calculated checksum: {:#010x}", checksum); + if checksum == module.data_checksum { + println!("[OK] Checksum correct without decrypt"); + } else { + println!("Checksum incorrect, try with decrypt..."); + let dec_data = decrypt_data(&data, &matching_key_array); + let checksum = adler32(&dec_data.as_slice()); + println!("Calculated checksum: {:#010x}", checksum); + if checksum == module.data_checksum { + println!("[OK] Checksum correct with decrypt"); + data = dec_data; + } else { + println!("[NG] Checksum incorrect, unknown data") + } + } + + if module.name() == "MAIN" { + main_data = Some(data); + println!("MAIN found - will extract later..."); + continue + } + + let output_path = Path::new(&output_folder).join(format!("{}.bin", module.name())); + + fs::create_dir_all(&output_folder)?; + let mut out_file = OpenOptions::new().write(true).create(true).open(output_path)?; + out_file.write_all(&data)?; + + println!("- Saved file!"); + + } + + if !main_data.is_some() { + println!("\nMAIN data not found, Extraction finished!"); + return Ok(()) + } + println!("\nExtracting MAIN section..."); + + let main_data_unwrap = main_data.as_ref().unwrap(); + let mut main_reader = Cursor::new(main_data_unwrap); + + let main_list_hdr: MainListHeader = main_reader.read_le()?; + println!("MAIN entry count: {}", main_list_hdr.entry_count()); + + let mut main_entries: Vec = Vec::new(); + for i in 0..main_list_hdr.entry_count() { + let main_entry: MainListEntry = main_reader.read_le()?; + println!("- Entry {}/{} - Size: {}, Checksum: {:#010x}", + i + 1, main_list_hdr.entry_count(), main_entry.size, main_entry.checksum); + main_entries.push(main_entry); + } + + for entry in main_entries { + let mut data = common::read_exact(&mut main_reader, entry.size as usize)?; + //decrypt first and last 5kb + let first_decrypted = decrypt_data(&data[..5120], &matching_key_array); + data[..5120].copy_from_slice(&first_decrypted); + + let last_decrypted = decrypt_data(&data[entry.size as usize - 5120..], &matching_key_array); + data[entry.size as usize - 5120..].copy_from_slice(&last_decrypted); + // + + let mut data_reader = Cursor::new(data); + let header: MainEntryHeader = data_reader.read_le()?; + println!("\nSaving entry - Compressed size: {}, Decompressed size: {}, Compression type: {}({})", + header.compressed_size, header.decompressed_size, header.compression_type_byte, header.compression_type()); + let compressed_data = common::read_exact(&mut data_reader, header.compressed_size as usize)?; + + let decompressed_data; + if header.compression_type_byte == 1 { //gzip + lzss + println!("- (1/2) Decompressing GZIP..."); + let decompressed_gzip = decompress_gzip(&compressed_data)?; + // the decompressed data has another header + let mut decompressed_gzip_reader = Cursor::new(decompressed_gzip); + let header: MainEntryHeader = decompressed_gzip_reader.read_le()?; + println!("- (2/2) Decompressing LZSS... (Compressed size: {}, Decompressed size: {})", header.compressed_size, header.decompressed_size); + let compressed_lzss = common::read_exact(&mut decompressed_gzip_reader, header.compressed_size as usize)?; + decompressed_data = decompress_lzss(&compressed_lzss); + } else if header.compression_type_byte == 2 { //only lzss + println!("- Decompressing LZSS..."); + decompressed_data = decompress_lzss(&compressed_data); + } else if header.compression_type_byte == 0 { //no compression. havent encountered one yet + decompressed_data = compressed_data; + } else { + println!("- Unknown compression method!"); + decompressed_data = compressed_data; + } + + let output_path = Path::new(&output_folder).join("MAIN.bin"); + + fs::create_dir_all(&output_folder)?; + let mut out_file = OpenOptions::new().append(true).create(true).open(output_path)?; + out_file.write_all(&decompressed_data)?; + + println!("- Saved to MAIN!"); + } + + println!("\nExtraction finished!"); + Ok(()) +} \ No newline at end of file diff --git a/src/keys.rs b/src/keys.rs index 4b553ce..7f5e502 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -74,4 +74,9 @@ pub static EPK2: &[(&str, &str)] = &[ pub static EPK3: &[(&str, &str)] = &[ ("34CC219D3AFC102433109BBC1DA44095", "m14 (m14tv) - LX webOS 1 (2014)"), ("52A208FA24E7E70730A40999B1C22C148F4920484BC50B515D243E35D14689F1", "o24n - LX webOS 10 (2025)") +]; + +//pana dvd keys (no AES) +pub static PANA_DVD_KEYONLY: &[&str] = &[ + ("08E03D859AF9F3EE"), //B3A25A0B9D864F08 # 87 devices, ..-~2011 ]; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 546b85b..b292c4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,6 +103,10 @@ fn main() -> Result<(), Box> { println!("PFL UPG file detected!"); formats::pfl_upg::extract_pfl_upg(&file, &output_path)?; } + else if formats::pana_dvd::is_pana_dvd_file(&file) { + println!("PANA_DVD file detected!"); + formats::pana_dvd::extract_pana_dvd(&file, &output_path)?; + } else if formats::pup::is_pup_file(&file) { println!("PUP file detected!"); formats::pup::extract_pup(&file, &output_path)?; diff --git a/src/utils.rs b/src/utils.rs index a8cfff6..856f2c6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1 +1,3 @@ -pub mod aes; \ No newline at end of file +pub mod aes; +pub mod pana_dvd_crypto; +pub mod lzss; \ No newline at end of file diff --git a/src/utils/lzss.rs b/src/utils/lzss.rs new file mode 100644 index 0000000..cef521d --- /dev/null +++ b/src/utils/lzss.rs @@ -0,0 +1,51 @@ +pub fn decompress_lzss(data: &[u8]) -> Vec { + let mut window = [0u8; 0x1000]; + let mut dst = Vec::new(); + let mut src_i = 0; + let mut win_pos = 0xFEE; + let mut flags = 0u16; + + while src_i < data.len() { + flags >>= 1; + if (flags & 0x100) == 0 { + if src_i >= data.len() { + break; + } + flags = data[src_i] as u16 | 0xFF00; + src_i += 1; + } + + if (flags & 1) == 0 { + // Backreference + if src_i + 1 >= data.len() { + break; + } + let b1 = data[src_i]; + let b2 = data[src_i + 1]; + src_i += 2; + + let mut offset = (b1 as usize) | (((b2 & 0xF0) as usize) << 4); + let length = ((b2 & 0x0F) as usize) + 3; + + for _ in 0..length { + let c = window[offset]; + dst.push(c); + window[win_pos] = c; + win_pos = (win_pos + 1) & 0xFFF; + offset = (offset + 1) & 0xFFF; + } + } else { + // Literal + if src_i >= data.len() { + break; + } + let c = data[src_i]; + src_i += 1; + dst.push(c); + window[win_pos] = c; + win_pos = (win_pos + 1) & 0xFFF; + } + } + + dst +} \ No newline at end of file diff --git a/src/utils/pana_dvd_crypto.rs b/src/utils/pana_dvd_crypto.rs new file mode 100644 index 0000000..3f653c6 --- /dev/null +++ b/src/utils/pana_dvd_crypto.rs @@ -0,0 +1,167 @@ +//libfmupre.so +//DAT_29f06d60 +const SBOX: [u8; 256] = [ + 0x00,0x01,0xbb,0x9c,0x3f,0x5e,0xc7,0x3e,0x36,0xa4,0x92,0x93,0x38,0x9b,0x8d,0x1a, + 0x3c,0x84,0xf7,0x2a,0xcb,0x3d,0x70,0xad,0x30,0xa2,0xc1,0x95,0x03,0x39,0xa5,0x0e, + 0xe6,0xfb,0x09,0x1e,0xb9,0xc4,0x24,0x57,0x5f,0xc3,0x5d,0xfa,0x60,0x56,0xfd,0xed, + 0x81,0xf3,0x4a,0xff,0x85,0xdb,0xcd,0xa0,0xd0,0x9d,0x8b,0x26,0x4c,0x50,0x0c,0x1d, + 0x7d,0x77,0xde,0x7a,0x0a,0x0b,0x73,0xe1,0x54,0x76,0xe8,0xc2,0x28,0x05,0x98,0x99, + 0x29,0x4b,0xee,0x87,0x42,0x59,0x65,0xb8,0x1f,0xdf,0x23,0x58,0x63,0x69,0x1c,0x79, + 0x64,0xc5,0x6f,0x13,0x3b,0xce,0x08,0xa7,0xb2,0x12,0x20,0x7f,0xe2,0x89,0x21,0x2b, + 0x41,0x49,0x7c,0x82,0xbe,0xf2,0x43,0x8e,0x86,0x02,0x9e,0xd8,0x67,0x8f,0xa3,0xe7, + 0xbc,0xf8,0x66,0x22,0x4d,0xe5,0xba,0xd2,0xda,0xea,0x61,0x25,0xb0,0xc0,0x7b,0xa6, + 0x48,0x96,0xdd,0xdc,0x71,0x90,0x55,0x5b,0x4f,0x4e,0x6d,0xd1,0x11,0x34,0xaa,0x1b, + 0xf4,0x17,0x80,0xb7,0xcc,0x8c,0xd9,0x37,0x8a,0x2c,0x94,0x16,0x72,0x40,0xef,0x9a, + 0xc8,0x47,0xf6,0xfc,0x2e,0xb3,0x2f,0xbd,0xcf,0xa9,0x15,0x10,0x18,0xec,0x6a,0x6e, + 0xc9,0x91,0x53,0x78,0xa8,0x3a,0xaf,0x0d,0xe0,0x97,0x32,0x51,0xb1,0xf5,0x27,0x2d, + 0x35,0x88,0x14,0xe3,0xfe,0x06,0xd7,0xd6,0xab,0xa1,0xd5,0xe4,0x45,0x44,0x9f,0x0f, + 0x5a,0x83,0xeb,0xca,0x07,0x04,0xb4,0xb6,0x52,0xae,0xd4,0x33,0x31,0x75,0xd3,0x6c, + 0x62,0xbf,0x6b,0xf9,0xac,0x46,0xf0,0x5c,0x19,0x7e,0x68,0xb5,0xf1,0x74,0xc6,0xe9, +]; + +//FUN_29f05520 +fn round_function(state: &mut [u8; 4], a5: &[u8; 4]) { + let v5 = a5[1] ^ state[1]; + let v6 = a5[2] ^ state[2]; + let v7 = state[3] ^ a5[3]; + + let v8 = ((v5 >> 7) | (v5 << 1)) & 0xFF; + let v9 = ((v6 >> 5) | (v6 << 3)) & 0xFF; + let v10 = a5[0] ^ state[0]; + let v11 = ((v7 >> 2) | (v7 << 6)) & 0xFF; + let v12 = v10 ^ v9 ^ v8; + + let temp_v7 = v10 >> 1; + let temp_v10 = ((v10 << 7) | (v11 >> 1)) & 0xFF; + + let new_a4 = ((v9 >> 1) | (v11 << 7)) & 0xFF; + let v13 = SBOX[(v11 ^ v12) as usize]; + let new_a3 = ((v8 >> 1) | (v9 << 7)) & 0xFF; + let new_a2 = (temp_v7 | (v8 << 7)) & 0xFF; + let new_result = temp_v10 & 0xFF; + + let a4_final = new_a4.wrapping_add(v13); + let a3_final = new_a3.wrapping_add(((v13 >> 7) | (v13 << 1)) & 0xFF); + let a2_final = new_a2.wrapping_add(((v13 >> 6) | (v13 << 2)) & 0xFF); + let result_final = new_result.wrapping_add(((v13 >> 5) | (v13 << 3)) & 0xFF); + + state[0] = result_final; + state[1] = a2_final; + state[2] = a3_final; + state[3] = a4_final; +} + +//FUN_29f055dc +fn key_schedule(xored_key: &[u8; 8], out: &mut [u8; 32]) { + //in original function 2 keys were XORed together to make the actual key (that the round keys are derived from) + // why? i dont know (obfuscation?) but i use the XORed key here directly + let mut v26 = [0u8; 8]; + for i in 0..7 { + v26[i] = xored_key[i]; + } + let v12 = xored_key[7]; + v26[1] ^= v12; + v26[4] ^= v12; + + for i in 0..7 { + out[i] = v26[i]; + } + for i in 7..32 { + out[i] = 0; + } + + let mut v22: [u8; 4] = [53, 125, 251, 154]; + + for v16 in 0..16 { + let v17 = 4 * (v16 & 7); + let mut state: [u8; 4] = out[v17..v17 + 4].try_into().unwrap(); + round_function(&mut state, &v22); + out[v17..v17 + 4].copy_from_slice(&state); + v22.copy_from_slice(&state); + } +} + +//FUN_29f056b0 +fn decrypt_block(a1: &[u8; 32], a2: &mut [u8; 8]) { + let mut v6 = a2[0]; + let mut v7 = a2[1]; + let mut v5 = a2[2]; + let mut v4 = a2[3]; + + let mut v24 = v6; + let mut v25 = v7; + let mut v26 = v5; + let mut v27 = v4; + + let mut v10 = a2[4]; + let mut v11 = a2[5]; + let mut v12 = a2[6]; + let mut v13 = a2[7]; + + let mut v15: u8 = 0; + let mut v16: u8 = 0; + let mut v17: u8 = 0; + let mut v18: u8 = 0; + + for v2 in (0..16).rev() { + let v14 = (v2 & 7) as usize; + let subkey: [u8; 4] = a1[4 * v14..4 * v14 + 4].try_into().unwrap(); + let mut st = [v24, v25, v26, v27]; + round_function(&mut st, &subkey); + v24 = st[0]; + v25 = st[1]; + v26 = st[2]; + v27 = st[3]; + + v15 = v10 ^ v24; + v16 = v11 ^ v25; + v17 = v12 ^ v26; + v18 = v13 ^ v27; + + v24 ^= v10; + v25 ^= v11; + v26 ^= v12; + v27 ^= v13; + + v10 = v6; + v11 = v7; + v12 = v5; + v13 = v4; + + if v2 == 0 { + break; + } + + v6 = v15; + v7 = v16; + v5 = v17; + v4 = v18; + } + + a2[0] = v6; + a2[1] = v7; + a2[2] = v5; + a2[3] = v4; + a2[4] = v15; + a2[5] = v16; + a2[6] = v17; + a2[7] = v18; +} + +fn decrypt_data_inplace(data: &mut [u8], key: &[u8; 8]) { + let mut subkeys = [0u8; 32]; + key_schedule(key, &mut subkeys); + for chunk in data.chunks_mut(8) { + if chunk.len() == 8 { + let mut block: [u8; 8] = chunk.try_into().unwrap(); + decrypt_block(&subkeys, &mut block); + chunk.copy_from_slice(&block); + } + } +} + +pub fn decrypt_data(data: &[u8], key: &[u8; 8]) -> Vec { + let mut buf = data.to_vec(); + decrypt_data_inplace(&mut buf, key); + buf +} \ No newline at end of file diff --git a/unixtract.md b/unixtract.md index a309f10..477064e 100644 --- a/unixtract.md +++ b/unixtract.md @@ -12,6 +12,7 @@ | Mediatek PKG | Many Mediatek-based TVs (Hisense, Sony, Panasonic, Philips...) | Only files with 144 byte header and using 4xVendor magic as enc key are supported | https://github.com/openlgtv/epk2extract | | Mediatek upgrade_loader | Older Mediatek-based TVs | All files should be supported providing they are not encrypted | - | | Novatek pkg | Some Novatek-based TVs (Philips, LG..) | All files should be supported | https://github.com/openlgtv/epk2extract | +| Panasonic Blu-Ray (PANA_DVD) | Panasonic Blu-Ray Players/Recorders | Only some files are supported (**depends on keys**) | - | | Philips UPG | Philips pre-TPVision TVS 2008-2013 | Only some files are supported (**depends on keys**) | https://github.com/frederic/pflupg-tool | | PUP | Sony PlayStation 4/ PlayStation 5 | File has to be decrypted | https://github.com/Zer0xFF/ps4-pup-unpacker | | RUF | Samsung Broadcom-based Blu-Ray players | Only BD-P1600 is supported (**depends on keys**) | - |