Skip to content

Commit 3fd701c

Browse files
committed
Ported wad convert command from WiiPy
1 parent 97fe838 commit 3fd701c

7 files changed

Lines changed: 169 additions & 18 deletions

File tree

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,4 @@ sha1 = { version = "0", features = ["oid"]}
3333
glob = "0"
3434
regex = "1"
3535
clap = { version = "4", features = ["derive"] }
36+
anyhow = "1"

src/bin/rustii/info.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,13 @@ fn print_tmd_info(tmd: tmd::TMD, cert: Option<cert::Certificate>) {
9292
}
9393
},
9494
},
95-
Err(_) => "Invalid (Modified TMD)"
95+
Err(_) => {
96+
if tmd.is_fakesigned() {
97+
"Fakesigned"
98+
} else {
99+
"Invalid (Modified TMD)"
100+
}
101+
}
96102
};
97103
println!(" Signature: {}", signing_str);
98104
} else {
@@ -120,12 +126,11 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) {
120126
} else {
121127
println!(" Title ID: {}", hex::encode(ticket.title_id).to_uppercase());
122128
}
123-
if hex::encode(ticket.title_id)[..8].eq("00000001") {
124-
if hex::encode(ticket.title_id).eq("0000000100000001") {
125-
println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version);
126-
} else {
127-
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());
128-
}
129+
let converted_ver = versions::dec_to_standard(ticket.title_version, &hex::encode(ticket.title_id), None);
130+
if hex::encode(ticket.title_id).eq("0000000100000001") {
131+
println!(" Title Version: {} (boot2v{})", ticket.title_version, ticket.title_version);
132+
} else if hex::encode(ticket.title_id)[..8].eq("00000001") && converted_ver.is_some() {
133+
println!(" Title Version: {} ({})", ticket.title_version, converted_ver.unwrap());
129134
} else {
130135
println!(" Title Version: {}", ticket.title_version);
131136
}
@@ -167,7 +172,13 @@ fn print_ticket_info(ticket: ticket::Ticket, cert: Option<cert::Certificate>) {
167172
}
168173
},
169174
},
170-
Err(_) => "Invalid (Modified Ticket)"
175+
Err(_) => {
176+
if ticket.is_fakesigned() {
177+
"Fakesigned"
178+
} else {
179+
"Invalid (Modified Ticket)"
180+
}
181+
}
171182
};
172183
println!(" Signature: {}", signing_str);
173184
} else {
@@ -214,7 +225,13 @@ fn print_wad_info(wad: wad::WAD) {
214225
}
215226
},
216227
},
217-
Err(_) => "Illegitimate (Modified TMD + Ticket)"
228+
Err(_) => {
229+
if title.is_fakesigned() {
230+
"Fakesigned"
231+
} else {
232+
"Illegitimate (Modified TMD + Ticket)"
233+
}
234+
}
218235
};
219236
println!(" Signing Status: {}", signing_str);
220237
println!();

src/bin/rustii/main.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod title;
77
mod filetypes;
88
mod info;
99

10+
use anyhow::Result;
1011
use clap::{Subcommand, Parser};
1112
use title::{wad, fakesign};
1213

@@ -40,14 +41,14 @@ enum Commands {
4041
}
4142
}
4243

