Ported wad convert command from WiiPy

This commit is contained in:
Campbell 2025-04-02 19:51:19 -04:00
parent 97fe838b8c
commit 3fd701cac6
Signed by: NinjaCheetah
GPG Key ID: 39C2500E1778B156
7 changed files with 169 additions and 18 deletions

7
Cargo.lock generated
View File

@ -72,6 +72,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "anyhow"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.4.0" version = "1.4.0"
@ -502,6 +508,7 @@ name = "rustii"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"aes", "aes",
"anyhow",
"byteorder", "byteorder",
"cbc", "cbc",
"clap", "clap",

View File

@ -33,3 +33,4 @@ sha1 = { version = "0", features = ["oid"]}
glob = "0" glob = "0"
regex = "1" regex = "1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
anyhow = "1"

View File

@ -92,7 +92,13 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) {
} }
}, },
}, },
Err(_) => "Invalid (Modified TMD)" Err(_) => {
if tmd.is_fakesigned() {
"Fakesigned"
} else {
"Invalid (Modified TMD)"
}
}
}; };
println!(" Signature: {}", signing_str); println!(" Signature: {}", signing_str);
} else { } else {
@ -120,12 +126,11 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) {
} else { } else {
println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase()); println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase());
} }
if hex::encode(ticket.title_id)[..8].eq("00000001") { let converted_ver = versions::dec_to_standard(ticket.title_version, &hex::encode(ticket.title_id), None);
if hex::encode(ticket.title_id).eq("0000000100000001") { if hex::encode(ticket.title_id).eq("0000000100000001") {
println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version); println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version);
} else { } else if hex::encode(ticket.title_id)[..8].eq("00000001") && converted_ver.is_some() {
println!(" Title Version: {} ({})", ticket.title_version, versions::dec_to_standard(ticket.title_version, &hex::encode(ticket.title_id), Some(ticket.common_key_index == 2)).unwrap()); println!(" Title Version: {} ({})", ticket.title_version, converted_ver.unwrap());
}
} else { } else {
println!(" Title Version: {}", ticket.title_version); println!(" Title Version: {}", ticket.title_version);
} }
@ -167,7 +172,13 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) {
} }
}, },
}, },
Err(_) => "Invalid (Modified Ticket)" Err(_) => {
if ticket.is_fakesigned() {
"Fakesigned"
} else {
"Invalid (Modified Ticket)"
}
}
}; };
println!(" Signature: {}", signing_str); println!(" Signature: {}", signing_str);
} else { } else {
@ -214,7 +225,13 @@ fn print_wad_info(wad: wad::WAD) {
} }
}, },
}, },
Err(_) => "Illegitimate (Modified TMD + Ticket)" Err(_) => {
if title.is_fakesigned() {
"Fakesigned"
} else {
"Illegitimate (Modified TMD + Ticket)"
}
}
}; };
println!(" Signing Status: {}", signing_str); println!(" Signing Status: {}", signing_str);
println!(); println!();

View File

