From 6ac1f3248805eec73e4b64511c781181842c887b Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Sun, 19 Oct 2025 14:56:51 +0200 Subject: [PATCH] Add RUF extractor + improve epk3 --- src/formats.rs | 1 + src/formats/epk3.rs | 112 +++++++++++++------------- src/formats/ruf.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++ src/keys.rs | 25 +++--- src/main.rs | 4 + 5 files changed, 263 insertions(+), 65 deletions(-) create mode 100644 src/formats/ruf.rs diff --git a/src/formats.rs b/src/formats.rs index 390a62c..42b04da 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -4,6 +4,7 @@ pub mod tpv_timg; pub mod pfl_upg; pub mod sddl_sec; pub mod novatek; +pub mod ruf; pub mod msd; pub mod msd10; diff --git a/src/formats/epk3.rs b/src/formats/epk3.rs index f0edbf2..8548f34 100644 --- a/src/formats/epk3.rs +++ b/src/formats/epk3.rs @@ -1,11 +1,55 @@ use std::fs::{self, File, OpenOptions}; use std::path::{Path}; use std::io::{Write, Seek, SeekFrom, Cursor}; +use binrw::{BinRead, BinReaderExt}; use crate::common; use crate::keys; use crate::formats::epk::{decrypt_aes_ecb_auto, find_key}; +#[derive(BinRead)] +struct Header { + #[br(count = 4)] _magic_bytes: Vec, + #[br(count = 4)] version: Vec, + #[br(count = 32)] ota_id_bytes: Vec, + package_info_size: u32, +} +impl Header { + fn ota_id(&self) -> String { + common::string_from_bytes(&self.ota_id_bytes) + } +} + +#[derive(BinRead)] +struct PkgInfoHeader { + package_info_list_size: u32, + package_info_count: u32, +} + +#[derive(BinRead)] +struct PkgInfoEntry { + _package_type: u32, + _package_info_size: u32, + #[br(count = 128)] package_name_bytes: Vec, + #[br(count = 96)] _package_version_bytes: Vec, + #[br(count = 32)] _package_architecture_bytes: Vec, + #[br(count = 32)] _checksum: Vec, + package_size: u32, + _dipk: u32, + //segment info + _is_segmented: u32, + segment_index: u32, + segment_count: u32, + segment_size: u32, + // + _unk: u32, +} +impl PkgInfoEntry { + fn package_name(&self) -> String { + common::string_from_bytes(&self.package_name_bytes) + } +} + pub fn extract_epk3(mut file: &File, output_folder: &str) -> Result<(), Box> { file.seek(SeekFrom::Start(128))?; //inital signature @@ -42,86 +86,45 @@ pub fn extract_epk3(mut file: &File, output_folder: &str) -> Result<(), Box 0 { - let segment = common::read_exact(&mut pkg_info_reader, 324)?; - - let segment_index_b = &segment[308..312]; - segment_index = u32::from_le_bytes(segment_index_b.try_into().unwrap()); - - let segment_size_b = &segment[316..320]; - segment_size = u32::from_le_bytes(segment_size_b.try_into().unwrap()); + entry = pkg_info_reader.read_le()?; } - println!("- Segment {}/{}, Size: {}", segment_index + 1, segment_count, segment_size); + println!("- Segment {}/{}, Size: {}", entry.segment_index + 1, entry.segment_count, entry.segment_size); let _signature = common::read_exact(&mut file, signature_size)?; - let encrypted_data = common::read_exact(&mut file, segment_size as usize + extra_segment_size)?; + let encrypted_data = common::read_exact(&mut file, entry.segment_size as usize + extra_segment_size)?; let out_data = decrypt_aes_ecb_auto(matching_key_bytes, &encrypted_data)?; - let output_path = Path::new(&output_folder).join(package_name.clone() + ".bin"); + let output_path = Path::new(&output_folder).join(entry.package_name().clone() + ".bin"); fs::create_dir_all(&output_folder)?; let mut out_file = OpenOptions::new() @@ -136,6 +139,5 @@ pub fn extract_epk3(mut file: &File, output_folder: &str) -> Result<(), Box; + +use crate::common; +use crate::keys; + +#[derive(BinRead)] +struct RufHeader { + #[br(count = 6)] _magic_bytes: Vec, + #[br(count = 2)] _upgrade_type_bytes: Vec, + _unk1: u32, + #[br(count = 32)] date_time_bytes: Vec, + #[br(count = 8)] buyer_bytes: Vec, + #[br(count = 32)] model_bytes: Vec, + #[br(count = 32)] region_info_bytes: Vec, + #[br(count = 4)] version_bytes: Vec, + data_size: u32, + #[br(count = 20)] _unk2: Vec, + dual_ruf_flag: u32, + #[br(count = 44)] _unk3: Vec, + payload_count: u16, + _payload_entry_size: u16, + payloads_start_offset: u32, +} +impl RufHeader { + fn date_time(&self) -> String { + common::string_from_bytes(&self.date_time_bytes) + } + fn buyer(&self) -> String { + common::string_from_bytes(&self.buyer_bytes) + } + fn model(&self) -> String { + common::string_from_bytes(&self.model_bytes) + } + fn region_info(&self) -> String { + common::string_from_bytes(&self.region_info_bytes) + } + fn is_dual_ruf(&self) -> bool { + if self.dual_ruf_flag == 0x44 {true} else {false} + } +} + +#[derive(BinRead)] +struct RufEntry { + #[br(count = 32)] _metadata: Vec, + payload_type_bytes: u32, + size: u32, + _unk1: u32, + #[br(count = 20)] _unk2: Vec, +} +impl RufEntry { + fn payload_type(&self) -> &str { + if self.payload_type_bytes == 1 { + return "squashfs" + } else if self.payload_type_bytes == 2 { + return "cfe" + } else if self.payload_type_bytes == 3 { + return "vmlinuz" + } else if self.payload_type_bytes == 4 { + return "loader" + } else if self.payload_type_bytes == 5 { + return "splash" + } else { + return "unknown" + } + } +} + +pub fn is_ruf_file(file: &File) -> bool { + let header = common::read_file(&file, 0, 3).expect("Failed to read from file."); + if header == b"RUF" { + true + } else { + false + } +} + +fn decrypt(encrypted_data: &[u8], key: &[u8; 16], iv: &[u8; 16]) -> Result, Box> { + let mut data = encrypted_data.to_vec(); + let decryptor = Aes128CbcDec::new(key.into(), iv.into()); + let decrypted = decryptor.decrypt_padded_mut::(&mut data) + .map_err(|e| format!("!!Decryption error!!: {:?}", e))?; + + Ok(decrypted.to_vec()) +} + +pub fn extract_ruf(mut file: &File, output_folder: &str) -> Result<(), Box> { + let header: RufHeader = file.read_be()?; + if header.is_dual_ruf() { + println!("\nDual RUF detected! Extracting 1st RUF...\n"); + actually_extract_ruf(&file, &format!("{}/RUF_1", output_folder), 0)?; + println!("\nExtracting 2nd RUF...\n"); + actually_extract_ruf(&file, &format!("{}/RUF_2", output_folder), 41943088)?; + } else { + actually_extract_ruf(&file, &output_folder, 0)?; + } + + println!("\nExtraction finished!"); + Ok(()) +} + +fn actually_extract_ruf(mut file: &File, output_folder: &str, start_offset: u64) -> Result<(), Box> { + file.seek(SeekFrom::Start(start_offset))?; + let header: RufHeader = file.read_be()?; + + println!("File info:\nBuyer: {} \nModel: {} \nRegion Info: {} \nDateTime: {}\nVersion:{:02x?} \nData Size: {} \nDual RUF: {}", + header.buyer(), header.model(), header.region_info(), header.date_time(), header.version_bytes, header.data_size, header.is_dual_ruf()); + + println!("\nPayload count: {}", header.payload_count); + file.seek(SeekFrom::Start(start_offset + header.payloads_start_offset as u64))?; + + let mut entries: Vec = Vec::new(); + + let mut vi = 0; + for _i in 0..28 { + if vi == header.payload_count { + break + } + let entry: RufEntry = file.read_be()?; + + if entry.payload_type_bytes == 0 && entry.size == 0 { + continue + } else { + vi += 1 + } + + println!("{}/{}: Type: {}({}), Size: {}", + vi, header.payload_count, entry.payload_type_bytes, entry.payload_type(), entry.size); + + entries.push(entry); + } + + let mut key: Option<&str> = None; + let key_bytes; + let iv_bytes = hex::decode("00000000000000000000000000000000")?.as_slice().try_into()?; + + //find key + for (prefix, value) in keys::RUF { + if header.model().starts_with(prefix) { + key = Some(value); + break; + } + } + if let Some(k) = key { + println!("\nKey: {}", k); + key_bytes = hex::decode(k)?.as_slice().try_into()?; + } else { + println!("\nSorry, this firmware is not supported!"); + std::process::exit(1); + } + + file.seek(SeekFrom::Start(start_offset + 2048))?; + let encrypted_data = common::read_exact(&mut file, header.data_size as usize)?; + println!("Decrypting data..."); + let decrypted_data = decrypt(&encrypted_data, &key_bytes, &iv_bytes)?; + + let mut data_reader = Cursor::new(decrypted_data); + + let mut ei = 1; + for entry in entries { + println!("\nEntry {}/{}: {} - {}, Size: {}", + ei, header.payload_count, entry.payload_type_bytes, entry.payload_type(), entry.size); + + let data = common::read_exact(&mut data_reader, entry.size as usize)?; + + let output_path = Path::new(&output_folder).join(format!("{}_{}.bin", entry.payload_type_bytes, entry.payload_type())); + 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!"); + + ei += 1; + } + + Ok(()) +} \ No newline at end of file diff --git a/src/keys.rs b/src/keys.rs index 3c4d529..2d7abf4 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -11,16 +11,6 @@ pub static SAMSUNG: &[(&str, &str)] = &[ ("T-ECP", "3EF6067262CF0C678598BFF22169D1F1EA57C284"), ]; -//pfl upg keys -pub static PFLUPG: &[(&str, &str)] = &[ - ("q5551", "D7EE8ED11AC048ED225D4F5F53F8509E55C5D94256A703C79E4CA78AE93ECE1639FE466363AD962BD6D6DE0C46FD19F363687C1D8A21820740A6E7FF87F41C4900DCE1E26EC122E5D4DFA76BFC8F296816B8D0910325E9DC5CBCC9579CF15FC0253EF7CF4919B7613491A5D7BF75DC1888531C458967FD1CF64B33139550BAA5"), - ("qf1eu", "ACD684155C7CCCB04372A8808514489FA9EE75D305987D1337420241FDBE0AE1F7CDFBB931C9D56C91D36F2CE79D222695B484FF42BCA12CE362C7C9ABBDEEC8E5D6107FADCF2D4DA5DF0693E13ACE54A18AEB21C051F6B62C075A1791985547C1CFF4FB5B6EA7E0A9405A1B2BB71EB89A9B209E0F62BF9794D673179C0E60F1"), - ("q591e", "AFAF89062747CBB29343C4E4EA775E4CFDF5FAFCD92C9DD858A8725201BA54AB973BEFEE04EBF3046910FBBC78B10120AE16D80BA734931E97248BC6B1D4A909F087D37BC0A9C2210FF8A2BE44C00F31E4DD8713A364623637FF75EBBCE9D3A840DB67E0FA910F127F679496F6C21112E3E3AD4ACA459FDE1CC58E300682E6F9"), - ("q522e", "C41BE92C212BAC76B48261E2A1704028287DD7E121C11DA25F709E864FBDC1BD8C7F226F57605A4B42D768CDD629AF9E54011A0967AFC2826331406FB1E90321620738526EA0BEA59F1A0E612AE891C396112F13531F423DF02F94D1C871429549F4D5B30D9CA3EFDCC6D7A96849F7C1788DE8FAAEDD36560337008DF06D612F"), - ("q5481", "9E7DA389815251E82A84A1182807702E72A0B0E0FF707C8E73E2EA71F79D5FAAFFC6E0B90ED16E13A4289C78A7D3BDA90626162AAE169D7BE28D6A635585CC10639C4E312E288EB8F7C5A44518B7E8A26A45C5023C5078A972A4CC219CA020BAF524F7429257B7AD76E1B15390879064C6ED59CC1F20CC04EEC26C9CF7FC0727"), - ("q5431", "C5FD937B301A5CEF4B6C25F187728C99636515D058895FEA469496E2B24907FC7721648841F8AE4C618E215673D0C029752FA970B6F9A7F48C9331293D3B1D43E4DFC7B52914973642CD3E4EE0AD11F5254505038F95CACE0DF21FC769B34E134435D88AB617D2981F66EF45BBC7796CFB1086C5D5672E837204991FE53BC1CF"), -]; - //MSD10 keys //github.com/bugficks/msddecrypt //fw prefix, type, key @@ -43,6 +33,21 @@ pub static MSD11: &[(&str, &str)] = &[ ("T-NKL", "5bab1098dab48792619ebd63650d929f"), //2020 ]; +//RUF keys +pub static RUF: &[(&str, &str)] = &[ + ("BD-P1600", "CB44682CFC0E0C50E1BEA74294ABBAE6"), +]; + +//pfl upg keys +pub static PFLUPG: &[(&str, &str)] = &[ + ("q5551", "D7EE8ED11AC048ED225D4F5F53F8509E55C5D94256A703C79E4CA78AE93ECE1639FE466363AD962BD6D6DE0C46FD19F363687C1D8A21820740A6E7FF87F41C4900DCE1E26EC122E5D4DFA76BFC8F296816B8D0910325E9DC5CBCC9579CF15FC0253EF7CF4919B7613491A5D7BF75DC1888531C458967FD1CF64B33139550BAA5"), + ("qf1eu", "ACD684155C7CCCB04372A8808514489FA9EE75D305987D1337420241FDBE0AE1F7CDFBB931C9D56C91D36F2CE79D222695B484FF42BCA12CE362C7C9ABBDEEC8E5D6107FADCF2D4DA5DF0693E13ACE54A18AEB21C051F6B62C075A1791985547C1CFF4FB5B6EA7E0A9405A1B2BB71EB89A9B209E0F62BF9794D673179C0E60F1"), + ("q591e", "AFAF89062747CBB29343C4E4EA775E4CFDF5FAFCD92C9DD858A8725201BA54AB973BEFEE04EBF3046910FBBC78B10120AE16D80BA734931E97248BC6B1D4A909F087D37BC0A9C2210FF8A2BE44C00F31E4DD8713A364623637FF75EBBCE9D3A840DB67E0FA910F127F679496F6C21112E3E3AD4ACA459FDE1CC58E300682E6F9"), + ("q522e", "C41BE92C212BAC76B48261E2A1704028287DD7E121C11DA25F709E864FBDC1BD8C7F226F57605A4B42D768CDD629AF9E54011A0967AFC2826331406FB1E90321620738526EA0BEA59F1A0E612AE891C396112F13531F423DF02F94D1C871429549F4D5B30D9CA3EFDCC6D7A96849F7C1788DE8FAAEDD36560337008DF06D612F"), + ("q5481", "9E7DA389815251E82A84A1182807702E72A0B0E0FF707C8E73E2EA71F79D5FAAFFC6E0B90ED16E13A4289C78A7D3BDA90626162AAE169D7BE28D6A635585CC10639C4E312E288EB8F7C5A44518B7E8A26A45C5023C5078A972A4CC219CA020BAF524F7429257B7AD76E1B15390879064C6ED59CC1F20CC04EEC26C9CF7FC0727"), + ("q5431", "C5FD937B301A5CEF4B6C25F187728C99636515D058895FEA469496E2B24907FC7721648841F8AE4C618E215673D0C029752FA970B6F9A7F48C9331293D3B1D43E4DFC7B52914973642CD3E4EE0AD11F5254505038F95CACE0DF21FC769B34E134435D88AB617D2981F66EF45BBC7796CFB1086C5D5672E837204991FE53BC1CF"), +]; + //epk2 keys //github.com/openlgtv/epk2extract pub static EPK2: &[(&str, &str)] = &[ diff --git a/src/main.rs b/src/main.rs index 0ebd9e7..a75e01e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,6 +82,10 @@ fn main() -> Result<(), Box> { println!("EPK file detected!"); formats::epk::extract_epk(&file, &output_path)?; } + else if formats::ruf::is_ruf_file(&file) { + println!("RUF file detected!"); + formats::ruf::extract_ruf(&file, &output_path)?; + } else if formats::pfl_upg::is_pfl_upg_file(&file) { println!("PFL UPG file detected!"); formats::pfl_upg::extract_pfl_upg(&file, &output_path)?;