Compare commits

..

No commits in common. "1bcc004af760e06727bcfaca5641e61aec073f0d" and "5731b8d4d8472f4d3f9f139a10a3c22ef4ef0f39" have entirely different histories.

5 changed files with 34 additions and 81 deletions

View File

@ -1,25 +1,17 @@
![rustii-banner](https://github.com/user-attachments/assets/08a7eea1-837e-4bce-939e-13c720b35226)
# rustii
*Like rusty but it's rustii because the Wii? Get it?*
rustii is a library and command line tool written in Rust for handling the various files and formats found on the Wii. rustii is a port of my other library, [libWiiPy](https://github.com/NinjaCheetah/libWiiPy), which aims to accomplish the same goal in Python. Compared to libWiiPy, rustii is in its very early stages of development and is missing most of the features present in its Python counterpart. The goal is for rustii and libWiiPy to eventually have feature parity, with the rustii CLI acting as a drop-in replacement for the (comparatively much less efficient) [WiiPy](https://github.com/NinjaCheetah/WiiPy) CLI.
I'm still very new to Rust, so pardon any messy code or confusing API decisions you may find. libWiiPy started off like that, too.
A very WIP and experimental port of [libWiiPy](https://github.com/NinjaCheetah/libWiiPy) to Rust.
### What's Included
- Structs for TMDs and Tickets that can be created from binary data
- Simple Title Key encryption/decryption
- Content encryption/decryption
- WAD parsing (allowing for packing/unpacking)
- A basic CLI that uses the above features to allow for packing/unpacking WADs
- A very basic test binary that makes sure these things work as expected
### What's Not Included
- Basically anything else. Any other features present in libWiiPy not listed here either do not yet exist, or are in an experimental state.
- Basically anything else. This library doesn't do a whole lot yet, and the included `rustii` binary isn't even remotely similar to WiiPy yet. Eventually it aims to be a more optimized replacement for it.
## Building
rustii is a standard Rust package. You'll need to have [Rust installed](https://www.rust-lang.org/learn/get-started), and then you can simply run:
```
cargo build --release
```
to compile the rustii library and CLI. The CLI can then be found at `target/release/rustii(.exe)`.
Please note that my Rust code isn't great, I'm still very new to it.

View File

@ -1,6 +0,0 @@
// title/cert.rs from rustii (c) 2025 NinjaCheetah & Contributors
// https://github.com/NinjaCheetah/rustii
//
// Implements the structures and methods required for validating the signatures of Wii titles.

View File

@ -15,20 +15,20 @@ fn title_id_to_iv(title_id: [u8; 8]) -> [u8; 16] {
}
// 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], is_dev: Option<bool>) -> [u8; 16] {
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, is_dev).into(), &iv.into());
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], is_dev: Option<bool>) -> [u8; 16] {
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, is_dev).into(), &iv.into());
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

View File

@ -9,7 +9,6 @@ pub mod crypto;
pub mod ticket;
pub mod tmd;
pub mod wad;
mod cert;
use std::error::Error;
use std::fmt;

View File