@ -7,6 +7,7 @@ mod title;
mod filetypes; mod filetypes;
mod info; mod info;
use anyhow::Result;
use clap::{Subcommand, Parser}; use clap::{Subcommand, Parser};
use title::{wad, fakesign}; use title::{wad, fakesign};
@ -40,14 +41,14 @@ enum Commands {
} }
} }
fn main() { fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
match &cli.command { match &cli.command {
Some(Commands::Wad { command }) => { Some(Commands::Wad { command }) => {
match command { match command {
Some(wad::Commands::Convert { input, output }) => { Some(wad::Commands::Convert { input, target, output }) => {
wad::convert_wad(input, output) wad::convert_wad(input, target, output)?
}, },
Some(wad::Commands::Pack { input, output}) => { Some(wad::Commands::Pack { input, output}) => {
wad::pack_wad(input, output) wad::pack_wad(input, output)
@ -66,4 +67,5 @@ fn main() {
} }
None => {} None => {}
} }
Ok(())
} }

View File

@ -3,11 +3,12 @@
// //
// Code for WAD-related commands in the rustii CLI. // Code for WAD-related commands in the rustii CLI.
use std::{str, fs}; use std::{str, fs, fmt};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
use clap::{Subcommand, Args}; use clap::{Subcommand, Args};
use glob::glob; use glob::glob;
use rustii::title::{cert, tmd, ticket, content, wad}; use rustii::title::{cert, crypto, tmd, ticket, content, wad};
use rustii::title; use rustii::title;
#[derive(Subcommand)] #[derive(Subcommand)]
@ -20,6 +21,8 @@ pub enum Commands {
/// An (optional) WAD name; defaults to <input name>_<new type>.wad /// An (optional) WAD name; defaults to <input name>_<new type>.wad
#[arg(short, long)] #[arg(short, long)]
output: Option<String>, output: Option<String>,
#[command(flatten)]
target: ConvertTargets,
}, },
/// Pack a directory into a WAD file /// Pack a directory into a WAD file
Pack { Pack {
@ -38,13 +41,103 @@ pub enum Commands {
} }
#[derive(Args)] #[derive(Args)]
#[clap(next_help_heading = "Encryption Targets")]
#[group(multiple = false, required = true)] #[group(multiple = false, required = true)]
struct ConvertTargets { pub struct ConvertTargets {
/// Use the retail common key, allowing this WAD to be installed on retail consoles and Dolphin
#[arg(long)]
retail: bool,
/// Use the development common key, allowing this WAD to be installed on development consoles
#[arg(long)]
dev: bool,
/// Use the vWii key, allowing this WAD to theoretically be installed from Wii U mode if a Wii U mode WAD installer is created
#[arg(long)]
vwii: bool,
} }
pub fn convert_wad(input: &str, output: &Option<String>) { enum Target {
todo!(); Retail,
Dev,
Vwii,
}
impl fmt::Display for Target {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Target::Retail => write!(f, "retail"),
Target::Dev => write!(f, "development"),
Target::Vwii => write!(f, "vWii"),
}
}
}
pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>) -> Result<()> {
let in_path = Path::new(input);
if !in_path.exists() {
bail!("Source WAD \"{}\" could not be found.", input);
}
// Parse the target passed to identify the encryption target.
let target = if target.dev {
Target::Dev
} else if target.vwii {
Target::Vwii
} else {
Target::Retail
};
// Get the output name now that we know the target, if one wasn't passed.
let out_path = if output.is_some() {
PathBuf::from(output.clone().unwrap()).with_extension("wad")
} else {
match target {
Target::Retail => PathBuf::from(format!("{}_retail", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
Target::Dev => PathBuf::from(format!("{}_dev", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
Target::Vwii => PathBuf::from(format!("{}_vWii", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
}
};
let mut title = title::Title::from_bytes(fs::read(in_path)?.as_slice()).with_context(|| "The provided WAD file could not be loaded, and is likely invalid.")?;
// Bail if the WAD is already using the selected encryption.
if matches!(target, Target::Dev) && title.ticket.is_dev() {
bail!("This is already a development WAD!");
} else if matches!(target, Target::Retail) && !title.ticket.is_dev() && !title.tmd.is_vwii() {
bail!("This is already a retail WAD!");
} else if matches!(target, Target::Vwii) && !title.ticket.is_dev() && title.tmd.is_vwii() {
bail!("This is already a vWii WAD!");
}
// Save the current encryption to display at the end.
let source = if title.ticket.is_dev() {
"development"
} else if title.tmd.is_vwii() {
"vWii"
} else {
"retail"
};
let title_key = title.ticket.dec_title_key();
let title_key_new: [u8; 16];
match target {
Target::Dev => {
title.tmd.set_signature_issuer(String::from("Root-CA00000002-CP00000007"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000002-XS00000006"))?;
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id, Some(true));
title.ticket.common_key_index = 0;
},
Target::Retail => {
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id, Some(false));
title.ticket.common_key_index = 0;
},
Target::Vwii => {
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
title_key_new = crypto::encrypt_title_key(title_key, 2, title.ticket.title_id, Some(false));
title.ticket.common_key_index = 2;
}
}
title.ticket.title_key = title_key_new;
title.fakesign()?;
fs::write(out_path.clone(), title.to_wad()?.to_bytes()?)?;
println!("Successfully converted {} WAD to {} WAD \"{}\"!", source, target, out_path.file_name().unwrap().to_str().unwrap());
Ok(())
} }
pub fn pack_wad(input: &str, output: &str) { pub fn pack_wad(input: &str, output: &str) {

View File

@ -14,6 +14,7 @@ use crate::title::crypto::decrypt_title_key;
pub enum TicketError { pub enum TicketError {
UnsupportedVersion, UnsupportedVersion,
CannotFakesign, CannotFakesign,
IssuerTooLong,
IOError(std::io::Error), IOError(std::io::Error),
} }
@ -22,6 +23,7 @@ impl fmt::Display for TicketError {
let description = match *self { let description = match *self {
TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).", TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).",
TicketError::CannotFakesign => "The Ticket data could not be fakesigned.", TicketError::CannotFakesign => "The Ticket data could not be fakesigned.",
TicketError::IssuerTooLong => "Signature issuer length must not exceed 64 characers.",
TicketError::IOError(_) => "The provided Ticket data was invalid.", TicketError::IOError(_) => "The provided Ticket data was invalid.",
}; };
f.write_str(description) f.write_str(description)
@ -232,4 +234,15 @@ impl Ticket {
pub fn signature_issuer(&self) -> String { pub fn signature_issuer(&self) -> String {
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned() String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
} }
/// Sets a new name for the certificate used to sign a Ticket.
pub fn set_signature_issuer(&mut self, signature_issuer: String) -> Result<(), TicketError> {
if signature_issuer.len() > 64 {
return Err(TicketError::IssuerTooLong);
}
let mut issuer = signature_issuer.into_bytes();
issuer.resize(64, 0);
self.signature_issuer = issuer.try_into().unwrap();
Ok(())
}
} }

View File

@ -13,6 +13,7 @@ use sha1::{Sha1, Digest};
#[derive(Debug)] #[derive(Debug)]
pub enum TMDError { pub enum TMDError {
CannotFakesign, CannotFakesign,
IssuerTooLong,
InvalidContentType(u16), InvalidContentType(u16),
IOError(std::io::Error), IOError(std::io::Error),
} }
@ -21,6 +22,7 @@ impl fmt::Display for TMDError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let description = match *self { let description = match *self {
TMDError::CannotFakesign => "The TMD data could not be fakesigned.", TMDError::CannotFakesign => "The TMD data could not be fakesigned.",
TMDError::IssuerTooLong => "Signature issuer length must not exceed 64 characters.",
TMDError::InvalidContentType(_) => "The TMD contains content with an invalid type.", TMDError::InvalidContentType(_) => "The TMD contains content with an invalid type.",
TMDError::IOError(_) => "The provided TMD data was invalid.", TMDError::IOError(_) => "The provided TMD data was invalid.",
}; };
@ -350,4 +352,20 @@ impl TMD {
pub fn signature_issuer(&self) -> String { pub fn signature_issuer(&self) -> String {
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned() String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
} }
/// Sets a new name for the certificate used to sign a TMD.
pub fn set_signature_issuer(&mut self, signature_issuer: String) -> Result<(), TMDError> {
if signature_issuer.len() > 64 {
return Err(TMDError::IssuerTooLong);
}
let mut issuer = signature_issuer.into_bytes();
issuer.resize(64, 0);
self.signature_issuer = issuer.try_into().unwrap();
Ok(())
}
/// Gets whether this TMD describes a vWii title or not.
pub fn is_vwii(&self) -> bool {
self.is_vwii == 1
}
} }