From 5f926ff5a25b5825078d7b133a142f1812f32f4b Mon Sep 17 00:00:00 2001 From: theubusu <80545678+theubusu@users.noreply.github.com> Date: Fri, 1 May 2026 21:38:35 +0200 Subject: [PATCH] add onkyo extractor --- README.md | 9 ++- src/formats.rs | 6 ++ src/formats/onkyo/crypto.rs | 53 ++++++++++++++ src/formats/onkyo/include.rs | 54 ++++++++++++++ src/formats/onkyo/mod.rs | 134 +++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 src/formats/onkyo/crypto.rs create mode 100644 src/formats/onkyo/include.rs create mode 100644 src/formats/onkyo/mod.rs diff --git a/README.md b/README.md index d2abb37..7348e9c 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,14 @@ Tip: if you have split ROM (.ROM-00 and .ROM-01), extract both into the same fol ## Novatek TIMG **Used in:** Newer Novatek-based TVs (Philips(TPVision), Hisense, TCL...) -**Notes:** None, all files should be supported. +**Notes:** None, all files should be supported. + +## Onkyo +**Used in:** Onkyo AVRs and other AV devices +**Notes:** Newer files seem to use a different encryption and are not (yet) supported. +**Thanks to:** http://divideoverflow.com/2014/04/decrypting-onkyo-firmware-files/ +**Options:** +※ Support `dump_dec_hdrs` option ## Panasonic Blu-Ray (PANA_DVD.FRM, PANA_ESD.FRM, PANAEUSB.FRM) **Used in:** Panasonic Blu-Ray Players and Recorders diff --git a/src/formats.rs b/src/formats.rs index f833c4a..12e69c4 100644 --- a/src/formats.rs +++ b/src/formats.rs @@ -30,6 +30,7 @@ pub mod sdboot; pub mod sdimage; pub mod cd5; pub mod gx_dvb; +pub mod onkyo; pub mod pup; @@ -226,6 +227,11 @@ pub fn get_registry() -> Vec { detector_func: crate::formats::gx_dvb::is_gx_dvb_file, extractor_func: crate::formats::gx_dvb::extract_gx_dvb, }, + Format { + name: "onkyo", + detector_func: crate::formats::onkyo::is_onkyo_file, + extractor_func: crate::formats::onkyo::extract_onkyo, + }, ] } \ No newline at end of file diff --git a/src/formats/onkyo/crypto.rs b/src/formats/onkyo/crypto.rs new file mode 100644 index 0000000..f916c1f --- /dev/null +++ b/src/formats/onkyo/crypto.rs @@ -0,0 +1,53 @@ +use super::include::ONKYO_MAGIC; + +pub fn ub_encrypte_block(input: &[u8], key: &[u8; 8]) -> Vec { + let mut output = Vec::with_capacity(input.len()); + let state_counter = 0; + let state_byte = 0; + + let mut u_var3: u8 = if state_counter == 0 { + key[0] + } else { + state_byte + }; + + let mut u_var4: u32 = (state_counter % 7) + 1; + let mut u_var7: u32 = state_counter; + + for &byte in input { + output.push(u_var3 ^ byte); + + let pb_var1 = key[u_var4 as usize]; + u_var7 = u_var7.wrapping_add(1); + + if (u_var4 + 1) * 0x10000 < 0x70001 { + u_var4 = (u_var4 + 1) & 0xFFFF; + } else { + u_var4 = 1; + } + + let rot = ((u_var3 >> 7) | ((u_var3 & 0x7F) << 1)) & 0xFF; + + u_var3 = pb_var1 + .wrapping_add(rot) + .wrapping_add((u_var7 >> 6) as u8); + } + + output +} + +pub fn calc_key(ciphertext: &[u8]) -> [u8; 8] { + let mut key = [0u8; 8]; + + let mut ks = ONKYO_MAGIC[0] ^ ciphertext[0]; + key[0] = ks; + + for j in 1..8 { + let rol = (ks >> 7) | ((ks & 0x7f) << 1); + ks = ONKYO_MAGIC[j] ^ ciphertext[j]; + + key[j] = ks.wrapping_sub(rol); + } + + key +} \ No newline at end of file diff --git a/src/formats/onkyo/include.rs b/src/formats/onkyo/include.rs new file mode 100644 index 0000000..3b9f4c4 --- /dev/null +++ b/src/formats/onkyo/include.rs @@ -0,0 +1,54 @@ +use crate::utils::common; +use binrw::BinRead; + +pub static ONKYO_MAGIC: &[u8] = b"ONKYO Encryption"; +pub static HEADER_KEY: [u8; 8] = [0xDA, 0x57, 0x68, 0x0D, 0x44, 0x21, 0x30, 0x7A]; +pub static DATA_KEY: [u8; 8] = [0xAE, 0xB7, 0x31, 0x74, 0x47, 0xE4, 0xFB, 0x5D]; + +#[derive(BinRead)] +pub struct Header { + _magic_bytes: [u8;16], //ONKYO Encryption + _header_size: u32, + _header_checksum: u32, + pub pack_info_offset: u32, + pub ids_versions_offset: u32, + pub table_offset: u32, + _pad: [u8;12], +} + +#[derive(BinRead)] +pub struct PackInfo { + package_id_bytes: [u8;32], + package_version_bytes: [u8;4], + pub entry_count: u8, //maximum number of entries that can be in the file + pub pack_count: u8, //how many .of* files there are + pub pack_id: u8, //which .of* file this is + pub entries_in_file: u8, //how many entries are actually in the file +} +impl PackInfo { + pub fn package_id(&self) -> String { + common::string_from_bytes(&self.package_id_bytes) + } + pub fn package_version(&self) -> String { + common::string_from_bytes(&self.package_version_bytes) + } +} + +#[derive(BinRead)] +pub struct IDsVersionsEntry { + pub pack_location: u8, //in which .of* file(pack) this entry is + id_bytes: [u8;7], +} +impl IDsVersionsEntry { + pub fn id(&self) -> String { + common::string_from_bytes(&self.id_bytes) + } +} + +#[derive(BinRead)] +pub struct TableEntry { + pub size: u32, + pub offset: u32, + pub checksum: u32, + _pad: [u8;4], +} \ No newline at end of file diff --git a/src/formats/onkyo/mod.rs b/src/formats/onkyo/mod.rs new file mode 100644 index 0000000..6e1ece6 --- /dev/null +++ b/src/formats/onkyo/mod.rs @@ -0,0 +1,134 @@ +mod include; +mod crypto; +use std::any::Any; +use crate::AppContext; +use crate::utils::global::opt_dump_dec_hdr; + +use std::path::Path; +use std::fs::{self, OpenOptions}; +use std::io::{Cursor, Seek, SeekFrom, Write}; +use binrw::BinReaderExt; + +use crate::utils::common; +use include::*; +use crypto::*; + +struct OnkyoCtx { + header_size: u32, +} + +pub fn is_onkyo_file(app_ctx: &AppContext) -> Result>, Box> { + let file = match app_ctx.file() {Some(f) => f, None => return Ok(None)}; + + let enc_inihdr = common::read_file(&file, 0, 20)?; + let dec_inihdr= ub_encrypte_block(&enc_inihdr, &HEADER_KEY); + + if dec_inihdr.starts_with(ONKYO_MAGIC) { + let header_size = u32::from_le_bytes(dec_inihdr[16..20].try_into().unwrap()); + Ok(Some(Box::new(OnkyoCtx {header_size}))) + } else { + Ok(None) + } +} + +pub fn extract_onkyo(app_ctx: &AppContext, ctx: Box) -> Result<(), Box> { + let mut file = app_ctx.file().ok_or("Extractor expected file")?; + let ctx = ctx.downcast::().expect("Missing context"); + + println!("Header size: {}", ctx.header_size); + + let enc_hdr = common::read_exact(&mut file, ctx.header_size as usize)?; + let dec_hdr = ub_encrypte_block(&enc_hdr, &HEADER_KEY); + opt_dump_dec_hdr(app_ctx, &dec_hdr, "header")?; + let mut hdr_rdr = Cursor::new(dec_hdr); + + let hdr: Header = hdr_rdr.read_le()?; + + //read info section + hdr_rdr.seek(SeekFrom::Start(hdr.pack_info_offset as u64))?; + let info: PackInfo = hdr_rdr.read_le()?; + println!("Info -\nPackage ID: {}\nVersion: {}\nEntry count: {}\nEntries in file: {}\nPack: {}/{}", + info.package_id(), info.package_version(), info.entry_count, info.entries_in_file, info.pack_id, info.pack_count,); + + if info.entries_in_file == 0 { + return Err("There is nothing to extract in this pack".into()) //should this be an error? + } + + //..."IDsVersions" section + hdr_rdr.seek(SeekFrom::Start(hdr.ids_versions_offset as u64))?; + let mut entries: Vec = Vec::new(); + for _ in 0..info.entry_count { + let entry: IDsVersionsEntry = hdr_rdr.read_le()?; + entries.push(entry); + } + + //...table section + hdr_rdr.seek(SeekFrom::Start(hdr.table_offset as u64))?; + let mut data_entries: Vec = Vec::new(); + for _ in 0..info.entry_count { + let entry: TableEntry = hdr_rdr.read_le()?; + data_entries.push(entry); + } + + // + let mut act_ei = 0; + for (i, entry) in entries.iter().enumerate() { + let data_entry = &data_entries[i]; + + if entry.pack_location == 0 || (data_entry.offset == 0 && data_entry.checksum == 0){ + continue; + } + + act_ei += 1; + println!("\n({}/{}) - {}, Size: {}, Offset: {}, Pack location: {}", + act_ei, info.entries_in_file, entry.id(), data_entry.size, data_entry.offset, entry.pack_location); + + let data = common::read_file(&mut file, data_entry.offset as u64, data_entry.size as usize)?; + let mut out_data; + + // -- try to decrypt -- + let mut dec_key: Option<[u8; 8]> = None; + let mut is_pack = false; + + //try using standard data key + if ub_encrypte_block(&data[..16], &DATA_KEY).starts_with(ONKYO_MAGIC) { + dec_key = Some(DATA_KEY); + + //try header key, if success it means this is a package inside a package, and it should not be decrypted + } else if ub_encrypte_block(&data[..16], &HEADER_KEY).starts_with(ONKYO_MAGIC) { + is_pack = true; + + //if not matched with data key, try to calc key + } else { + let calced_key = calc_key(&data[..8]); + + if ub_encrypte_block(&data[..16], &calced_key).starts_with(ONKYO_MAGIC) { + dec_key = Some(calced_key); + } + } + + if let Some(key) = dec_key { + println!("- Detected encrypted data, decrypting..."); + out_data = ub_encrypte_block(&data, &key); + out_data.drain(0..16); //remove ONKYO Encryption heading + + } else if is_pack { + println!("- Inner pack detected!"); //maybe handle this... + out_data = data; + + } else { + println!("- Failed to decrypt data or entry is not encrypted, saving raw data..."); + out_data = data; + } + + let output_path = Path::new(&app_ctx.output_dir).join(format!("{}_{}.bin", act_ei, entry.id())); + fs::create_dir_all(&app_ctx.output_dir)?; + let mut out_file = OpenOptions::new().write(true).create(true).open(output_path)?; + out_file.write_all(&out_data)?; + + println!("-- Saved file!"); + + } + + Ok(()) +} \ No newline at end of file