mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2025-06-05 23:11:02 -04:00
Basic TMD/Ticket structs, Title Key encryption handling
This commit is contained in:
parent
fcde6831de
commit
522e42a749
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
120
Cargo.lock
generated
Normal file
120
Cargo.lock
generated
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-padding"
|
||||||
|
version = "0.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cbc"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generic-array"
|
||||||
|
version = "0.14.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"block-padding",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.171"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustii"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"aes",
|
||||||
|
"byteorder",
|
||||||
|
"cbc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "version_check"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "rustii"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "rustii"
|
||||||
|
path = "src/bin/rustii/main.rs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/lib.rs"
|
||||||
|
test = true
|
||||||
|
doc = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
byteorder = "1"
|
||||||
|
cbc = "0"
|
||||||
|
aes = "0"
|
21
src/bin/rustii/main.rs
Normal file
21
src/bin/rustii/main.rs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
use std::fs;
|
||||||
|
use rustii::title::{tmd, ticket, crypto};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let data = fs::read("title.tmd").unwrap();
|
||||||
|
let tmd = tmd::TMD::from_bytes(&data).unwrap();
|
||||||
|
println!("num content records: {:?}", tmd.content_records.len());
|
||||||
|
println!("first record data: {:?}", tmd.content_records.first().unwrap());
|
||||||
|
assert_eq!(data, tmd.to_vec().unwrap());
|
||||||
|
|
||||||
|
let data = fs::read("tik").unwrap();
|
||||||
|
let tik = ticket::Ticket::from_bytes(&data).unwrap();
|
||||||
|
println!("title version from ticket is: {:?}", tik.title_version);
|
||||||
|
println!("title key (enc): {:?}", tik.title_key);
|
||||||
|
println!("title key (dec): {:?}", tik.dec_title_key());
|
||||||
|
assert_eq!(data, tik.to_vec().unwrap());
|
||||||
|
|
||||||
|
|
||||||
|
assert_eq!(tik.title_key, crypto::encrypt_title_key(tik.dec_title_key(), tik.common_key_index, tik.title_id));
|
||||||
|
println!("re-encrypted key matched");
|
||||||
|
}
|
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// lib.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii-lib
|
||||||
|
|
||||||
|
|
||||||
|
pub mod title;
|
49
src/title/commonkeys.rs
Normal file
49
src/title/commonkeys.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// title/commonkeys.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii-lib
|
||||||
|
|
||||||
|
const COMMON_KEY: [u8; 16] = [0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7];
|
||||||
|
const KOREAN_KEY: [u8; 16] = [0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e];
|
||||||
|
const VWII_KEY: [u8; 16] = [0x30, 0xbf, 0xc7, 0x6e, 0x7c, 0x19, 0xaf, 0xbb, 0x23, 0x16, 0x33, 0x30, 0xce, 0xd7, 0xc2, 0x8d];
|
||||||
|
const DEV_COMMON_KEY: [u8; 16] = [0xa1, 0x60, 0x4a, 0x6a, 0x71, 0x23, 0xb5, 0x29, 0xae, 0x8b, 0xec, 0x32, 0xc8, 0x16, 0xfc, 0xaa];
|
||||||
|
|
||||||
|
pub fn get_common_key(index: u8, is_dev: Option<bool>) -> [u8; 16] {
|
||||||
|
// Match the Korean and vWii keys, and if they don't match then fall back on the common key.
|
||||||
|
// The is_dev argument is an option, and if it's set to false or None, then the regular
|
||||||
|
// common key will be used.
|
||||||
|
match index {
|
||||||
|
1 => KOREAN_KEY,
|
||||||
|
2 => VWII_KEY,
|
||||||
|
_ => {
|
||||||
|
match is_dev {
|
||||||
|
Some(true) => DEV_COMMON_KEY,
|
||||||
|
_ => COMMON_KEY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_common_key() {
|
||||||
|
assert_eq!(get_common_key(0, None), [0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7]);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_get_invalid_index() {
|
||||||
|
assert_eq!(get_common_key(57, None), [0xeb, 0xe4, 0x2a, 0x22, 0x5e, 0x85, 0x93, 0xe4, 0x48, 0xd9, 0xc5, 0x45, 0x73, 0x81, 0xaa, 0xf7]);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_get_korean_key() {
|
||||||
|
assert_eq!(get_common_key(1, None), [0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e]);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_get_vwii_key() {
|
||||||
|
assert_eq!(get_common_key(2, None), [0x30, 0xbf, 0xc7, 0x6e, 0x7c, 0x19, 0xaf, 0xbb, 0x23, 0x16, 0x33, 0x30, 0xce, 0xd7, 0xc2, 0x8d]);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn test_get_dev_key() {
|
||||||
|
assert_eq!(get_common_key(0, Some(true)), [0xa1, 0x60, 0x4a, 0x6a, 0x71, 0x23, 0xb5, 0x29, 0xae, 0x8b, 0xec, 0x32, 0xc8, 0x16, 0xfc, 0xaa]);
|
||||||
|
}
|
||||||
|
}
|
35
src/title/crypto.rs
Normal file
35
src/title/crypto.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// title/crypto.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii-lib
|
||||||
|
//
|
||||||
|
// Implements the common crypto functions required to handle Wii content encryption.
|
||||||
|
|
||||||
|
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
||||||
|
use aes::cipher::block_padding::ZeroPadding;
|
||||||
|
use crate::title::commonkeys::get_common_key;
|
||||||
|
|
||||||
|
// Convert a Title ID into the format required for use as the Title Key decryption IV.
|
||||||
|
fn title_id_to_iv(title_id: [u8; 8]) -> [u8; 16] {
|
||||||
|
let mut iv: Vec<u8> = Vec::from(title_id);
|
||||||
|
iv.resize(16, 0);
|
||||||
|
iv.as_slice().try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt a Title Key using the specified common key.
|
||||||
|
pub fn decrypt_title_key(title_key_enc: [u8; 16], common_key_index: u8, title_id: [u8; 8]) -> [u8; 16] {
|
||||||
|
let iv = title_id_to_iv(title_id);
|
||||||
|
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
|
||||||
|
let decryptor = Aes128CbcDec::new(&get_common_key(common_key_index, None).into(), &iv.into());
|
||||||
|
let mut title_key = title_key_enc;
|
||||||
|
decryptor.decrypt_padded_mut::<ZeroPadding>(&mut title_key).unwrap();
|
||||||
|
title_key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt a Title Key using the specified common key.
|
||||||
|
pub fn encrypt_title_key(title_key_dec: [u8; 16], common_key_index: u8, title_id: [u8; 8]) -> [u8; 16] {
|
||||||
|
let iv = title_id_to_iv(title_id);
|
||||||
|
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
|
||||||
|
let encryptor = Aes128CbcEnc::new(&get_common_key(common_key_index, None).into(), &iv.into());
|
||||||
|
let mut title_key = title_key_dec;
|
||||||
|
encryptor.encrypt_padded_mut::<ZeroPadding>(&mut title_key, 16).unwrap();
|
||||||
|
title_key
|
||||||
|
}
|
8
src/title/mod.rs
Normal file
8
src/title/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// title/mod.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii-lib
|
||||||
|
|
||||||
|
pub mod commonkeys;
|
||||||
|
pub mod crypto;
|
||||||
|
pub mod ticket;
|
||||||
|
pub mod tmd;
|
||||||
|
|
155
src/title/ticket.rs
Normal file
155
src/title/ticket.rs
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// title/tik.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii-lib
|
||||||
|
//
|
||||||
|
// Implements the structures and methods required for Ticket parsing and editing.
|
||||||
|
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use crate::title::crypto::decrypt_title_key;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[derive(Copy)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TitleLimit {
|
||||||
|
// The type of limit being applied (time, launch count, etc.)
|
||||||
|
pub limit_type: u32,
|
||||||
|
// The maximum value for that limit (seconds, max launches, etc.)
|
||||||
|
pub limit_max: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Ticket {
|
||||||
|
pub signature_type: u32,
|
||||||
|
pub signature: [u8; 256],
|
||||||
|
padding1: [u8; 60],
|
||||||
|
pub signature_issuer: [u8; 64],
|
||||||
|
pub ecdh_data: [u8; 60],
|
||||||
|
pub ticket_version: u8,
|
||||||
|
reserved1: [u8; 2],
|
||||||
|
pub title_key: [u8; 16],
|
||||||
|
unknown1: [u8; 1],
|
||||||
|
pub ticket_id: [u8; 8],
|
||||||
|
pub console_id: [u8; 4],
|
||||||
|
pub title_id: [u8; 8],
|
||||||
|
unknown2: [u8; 2],
|
||||||
|
pub title_version: u16,
|
||||||
|
pub permitted_titles_mask: [u8; 4],
|
||||||
|
pub permit_mask: [u8; 4],
|
||||||
|
pub title_export_allowed: u8,
|
||||||
|
pub common_key_index: u8,
|
||||||
|
unknown3: [u8; 48],
|
||||||
|
pub content_access_permission: [u8; 64],
|
||||||
|
padding2: [u8; 2],
|
||||||
|
pub title_limits: [TitleLimit; 8],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ticket {
|
||||||
|
pub fn from_bytes(data: &[u8]) -> Result<Self, std::io::Error> {
|
||||||
|
let mut buf = Cursor::new(data);
|
||||||
|
let signature_type = buf.read_u32::<BigEndian>()?;
|
||||||
|
let mut signature = [0u8; 256];
|
||||||
|
buf.read_exact(&mut signature)?;
|
||||||
|
// Maybe this can be read differently?
|
||||||
|
let mut padding1 = [0u8; 60];
|
||||||
|
buf.read_exact(&mut padding1)?;
|
||||||
|
let mut signature_issuer = [0u8; 64];
|
||||||
|
buf.read_exact(&mut signature_issuer)?;
|
||||||
|
let mut ecdh_data = [0u8; 60];
|
||||||
|
buf.read_exact(&mut ecdh_data)?;
|
||||||
|
let ticket_version = buf.read_u8()?;
|
||||||
|
let mut reserved1 = [0u8; 2];
|
||||||
|
buf.read_exact(&mut reserved1)?;
|
||||||
|
let mut title_key = [0u8; 16];
|
||||||
|
buf.read_exact(&mut title_key)?;
|
||||||
|
let mut unknown1 = [0u8; 1];
|
||||||
|
buf.read_exact(&mut unknown1)?;
|
||||||
|
let mut ticket_id = [0u8; 8];
|
||||||
|
buf.read_exact(&mut ticket_id)?;
|
||||||
|
let mut console_id = [0u8; 4];
|
||||||
|
buf.read_exact(&mut console_id)?;
|
||||||
|
let mut title_id = [0u8; 8];
|
||||||
|
buf.read_exact(&mut title_id)?;
|
||||||
|
let mut unknown2 = [0u8; 2];
|
||||||
|
buf.read_exact(&mut unknown2)?;
|
||||||
|
let title_version = buf.read_u16::<BigEndian>()?;
|
||||||
|
let mut permitted_titles_mask = [0u8; 4];
|
||||||
|
buf.read_exact(&mut permitted_titles_mask)?;
|
||||||
|
let mut permit_mask = [0u8; 4];
|
||||||
|
buf.read_exact(&mut permit_mask)?;
|
||||||
|
let title_export_allowed = buf.read_u8()?;
|
||||||
|
let common_key_index = buf.read_u8()?;
|
||||||
|
let mut unknown3 = [0u8; 48];
|
||||||
|
buf.read_exact(&mut unknown3)?;
|
||||||
|
let mut content_access_permission = [0u8; 64];
|
||||||
|
buf.read_exact(&mut content_access_permission)?;
|
||||||
|
let mut padding2 = [0u8; 2];
|
||||||
|
buf.read_exact(&mut padding2)?;
|
||||||
|
// Build the array of title limits.
|
||||||
|
let mut title_limits: Vec<TitleLimit> = Vec::new();
|
||||||
|
for _ in 0..8 {
|
||||||
|
let limit_type = buf.read_u32::<BigEndian>()?;
|
||||||
|
let limit_max = buf.read_u32::<BigEndian>()?;
|
||||||
|
title_limits.push(TitleLimit { limit_type, limit_max });
|
||||||
|
}
|
||||||
|
let title_limits = title_limits.try_into().unwrap();
|
||||||
|
Ok(Ticket {
|
||||||
|
signature_type,
|
||||||
|
signature,
|
||||||
|
padding1,
|
||||||
|
signature_issuer,
|
||||||
|
ecdh_data,
|
||||||
|
ticket_version,
|
||||||
|
reserved1,
|
||||||
|
title_key,
|
||||||
|
unknown1,
|
||||||
|
ticket_id,
|
||||||
|
console_id,
|
||||||
|
title_id,
|
||||||
|
unknown2,
|
||||||
|
title_version,
|
||||||
|
permitted_titles_mask,
|
||||||
|
permit_mask,
|
||||||
|
title_export_allowed,
|
||||||
|
common_key_index,
|
||||||
|
unknown3,
|
||||||
|
content_access_permission,
|
||||||
|
padding2,
|
||||||
|
title_limits,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.write_u32::<BigEndian>(self.signature_type)?;
|
||||||
|
buf.write_all(&self.signature)?;
|
||||||
|
buf.write_all(&self.padding1)?;
|
||||||
|
buf.write_all(&self.signature_issuer)?;
|
||||||
|
buf.write_all(&self.ecdh_data)?;
|
||||||
|
buf.write_u8(self.ticket_version)?;
|
||||||
|
buf.write_all(&self.reserved1)?;
|
||||||
|
buf.write_all(&self.title_key)?;
|
||||||
|
buf.write_all(&self.unknown1)?;
|
||||||
|
buf.write_all(&self.ticket_id)?;
|
||||||
|
buf.write_all(&self.console_id)?;
|
||||||
|
buf.write_all(&self.title_id)?;
|
||||||
|
buf.write_all(&self.unknown2)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.title_version)?;
|
||||||
|
buf.write_all(&self.permitted_titles_mask)?;
|
||||||
|
buf.write_all(&self.permit_mask)?;
|
||||||
|
buf.write_u8(self.title_export_allowed)?;
|
||||||
|
buf.write_u8(self.common_key_index)?;
|
||||||
|
buf.write_all(&self.unknown3)?;
|
||||||
|
buf.write_all(&self.content_access_permission)?;
|
||||||
|
buf.write_all(&self.padding2)?;
|
||||||
|
// Iterate over title limits and write out their data.
|
||||||
|
for limit in &self.title_limits {
|
||||||
|
buf.write_u32::<BigEndian>(limit.limit_type)?;
|
||||||
|
buf.write_u32::<BigEndian>(limit.limit_max)?;
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dec_title_key(&self) -> [u8; 16] {
|
||||||
|
decrypt_title_key(self.title_key, self.common_key_index, self.title_id)
|
||||||
|
}
|
||||||
|
}
|
185
src/title/tmd.rs
Normal file
185
src/title/tmd.rs
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
// title/tmd.rs from rustii-lib (c) 2025 NinjaCheetah & Contributors
|
||||||
|
// https://github.com/NinjaCheetah/rustii-lib
|
||||||
|
//
|
||||||
|
// Implements the structures and methods required for TMD parsing and editing.
|
||||||
|
|
||||||
|
use std::io::{Cursor, Read, Write};
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ContentRecord {
|
||||||
|
pub content_id: u32,
|
||||||
|
pub index: u16,
|
||||||
|
pub content_type: u16,
|
||||||
|
pub content_size: u64,
|
||||||
|
pub content_hash: [u8; 20],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TMD {
|
||||||
|
pub signature_type: u32,
|
||||||
|
pub signature: [u8; 256],
|
||||||
|
padding1: [u8; 60],
|
||||||
|
pub signature_issuer: [u8; 64],
|
||||||
|
pub tmd_version: u8,
|
||||||
|
pub ca_crl_version: u8,
|
||||||
|
pub signer_crl_version: u8,
|
||||||
|
pub is_vwii: u8,
|
||||||
|
pub ios_tid: [u8; 8],
|
||||||
|
pub title_id: [u8; 8],
|
||||||
|
pub title_type: [u8; 4],
|
||||||
|
pub group_id: u16,
|
||||||
|
padding2: [u8; 2],
|
||||||
|
pub region: u16,
|
||||||
|
pub ratings: [u8; 16],
|
||||||
|
reserved1: [u8; 12],
|
||||||
|
pub ipc_mask: [u8; 12],
|
||||||
|
reserved2: [u8; 18],
|
||||||
|
pub access_rights: u32,
|
||||||
|
pub title_version: u16,
|
||||||
|
pub num_contents: u16,
|
||||||
|
pub boot_index: u16,
|
||||||
|
pub minor_version: u16, // Normally unused, but good for fakesigning!
|
||||||
|
pub content_records: Vec<ContentRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TMD {
|
||||||
|
pub fn from_bytes(data: &[u8]) -> Result<Self, std::io::Error> {
|
||||||
|
let mut buf = Cursor::new(data);
|
||||||
|
let signature_type = buf.read_u32::<BigEndian>()?;
|
||||||
|
let mut signature = [0u8; 256];
|
||||||
|
buf.read_exact(&mut signature)?;
|
||||||
|
// Maybe this can be read differently?
|
||||||
|
let mut padding1 = [0u8; 60];
|
||||||
|
buf.read_exact(&mut padding1)?;
|
||||||
|
let mut signature_issuer = [0u8; 64];
|
||||||
|
buf.read_exact(&mut signature_issuer)?;
|
||||||
|
let tmd_version = buf.read_u8()?;
|
||||||
|
let ca_crl_version = buf.read_u8()?;
|
||||||
|
let signer_crl_version = buf.read_u8()?;
|
||||||
|
let is_vwii = buf.read_u8()?;
|
||||||
|
let mut ios_tid = [0u8; 8];
|
||||||
|
buf.read_exact(&mut ios_tid)?;
|
||||||
|
let mut title_id = [0u8; 8];
|
||||||
|
buf.read_exact(&mut title_id)?;
|
||||||
|
let mut title_type = [0u8; 4];
|
||||||
|
buf.read_exact(&mut title_type)?;
|
||||||
|
let group_id = buf.read_u16::<BigEndian>()?;
|
||||||
|
// Same here...
|
||||||
|
let mut padding2 = [0u8; 2];
|
||||||
|
buf.read_exact(&mut padding2)?;
|
||||||
|
let region = buf.read_u16::<BigEndian>()?;
|
||||||
|
let mut ratings = [0u8; 16];
|
||||||
|
buf.read_exact(&mut ratings)?;
|
||||||
|
// ...and here...
|
||||||
|
let mut reserved1 = [0u8; 12];
|
||||||
|
buf.read_exact(&mut reserved1)?;
|
||||||
|
let mut ipc_mask = [0u8; 12];
|
||||||
|
buf.read_exact(&mut ipc_mask)?;
|
||||||
|
// ...and here.
|
||||||
|
let mut reserved2 = [0u8; 18];
|
||||||
|
buf.read_exact(&mut reserved2)?;
|
||||||
|
let access_rights = buf.read_u32::<BigEndian>()?;
|
||||||
|
let title_version = buf.read_u16::<BigEndian>()?;
|
||||||
|
let num_contents = buf.read_u16::<BigEndian>()?;
|
||||||
|
let boot_index = buf.read_u16::<BigEndian>()?;
|
||||||
|
let minor_version = buf.read_u16::<BigEndian>()?;
|
||||||
|
// Build content records by iterating over the rest of the data num_contents times.
|
||||||
|
let mut content_records = Vec::with_capacity(num_contents as usize);
|
||||||
|
for _ in 0..num_contents {
|
||||||
|
let content_id = buf.read_u32::<BigEndian>()?;
|
||||||
|
let index = buf.read_u16::<BigEndian>()?;
|
||||||
|
let content_type = buf.read_u16::<BigEndian>()?;
|
||||||
|
let content_size = buf.read_u64::<BigEndian>()?;
|
||||||
|
let mut content_hash = [0u8; 20];
|
||||||
|
buf.read_exact(&mut content_hash)?;
|
||||||
|
content_records.push(ContentRecord {
|
||||||
|
content_id,
|
||||||
|
index,
|
||||||
|
content_type,
|
||||||
|
content_size,
|
||||||
|
content_hash,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(TMD {
|
||||||
|
signature_type,
|
||||||
|
signature,
|
||||||
|
padding1,
|
||||||
|
signature_issuer,
|
||||||
|
tmd_version,
|
||||||
|
ca_crl_version,
|
||||||
|
signer_crl_version,
|
||||||
|
is_vwii,
|
||||||
|
ios_tid,
|
||||||
|
title_id,
|
||||||
|
title_type,
|
||||||
|
group_id,
|
||||||
|
padding2,
|
||||||
|
region,
|
||||||
|
ratings,
|
||||||
|
reserved1,
|
||||||
|
ipc_mask,
|
||||||
|
reserved2,
|
||||||
|
access_rights,
|
||||||
|
title_version,
|
||||||
|
num_contents,
|
||||||
|
boot_index,
|
||||||
|
minor_version,
|
||||||
|
content_records,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_vec(&self) -> Result<Vec<u8>, std::io::Error> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
buf.write_u32::<BigEndian>(self.signature_type)?;
|
||||||
|
buf.write_all(&self.signature)?;
|
||||||
|
buf.write_all(&self.padding1)?;
|
||||||
|
buf.write_all(&self.signature_issuer)?;
|
||||||
|
buf.write_u8(self.tmd_version)?;
|
||||||
|
buf.write_u8(self.ca_crl_version)?;
|
||||||
|
buf.write_u8(self.signer_crl_version)?;
|
||||||
|
buf.write_u8(self.is_vwii)?;
|
||||||
|
buf.write_all(&self.ios_tid)?;
|
||||||
|
buf.write_all(&self.title_id)?;
|
||||||
|
buf.write_all(&self.title_type)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.group_id)?;
|
||||||
|
buf.write_all(&self.padding2)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.region)?;
|
||||||
|
buf.write_all(&self.ratings)?;
|
||||||
|
buf.write_all(&self.reserved1)?;
|
||||||
|
buf.write_all(&self.ipc_mask)?;
|
||||||
|
buf.write_all(&self.reserved2)?;
|
||||||
|
buf.write_u32::<BigEndian>(self.access_rights)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.title_version)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.num_contents)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.boot_index)?;
|
||||||
|
buf.write_u16::<BigEndian>(self.minor_version)?;
|
||||||
|
// Iterate over content records and write out content record data.
|
||||||
|
for content in &self.content_records {
|
||||||
|
buf.write_u32::<BigEndian>(content.content_id)?;
|
||||||
|
buf.write_u16::<BigEndian>(content.index)?;
|
||||||
|
buf.write_u16::<BigEndian>(content.content_type)?;
|
||||||
|
buf.write_u64::<BigEndian>(content.content_size)?;
|
||||||
|
buf.write_all(&content.content_hash)?;
|
||||||
|
}
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn title_version(&self) -> u16 {
|
||||||
|
self.title_version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_load_tmd() {
|
||||||
|
let data = fs::read("title.tmd").unwrap();
|
||||||
|
let tmd = TMD::from_bytes(&data).unwrap();
|
||||||
|
assert_eq!(tmd.tmd_version, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user