diff --git a/Cargo.lock b/Cargo.lock index c0a94f9..f742def 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,6 +72,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + [[package]] name = "autocfg" version = "1.4.0" @@ -502,6 +508,7 @@ name = "rustii" version = "0.1.0" dependencies = [ "aes", + "anyhow", "byteorder", "cbc", "clap", diff --git a/Cargo.toml b/Cargo.toml index 6b53d8d..2d38ed6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,3 +33,4 @@ sha1 = { version = "0", features = ["oid"]} glob = "0" regex = "1" clap = { version = "4", features = ["derive"] } +anyhow = "1" diff --git a/src/bin/rustii/info.rs b/src/bin/rustii/info.rs index a863258..e6ad5d2 100644 --- a/src/bin/rustii/info.rs +++ b/src/bin/rustii/info.rs @@ -92,7 +92,13 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option) { } }, }, - Err(_) => "Invalid (Modified TMD)" + Err(_) => { + if tmd.is_fakesigned() { + "Fakesigned" + } else { + "Invalid (Modified TMD)" + } + } }; println!(" Signature: {}", signing_str); } else { @@ -120,12 +126,11 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option) { } else { println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase()); } - if hex::encode(ticket.title_id)[..8].eq("00000001") { - if hex::encode(ticket.title_id).eq("0000000100000001") { - println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version); - } else { - 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()); - } + let converted_ver = versions::dec_to_standard(ticket.title_version, &hex::encode(ticket.title_id), None); + if hex::encode(ticket.title_id).eq("0000000100000001") { + println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version); + } else if hex::encode(ticket.title_id)[..8].eq("00000001") && converted_ver.is_some() { + println!(" Title Version: {} ({})", ticket.title_version, converted_ver.unwrap()); } else { println!(" Title Version: {}", ticket.title_version); } @@ -167,7 +172,13 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option) { } }, }, - Err(_) => "Invalid (Modified Ticket)" + Err(_) => { + if ticket.is_fakesigned() { + "Fakesigned" + } else { + "Invalid (Modified Ticket)" + } + } }; println!(" Signature: {}", signing_str); } 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!(); diff --git a/src/bin/rustii/main.rs b/src/bin/rustii/main.rs index 638447b..badccb4 100644 --- a/src/bin/rustii/main.rs +++ b/src/bin/rustii/main.rs @@ -7,6 +7,7 @@ mod title; mod filetypes; mod info; +use anyhow::Result; use clap::{Subcommand, Parser}; use title::{wad, fakesign}; @@ -40,14 +41,14 @@ enum Commands { } } -fn main() { +fn main() -> Result<()> { let cli = Cli::parse(); match &cli.command { Some(Commands::Wad { command }) => { match command { - Some(wad::Commands::Convert { input, output }) => { - wad::convert_wad(input, output) + Some(wad::Commands::Convert { input, target, output }) => { + wad::convert_wad(input, target, output)? }, Some(wad::Commands::Pack { input, output}) => { wad::pack_wad(input, output) @@ -66,4 +67,5 @@ fn main() { } None => {} } + Ok(()) } diff --git a/src/bin/rustii/title/wad.rs b/src/bin/rustii/title/wad.rs index 6675d75..620d769 100644 --- a/src/bin/rustii/title/wad.rs +++ b/src/bin/rustii/title/wad.rs @@ -3,11 +3,12 @@ // // Code for WAD-related commands in the rustii CLI. -use std::{str, fs}; +use std::{str, fs, fmt}; use std::path::{Path, PathBuf}; +use anyhow::{bail, Context, Result}; use clap::{Subcommand, Args}; use glob::glob; -use rustii::title::{cert, tmd, ticket, content, wad}; +use rustii::title::{cert, crypto, tmd, ticket, content, wad}; use rustii::title; #[derive(Subcommand)] @@ -20,6 +21,8 @@ pub enum Commands { /// An (optional) WAD name; defaults to _.wad #[arg(short, long)] output: Option, + #[command(flatten)] + target: ConvertTargets, }, /// Pack a directory into a WAD file Pack { @@ -38,13 +41,103 @@ pub enum Commands { } #[derive(Args)] +#[clap(next_help_heading = "Encryption Targets")] #[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) { - todo!(); +enum Target { + 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) -> 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) { diff --git a/src/title/ticket.rs b/src/title/ticket.rs index f489f73..dea23ea 100644 --- a/src/title/ticket.rs +++ b/src/title/ticket.rs @@ -14,6 +14,7 @@ use crate::title::crypto::decrypt_title_key; pub enum TicketError { UnsupportedVersion, CannotFakesign, + IssuerTooLong, IOError(std::io::Error), } @@ -22,6 +23,7 @@ impl fmt::Display for TicketError { let description = match *self { TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).", 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.", }; f.write_str(description) @@ -232,4 +234,15 @@ impl Ticket { pub fn signature_issuer(&self) -> String { 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(()) + } } diff --git a/src/title/tmd.rs b/src/title/tmd.rs index 4be766e..6c93d45 100644 --- a/src/title/tmd.rs +++ b/src/title/tmd.rs @@ -13,6 +13,7 @@ use sha1::{Sha1, Digest}; #[derive(Debug)] pub enum TMDError { CannotFakesign, + IssuerTooLong, InvalidContentType(u16), IOError(std::io::Error), } @@ -21,6 +22,7 @@ impl fmt::Display for TMDError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let description = match *self { 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::IOError(_) => "The provided TMD data was invalid.", }; @@ -350,4 +352,20 @@ impl TMD { pub fn signature_issuer(&self) -> String { 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 + } }