add tsb bin extractor

This commit is contained in:
theubusu
2026-05-24 22:18:58 +02:00
parent b09cb32289
commit 8a90f8b04a
5 changed files with 474 additions and 0 deletions
+6
View File
@@ -224,5 +224,11 @@ Tip: if you have split ROM (.ROM-00 and .ROM-01), extract both into the same fol
**Notes:** **Depends on keys** - see keys.rs (Platforms up to MSB29 are supported)
**Thanks to:** http://malcolmstagg.com/bdp/s390-firmware.html
## TSB Bin
**Used in:** Older Toshiba TVs
**Notes:** None, all files should be supported.
**Options:**
※ Support `dump_dec_hdrs` option
# License
Licensed under GNU GPL v3.
+6
View File
@@ -34,6 +34,7 @@ pub mod cd5;
pub mod gx_dvb;
pub mod onkyo;
pub mod philips_bdp;
pub mod tsb_bin;
pub mod pup;
@@ -250,5 +251,10 @@ pub fn get_registry() -> Vec<Format> {
detector_func: crate::formats::mstar_secure_old::is_mstar_secure_old_file,
extractor_func: crate::formats::mstar_secure_old::extract_mstar_secure_old,
},
Format {
name: "tsb_bin",
detector_func: crate::formats::tsb_bin::is_tsb_bin_file,
extractor_func: crate::formats::tsb_bin::extract_tsb_bin,
},
]
}
+69
View File
@@ -0,0 +1,69 @@
use crate::utils::common;
use binrw::BinRead;
#[derive(BinRead)]
pub struct Header {
_signature: [u8; 128],
_control_id: u32, //must be zero
pub lenght: u32, //file size
pub entry_count: u32,
_pad1: [u8; 4],
build_no_bytes: [u8; 16],
_maker_id: u32,
_model_id: u32,
_group_id: u32,
_target_version: u32,
pub entry_addr: u32,
_pad3: [u8; 12],
_tsb_bin_ver: u32, //known is 3
_pad4: [u8; 12],
_active_flag: u32, //used in NAND to mark the active boot slot
_pad5: [u8; 24],
_key_component: u32, //used for calculating key for old type "signature"
_pad6: [u8; 8],
_image_checksum: u32, //sum 256..1024
_header_checksum: u32, //sum 0..252
#[br(count=entry_count)] pub entries: Vec<Entry>,
}
impl Header {
pub fn build_no(&self) -> String {
common::string_from_bytes(&self.build_no_bytes)
}
}
#[derive(BinRead)]
pub struct Entry {
name_bytes: [u8; 4],
image_flag: u32,
pub offset: u32,
pub size: u32,
pub load_addr: u32,
_pad1: [u8; 8],
_image_checksum: u32,
_pad2: [u8; 32],
}
impl Entry {
pub fn name(&self) -> String {
common::string_from_bytes(&self.name_bytes)
}
pub fn is_compressed(&self) -> bool {
(self.image_flag & 1 << 4) != 0
}
}
pub fn is_valid_header_checksum(header_data: &[u8]) -> bool {
if header_data.len() < 256 {
return false;
}
//calc first sum 63*4 = 252 bytes
let calc: u32 = header_data[..252]
.chunks_exact(4)
.map(|ch| {u32::from_be_bytes(ch.try_into().unwrap())})
.fold(0u32, |a, x| a.wrapping_add(x));
//expected checksum at end
let exp = u32::from_be_bytes(header_data[252..256].try_into().unwrap());
calc == exp
}
+92
View File
@@ -0,0 +1,92 @@
mod include;
mod tsb_des;
use std::any::Any;
use crate::AppContext;
use crate::utils::compression::decompress_zlib;
use crate::utils::global::opt_dump_dec_hdr;
use std::path::Path;
use std::fs::{self, OpenOptions};
use std::io::{Cursor, Write};
use binrw::BinReaderExt;
use crate::utils::common;
use include::*;
use tsb_des::decrypt;
struct TsbBinCtx {
key: Option<[u8; 8]>
}
pub fn is_tsb_bin_file(app_ctx: &AppContext) -> Result<Option<Box<dyn Any>>, Box<dyn std::error::Error>> {
let file = match app_ctx.file() {Some(f) => f, None => return Ok(None)};
let header = common::read_file(&file, 0, 256)?;
if is_valid_header_checksum(&header) {
return Ok(Some(Box::new(TsbBinCtx {key: None})));
}
// -- failed, try with decrypt
//derive key from FILE SIZE (yes)
let file_size = file.metadata()?.len() as u32;
let mut key = [0u8; 8];
key[..4].copy_from_slice(&file_size.to_le_bytes());
let inv = !file_size;
key[4..].copy_from_slice(&inv.to_le_bytes());
let dec_header = decrypt(&header, &key);
if is_valid_header_checksum(&dec_header) {
Ok(Some(Box::new(TsbBinCtx {key: Some(key)})))
} else {
Ok(None)
}
}
pub fn extract_tsb_bin(app_ctx: &AppContext, ctx: Box<dyn Any>) -> Result<(), Box<dyn std::error::Error>> {
let mut file = app_ctx.file().ok_or("Extractor expected file")?;
let ctx = ctx.downcast::<TsbBinCtx>().expect("Missing context");
let mut header = common::read_file(&mut file, 0, 0x400)?;
if let Some(key) = ctx.key { //decrypt header
println!("File is encrypted, using key: {}", hex::encode(&key));
header = decrypt(&header, &key);
opt_dump_dec_hdr(app_ctx, &header, "header")?;
}
let mut hdr_rdr = Cursor::new(header);
let hdr: Header = hdr_rdr.read_be()?;
println!("File info -\nSize: {}\nEntry count: {}\nBuild no.: {}\nEntry address: 0x{:02x}"
,hdr.lenght, hdr.entry_count, hdr.build_no(), hdr.entry_addr);
for (i, entry) in hdr.entries.iter().enumerate() {
println!("\n({}/{}) - {}, Size: {}, Offset: {}, Load address: 0x{:02x}",
i+1, hdr.entry_count, entry.name(), entry.size, entry.offset, entry.load_addr);
let mut data;
if let Some(key) = ctx.key {
let enc_data = common::read_file(&mut file, entry.offset as u64, (entry.size as usize + 7) & !7)?; //read aligned to 8b blocks for decryption
println!("- Decrypting...");
data = decrypt(&enc_data, &key);
data.truncate(entry.size as usize); //discard alignment
} else {
data = common::read_file(&mut file, entry.offset as u64, entry.size as usize)?;
}
if entry.is_compressed() {
println!("- Decompressing...");
data = decompress_zlib(&data)?;
}
let output_path = Path::new(&app_ctx.output_dir).join(format!("{}.bin", entry.name()));
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(&data)?;
println!("-- Saved file!");
}
Ok(())
}
+301
View File
@@ -0,0 +1,301 @@
// Source: github.com/antoyo/des-rs
// The des() function has been modified to work with Toshiba variant
/*
* Copyright (c) 2016 Boucher, Antoni <bouanto@zoho.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
pub type Key = [u8; 8];
const FIRST_BIT: u64 = 1 << 63;
const HALF_KEY_SIZE: i64 = KEY_SIZE / 2;
const KEY_SIZE: i64 = 56;
/// Do a circular left shift on a width of `HALF_KEY_SIZE`.
fn circular_left_shift(n1: u64, n2: u64, shift_count: i64) -> (u64, u64) {
let mut new_value1 = n1;
let mut new_value2 = n2;
for _ in 0 .. shift_count {
let first_bit = new_value1 & FIRST_BIT;
new_value1 = (new_value1 << 1) | (first_bit >> (HALF_KEY_SIZE - 1));
let first_bit = new_value2 & FIRST_BIT;
new_value2 = (new_value2 << 1) | (first_bit >> (HALF_KEY_SIZE - 1));
}
(new_value1, new_value2)
}
/// Create the 16 subkeys.
fn compute_subkeys(key: u64) -> Vec<u64> {
let table = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
let k0 = pc1(key);
let mut subkeys = vec![k0];
for shift_count in &table {
let last_key = *subkeys.last().unwrap();
let last_ci = last_key & 0xFFFFFFF000000000;
let last_di = last_key << HALF_KEY_SIZE;
let (ci, di) = circular_left_shift(last_ci, last_di, *shift_count);
let current_key = ci | (di >> HALF_KEY_SIZE);
subkeys.push(current_key);
}
subkeys.remove(0);
subkeys.iter().map(|&n| { pc2(n) }).collect()
}
/// Decrypt `message` using the `key`.
pub fn decrypt(cipher: &[u8], key: &Key) -> Vec<u8> {
let key = key_to_u64(key);
let mut subkeys = compute_subkeys(key);
subkeys.reverse();
des(cipher, subkeys)
}
/// Swap bits in `a` using a delta swap.
fn delta_swap(a: u64, delta: u64, mask: u64) -> u64 {
let b = (a ^ (a >> delta)) & mask;
a ^ b ^ (b << delta)
}
/// Encrypt `message` using `subkeys`.
fn des(message: &[u8], subkeys: Vec<u64>) -> Vec<u8> {
let message_len = message.len();
let message = message_to_u64s(message);
let mut blocks = vec![];
blocks = message.iter().map(|&block| {
let permuted = ip(block);
let mut li = permuted & 0xFFFFFFFF00000000;
let mut ri = permuted << 32;
for subkey in &subkeys { // SWAPPED - in toshiba, the feistel function is used on the LEFT half
let last_ri = ri;
ri = li;
li = last_ri ^ feistel(li, *subkey);
}
let r16l16 = li | (ri >> 32); // SWAPPED - order swapped in toshiba
to_u8_vec(fp(r16l16))
}).collect();
let mut result = Vec::with_capacity(message_len);
for mut block in blocks.into_iter() {
result.append(&mut block);
}
result
}
/// Swap bits using the E table.
fn e(block: u64) -> u64 {
const BLOCK_LEN: usize = 32;
const RESULT_LEN: usize = 48;
let b1 = (block << (BLOCK_LEN - 1)) & 0x8000000000000000;
let b2 = (block >> 1) & 0x7C00000000000000;
let b3 = (block >> 3) & 0x03F0000000000000;
let b4 = (block >> 5) & 0x000FC00000000000;
let b5 = (block >> 7) & 0x00003F0000000000;
let b6 = (block >> 9) & 0x000000FC00000000;
let b7 = (block >> 11) & 0x00000003F0000000;
let b8 = (block >> 13) & 0x000000000FC00000;
let b9 = (block >> 15) & 0x00000000003E0000;
let b10 = (block >> (RESULT_LEN - 1)) & 0x0000000000010000;
b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 | b10
}
/// Encrypt `message` using the `key`.
pub fn _encrypt(message: &[u8], key: &Key) -> Vec<u8> {
let key = key_to_u64(key);
let subkeys = compute_subkeys(key);
des(message, subkeys)
}
/// Feistel function.
fn feistel(half_block: u64, subkey: u64) -> u64 {
let expanded = e(half_block);
let mut intermediate = expanded ^ subkey;
let mut result = 0;
for i in 0 .. 8 {
let block = (intermediate & 0xFC00000000000000) >> 58;
intermediate <<= 6;
result <<= 4;
result |= s(i, block);
}
p(result << 32)
}
/// Swap bits using the reverse FP table.
fn fp(message: u64) -> u64 {
let message = delta_swap(message, 24, 0x000000FF000000FF);
let message = delta_swap(message, 24, 0x00000000FF00FF00);
let message = delta_swap(message, 36, 0x000000000F0F0F0F);
let message = delta_swap(message, 18, 0x0000333300003333);
delta_swap(message, 9, 0x0055005500550055)
}
/// Swap bits using the IP table.
fn ip(message: u64) -> u64 {
let message = delta_swap(message, 9, 0x0055005500550055);
let message = delta_swap(message, 18, 0x0000333300003333);
let message = delta_swap(message, 36, 0x000000000F0F0F0F);
let message = delta_swap(message, 24, 0x00000000FF00FF00);
delta_swap(message, 24, 0x000000FF000000FF)
}
/// Convert a `Key` to a 64-bits integer.
fn key_to_u64(key: &Key) -> u64 {
let mut result = 0;
for &part in key {
result <<= 8;
result += part as u64;
}
result
}
/// Convert a message to a vector of 64-bits integer.
fn message_to_u64s(message: &[u8]) -> Vec<u64> {
message.chunks(8)
.map(|m| key_to_u64(&to_key(m)))
.collect()
}
/// Swap bits using the P table.
fn p(block: u64) -> u64 {
let block = block.rotate_left(44);
let b1 = (block & 0x0000000000200000) << 32;
let b2 = (block & 0x0000000000480000) << 13;
let b3 = (block & 0x0000088000000000) << 12;
let b4 = (block & 0x0000002020120000) << 25;
let b5 = (block & 0x0000000442000000) << 14;
let b6 = (block & 0x0000000001800000) << 37;
let b7 = (block & 0x0000000004000000) << 24;
let b8 = (block & 0x0000020280015000).wrapping_mul(0x0000020080800083) & 0x02000a6400000000;
let b9 = (block.rotate_left(29) & 0x01001400000000aa).wrapping_mul(0x0000210210008081) & 0x0902c01200000000;
let b10 = (block & 0x0000000910040000).wrapping_mul(0x0000000c04000020) & 0x8410010000000000;
b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 | b10
}
/// Swap bits using the PC-1 table.
fn pc1(key: u64) -> u64 {
let key = delta_swap(key, 2, 0x3333000033330000);
let key = delta_swap(key, 4, 0x0f0f0f0f00000000);
let key = delta_swap(key, 8, 0x009a000a00a200a8);
let key = delta_swap(key, 16, 0x00006c6c0000cccc);
let key = delta_swap(key, 1, 0x1045500500550550);
let key = delta_swap(key, 32, 0x00000000f0f0f5fa);
let key = delta_swap(key, 8, 0x00550055006a00aa);
let key = delta_swap(key, 2, 0x0000333330000300);
key & 0xFFFFFFFFFFFFFF00
}
/// Swap bits using the PC-2 table.
fn pc2(key: u64) -> u64 {
let key = key.rotate_left(61);
let b1 = (key & 0x0021000002000000) >> 7;
let b2 = (key & 0x0008020010080000) << 1;
let b3 = key & 0x0002200000000000;
let b4 = (key & 0x0000000000100020) << 19;
let b5 = (key.rotate_left(54) & 0x0005312400000011).wrapping_mul(0x0000000094200201) & 0xea40100880000000;
let b6 = (key.rotate_left(7) & 0x0022110000012001).wrapping_mul(0x0001000000610006) & 0x1185004400000000;
let b7 = (key.rotate_left(6) & 0x0000520040200002).wrapping_mul(0x00000080000000c1) & 0x0028811000200000;
let b8 = (key & 0x01000004c0011100).wrapping_mul(0x0000000000004284) & 0x0400082244400000;
let b9 = (key.rotate_left(60) & 0x0000000000820280).wrapping_mul(0x0000000000089001) & 0x0000000110880000;
let b10 = (key.rotate_left(49) & 0x0000000000024084).wrapping_mul(0x0000000002040005) & 0x000000000a030000;
b1 | b2 | b3 | b4 | b5 | b6 | b7 | b8 | b9 | b10
}
/// Produce 4-bits using an S box.
fn s(box_id: usize, block: u64) -> u64 {
const TABLES: [[[u64; 16]; 4]; 8] =
[[[ 14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7]
, [ 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8]
, [ 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0]
, [ 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13]
],
[ [ 15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10]
, [ 3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5]
, [ 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15]
, [ 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9]
],
[ [ 10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8]
, [ 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1]
, [ 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7]
, [ 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12]
],
[ [ 7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15]
, [ 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9]
, [ 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4]
, [ 3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14]
],
[ [ 2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9]
, [ 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6]
, [ 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14]
, [ 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3]
],
[ [ 12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11]
, [ 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8]
, [ 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6]
, [ 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13]
],
[ [ 4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1]
, [ 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6]
, [ 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2]
, [ 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12]
],
[ [ 13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7]
, [ 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2]
, [ 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8]
, [ 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11]
]];
let i = ((block & 0x20) >> 4 | (block & 1)) as usize;
let j = ((block & 0x1E) >> 1) as usize;
// TODO: use get_unchecked().
TABLES[box_id][i][j]
}
/// Convert a slice to a `Key`.
fn to_key(slice: &[u8]) -> Key {
let mut vec: Vec<u8> = slice.iter().cloned().collect();
let mut key = [0; 8];
let diff = key.len() - vec.len();
if diff > 0 {
vec.append(&mut vec![0; diff]);
}
key.clone_from_slice(&vec);
key
}
/// Convert a `u64` to a `Vec<u8>`.
fn to_u8_vec(num: u64) -> Vec<u8> {
vec!
[ ((num & 0xFF00000000000000) >> 56) as u8
, ((num & 0x00FF000000000000) >> 48) as u8
, ((num & 0x0000FF0000000000) >> 40) as u8
, ((num & 0x000000FF00000000) >> 32) as u8
, ((num & 0x00000000FF000000) >> 24) as u8
, ((num & 0x0000000000FF0000) >> 16) as u8
, ((num & 0x000000000000FF00) >> 8) as u8
, (num & 0x00000000000000FF) as u8
]
}