@ -3,30 +3,10 @@
//
// Implements the structures and methods required for Ticket parsing and editing.
use std::error::Error;
use std::fmt;
use std::io::{Cursor, Read, Write};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use crate::title::crypto::decrypt_title_key;
#[derive(Debug)]
pub enum TicketError {
UnsupportedVersion,
IOError(std::io::Error),
}
impl fmt::Display for TicketError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let description = match *self {
TicketError::UnsupportedVersion => "The provided Ticket is not a supported version (only v0 is supported).",
TicketError::IOError(_) => "The provided Ticket data was invalid.",
};
f.write_str(description)
}
}
impl Error for TicketError {}
#[derive(Debug)]
#[derive(Copy)]
#[derive(Clone)]
@ -64,55 +44,51 @@ pub struct Ticket {
}
impl Ticket {
pub fn from_bytes(data: &[u8]) -> Result<Self, TicketError> {
pub fn from_bytes(data: &[u8]) -> Result<Self, std::io::Error> {
let mut buf = Cursor::new(data);
let signature_type = buf.read_u32::<BigEndian>().map_err(TicketError::IOError)?;
let signature_type = buf.read_u32::<BigEndian>()?;
let mut signature = [0u8; 256];
buf.read_exact(&mut signature).map_err(TicketError::IOError)?;
buf.read_exact(&mut signature)?;
// Maybe this can be read differently?
let mut padding1 = [0u8; 60];
buf.read_exact(&mut padding1).map_err(TicketError::IOError)?;
buf.read_exact(&mut padding1)?;
let mut signature_issuer = [0u8; 64];
buf.read_exact(&mut signature_issuer).map_err(TicketError::IOError)?;
buf.read_exact(&mut signature_issuer)?;
let mut ecdh_data = [0u8; 60];
buf.read_exact(&mut ecdh_data).map_err(TicketError::IOError)?;
let ticket_version = buf.read_u8().map_err(TicketError::IOError)?;
// v1 Tickets are NOT supported (just like in libWiiPy).
if ticket_version != 0 {
return Err(TicketError::UnsupportedVersion);
}
buf.read_exact(&mut ecdh_data)?;
let ticket_version = buf.read_u8()?;
let mut reserved1 = [0u8; 2];
buf.read_exact(&mut reserved1).map_err(TicketError::IOError)?;
buf.read_exact(&mut reserved1)?;
let mut title_key = [0u8; 16];
buf.read_exact(&mut title_key).map_err(TicketError::IOError)?;
buf.read_exact(&mut title_key)?;
let mut unknown1 = [0u8; 1];
buf.read_exact(&mut unknown1).map_err(TicketError::IOError)?;
buf.read_exact(&mut unknown1)?;
let mut ticket_id = [0u8; 8];
buf.read_exact(&mut ticket_id).map_err(TicketError::IOError)?;
buf.read_exact(&mut ticket_id)?;
let mut console_id = [0u8; 4];
buf.read_exact(&mut console_id).map_err(TicketError::IOError)?;
buf.read_exact(&mut console_id)?;
let mut title_id = [0u8; 8];
buf.read_exact(&mut title_id).map_err(TicketError::IOError)?;
buf.read_exact(&mut title_id)?;
let mut unknown2 = [0u8; 2];
buf.read_exact(&mut unknown2).map_err(TicketError::IOError)?;
let title_version = buf.read_u16::<BigEndian>().map_err(TicketError::IOError)?;
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).map_err(TicketError::IOError)?;
buf.read_exact(&mut permitted_titles_mask)?;
let mut permit_mask = [0u8; 4];
buf.read_exact(&mut permit_mask).map_err(TicketError::IOError)?;
let title_export_allowed = buf.read_u8().map_err(TicketError::IOError)?;
let common_key_index = buf.read_u8().map_err(TicketError::IOError)?;
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).map_err(TicketError::IOError)?;
buf.read_exact(&mut unknown3)?;
let mut content_access_permission = [0u8; 64];
buf.read_exact(&mut content_access_permission).map_err(TicketError::IOError)?;
buf.read_exact(&mut content_access_permission)?;
let mut padding2 = [0u8; 2];
buf.read_exact(&mut padding2).map_err(TicketError::IOError)?;
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>().map_err(TicketError::IOError)?;
let limit_max = buf.read_u32::<BigEndian>().map_err(TicketError::IOError)?;
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();
@ -174,14 +150,6 @@ impl Ticket {
}
pub fn dec_title_key(&self) -> [u8; 16] {
// Get the dev status of this Ticket so decrypt_title_key knows the right common key.
let is_dev = self.is_dev();
decrypt_title_key(self.title_key, self.common_key_index, self.title_id, Some(is_dev))
}
pub fn is_dev(&self) -> bool {
// Parse the signature issuer to determine if this is a dev Ticket or not.
let issuer_str = String::from_utf8(Vec::from(&self.signature_issuer)).unwrap_or_default();
issuer_str.contains("Root-CA00000002-XS00000004") || issuer_str.contains("Root-CA00000002-XS00000006")
decrypt_title_key(self.title_key, self.common_key_index, self.title_id)
}
}