43-
fn main() {
44+
fn main() -> Result<()> {
4445
let cli = Cli::parse();
4546

4647
match &cli.command {
4748
Some(Commands::Wad { command }) => {
4849
match command {
49-
Some(wad::Commands::Convert { input, output }) => {
50-
wad::convert_wad(input, output)
50+
Some(wad::Commands::Convert { input, target, output }) => {
51+
wad::convert_wad(input, target, output)?
5152
},
5253
Some(wad::Commands::Pack { input, output}) => {
5354
wad::pack_wad(input, output)
@@ -66,4 +67,5 @@ fn main() {
6667
}
6768
None => {}
6869
}
70+
Ok(())
6971
}

src/bin/rustii/title/wad.rs

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
//
44
// Code for WAD-related commands in the rustii CLI.
55

6-
use std::{str, fs};
6+
use std::{str, fs, fmt};
77
use std::path::{Path, PathBuf};
8+
use anyhow::{bail, Context, Result};
89
use clap::{Subcommand, Args};
910
use glob::glob;
10-
use rustii::title::{cert, tmd, ticket, content, wad};
11+
use rustii::title::{cert, crypto, tmd, ticket, content, wad};
1112
use rustii::title;
1213

1314
#[derive(Subcommand)]
@@ -20,6 +21,8 @@ pub enum Commands {
2021
/// An (optional) WAD name; defaults to <input name>_<new type>.wad
2122
#[arg(short, long)]
2223
output: Option<String>,
24+
#[command(flatten)]
25+
target: ConvertTargets,
2326
},
2427
/// Pack a directory into a WAD file
2528
Pack {
@@ -38,13 +41,103 @@ pub enum Commands {
3841
}
3942

4043
#[derive(Args)]
44+
#[clap(next_help_heading = "Encryption Targets")]
4145
#[group(multiple = false, required = true)]
42-
struct ConvertTargets {
43-
46+
pub struct ConvertTargets {
47+
/// Use the retail common key, allowing this WAD to be installed on retail consoles and Dolphin
48+
#[arg(long)]
49+
retail: bool,
50+
/// Use the development common key, allowing this WAD to be installed on development consoles
51+
#[arg(long)]
52+
dev: bool,
53+
/// Use the vWii key, allowing this WAD to theoretically be installed from Wii U mode if a Wii U mode WAD installer is created
54+
#[arg(long)]
55+
vwii: bool,
4456
}
4557

46-
pub fn convert_wad(input: &str, output: &Option<String>) {
47-
todo!();
58+
enum Target {
59+
Retail,
60+
Dev,
61+
Vwii,
62+
}
63+
64+
impl fmt::Display for Target {
65+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
66+
match self {
67+
Target::Retail => write!(f, "retail"),
68+
Target::Dev => write!(f, "development"),
69+
Target::Vwii => write!(f, "vWii"),
70+
}
71+
}
72+
}
73+
74+
pub fn convert_wad(input: &str, target: &ConvertTargets, output: &Option<String>) -> Result<()> {
75+
let in_path = Path::new(input);
76+
if !in_path.exists() {
77+
bail!("Source WAD \"{}\" could not be found.", input);
78+
}
79+
// Parse the target passed to identify the encryption target.
80+
let target = if target.dev {
81+
Target::Dev
82+
} else if target.vwii {
83+
Target::Vwii
84+
} else {
85+
Target::Retail
86+
};
87+
// Get the output name now that we know the target, if one wasn't passed.
88+
let out_path = if output.is_some() {
89+
PathBuf::from(output.clone().unwrap()).with_extension("wad")
90+
} else {
91+
match target {
92+
Target::Retail => PathBuf::from(format!("{}_retail", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
93+
Target::Dev => PathBuf::from(format!("{}_dev", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
94+
Target::Vwii => PathBuf::from(format!("{}_vWii", in_path.file_stem().unwrap().to_str().unwrap())).with_extension("wad"),
95+
}
96+
};
97+
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.")?;
98+
// Bail if the WAD is already using the selected encryption.
99+
if matches!(target, Target::Dev) && title.ticket.is_dev() {
100+
bail!("This is already a development WAD!");
101+
} else if matches!(target, Target::Retail) && !title.ticket.is_dev() && !title.tmd.is_vwii() {
102+
bail!("This is already a retail WAD!");
103+
} else if matches!(target, Target::Vwii) && !title.ticket.is_dev() && title.tmd.is_vwii() {
104+
bail!("This is already a vWii WAD!");
105+
}
106+
// Save the current encryption to display at the end.
107+
let source = if title.ticket.is_dev() {
108+
"development"
109+
} else if title.tmd.is_vwii() {
110+
"vWii"
111+
} else {
112+
"retail"
113+
};
114+
let title_key = title.ticket.dec_title_key();
115+
let title_key_new: [u8; 16];
116+
match target {
117+
Target::Dev => {
118+
title.tmd.set_signature_issuer(String::from("Root-CA00000002-CP00000007"))?;
119+
title.ticket.set_signature_issuer(String::from("Root-CA00000002-XS00000006"))?;
120+
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id, Some(true));
121+
title.ticket.common_key_index = 0;
122+
},
123+
Target::Retail => {
124+
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
125+
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
126+
title_key_new = crypto::encrypt_title_key(title_key, 0, title.ticket.title_id, Some(false));
127+
title.ticket.common_key_index = 0;
128+
},
129+
Target::Vwii => {
130+
title.tmd.set_signature_issuer(String::from("Root-CA00000001-CP00000004"))?;
131+
title.ticket.set_signature_issuer(String::from("Root-CA00000001-XS00000003"))?;
132+
title_key_new = crypto::encrypt_title_key(title_key, 2, title.ticket.title_id, Some(false));
133+
title.ticket.common_key_index = 2;
134+
}
135+
}
136+
title.ticket.title_key = title_key_new;
137+
title.fakesign()?;
138+
fs::write(out_path.clone(), title.to_wad()?.to_bytes()?)?;
139+
println!("Successfully converted {} WAD to {} WAD \"{}\"!", source, target, out_path.file_name().unwrap().to_str().unwrap());
140+
Ok(())
48141
}
49142

50143
pub fn pack_wad(input: &str, output: &str) {

src/title/ticket.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::title::crypto::decrypt_title_key;
1414
pub enum TicketError {
1515
UnsupportedVersion,
1616
CannotFakesign,
17+
IssuerTooLong,
1718
IOError(std::io::Error),
1819
}
1920

@@ -22,6 +23,7 @@ impl fmt::Display for TicketError {
2223
let description = match *self {
2324
TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).",
2425
TicketError::CannotFakesign => "The Ticket data could not be fakesigned.",
26+
TicketError::IssuerTooLong => "Signature issuer length must not exceed 64 characers.",
2527
TicketError::IOError(_) => "The provided Ticket data was invalid.",
2628
};
2729
f.write_str(description)
@@ -232,4 +234,15 @@ impl Ticket {
232234
pub fn signature_issuer(&self) -> String {
233235
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
234236
}
237+
238+
/// Sets a new name for the certificate used to sign a Ticket.
239+
pub fn set_signature_issuer(&mut self, signature_issuer: String) -> Result<(), TicketError> {
240+
if signature_issuer.len() > 64 {
241+
return Err(TicketError::IssuerTooLong);
242+
}
243+
let mut issuer = signature_issuer.into_bytes();
244+
issuer.resize(64, 0);
245+
self.signature_issuer = issuer.try_into().unwrap();
246+
Ok(())
247+
}
235248
}

src/title/tmd.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use sha1::{Sha1, Digest};
1313
#[derive(Debug)]
1414
pub enum TMDError {
1515
CannotFakesign,
16+
IssuerTooLong,
1617
InvalidContentType(u16),
1718
IOError(std::io::Error),
1819
}
@@ -21,6 +22,7 @@ impl fmt::Display for TMDError {
2122
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
2223
let description = match *self {
2324
TMDError::CannotFakesign => "The TMD data could not be fakesigned.",
25+
TMDError::IssuerTooLong => "Signature issuer length must not exceed 64 characters.",
2426
TMDError::InvalidContentType(_) => "The TMD contains content with an invalid type.",
2527
TMDError::IOError(_) => "The provided TMD data was invalid.",
2628
};
@@ -350,4 +352,20 @@ impl TMD {
350352
pub fn signature_issuer(&self) -> String {
351353
String::from_utf8_lossy(&self.signature_issuer).trim_end_matches('\0').to_owned()
352354
}
355+
356+
/// Sets a new name for the certificate used to sign a TMD.
357+
pub fn set_signature_issuer(&mut self, signature_issuer: String) -> Result<(), TMDError> {
358+
if signature_issuer.len() > 64 {
359+
return Err(TMDError::IssuerTooLong);
360+
}
361+
let mut issuer = signature_issuer.into_bytes();
362+
issuer.resize(64, 0);
363+
self.signature_issuer = issuer.try_into().unwrap();
364+
Ok(())
365+
}
366+
367+
/// Gets whether this TMD describes a vWii title or not.
368+
pub fn is_vwii(&self) -> bool {
369+
self.is_vwii == 1
370+
}
353371
}

0 commit comments

Comments
 (0)