mirror of
https://github.com/NinjaCheetah/rustii.git
synced 2026-03-17 06:47:49 -04:00
Ported all NUS download functions from libWiiPy and corresponding CLI commands
Also adds the basics of U8 archive packing/unpacking, however they are not in a usable state yet and there are no working CLI commands associated with them.
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use sha1::{Sha1, Digest};
|
||||
use thiserror::Error;
|
||||
use crate::title::content::ContentError::MissingContents;
|
||||
use crate::title::tmd::ContentRecord;
|
||||
use crate::title::crypto;
|
||||
|
||||
@@ -13,6 +14,8 @@ use crate::title::crypto;
|
||||
pub enum ContentError {
|
||||
#[error("requested index {index} is out of range (must not exceed {max})")]
|
||||
IndexOutOfRange { index: usize, max: usize },
|
||||
#[error("expected {required} contents based on content records but found {found}")]
|
||||
MissingContents { required: usize, found: usize },
|
||||
#[error("content with requested Content ID {0} could not be found")]
|
||||
CIDNotFound(u32),
|
||||
#[error("content's hash did not match the expected value (was {hash}, expected {expected})")]
|
||||
@@ -70,6 +73,19 @@ impl ContentRegion {
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a ContentRegion instance that can be used to parse and edit content stored in a
|
||||
/// digital Wii title from a vector of contents and the ContentRecords from a TMD.
|
||||
pub fn from_contents(contents: Vec<Vec<u8>>, content_records: Vec<ContentRecord>) -> Result<Self, ContentError> {
|
||||
if contents.len() != content_records.len() {
|
||||
return Err(MissingContents { required: content_records.len(), found: contents.len()});
|
||||
}
|
||||
let mut content_region = Self::new(content_records)?;
|
||||
for i in 0..contents.len() {
|
||||
content_region.load_enc_content(&contents[i], content_region.content_records[i].index as usize)?;
|
||||
}
|
||||
Ok(content_region)
|
||||
}
|
||||
|
||||
/// Creates a ContentRegion instance from the ContentRecords of a TMD that contains no actual
|
||||
/// content. This can be used to load existing content from files.
|
||||
@@ -77,9 +93,8 @@ impl ContentRegion {
|
||||
let content_region_size: u64 = content_records.iter().map(|x| (x.content_size + 63) & !63).sum();
|
||||
let content_region_size = content_region_size as u32;
|
||||
let num_contents = content_records.len() as u16;
|
||||
let content_start_offsets: Vec<u64> = Vec::new();
|
||||
let mut contents: Vec<Vec<u8>> = Vec::new();
|
||||
contents.resize(num_contents as usize, Vec::new());
|
||||
let content_start_offsets: Vec<u64> = vec![0; num_contents as usize];
|
||||
let contents: Vec<Vec<u8>> = vec![Vec::new(); num_contents as usize];
|
||||
Ok(ContentRegion {
|
||||
content_records,
|
||||
content_region_size,
|
||||
@@ -143,6 +158,16 @@ impl ContentRegion {
|
||||
Err(ContentError::CIDNotFound(cid))
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||
/// must be encrypted.
|
||||
pub fn load_enc_content(&mut self, content: &[u8], index: usize) -> Result<(), ContentError> {
|
||||
if index >= self.content_records.len() {
|
||||
return Err(ContentError::IndexOutOfRange { index, max: self.content_records.len() - 1 });
|
||||
}
|
||||
self.contents[index] = Vec::from(content);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Loads existing content into the specified index of a ContentRegion instance. This content
|
||||
/// must be decrypted and needs to match the size and hash listed in the content record at that
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod cert;
|
||||
pub mod commonkeys;
|
||||
pub mod content;
|
||||
pub mod crypto;
|
||||
pub mod nus;
|
||||
pub mod ticket;
|
||||
pub mod tmd;
|
||||
pub mod versions;
|
||||
@@ -52,15 +53,37 @@ impl Title {
|
||||
let ticket = ticket::Ticket::from_bytes(&wad.ticket()).map_err(TitleError::Ticket)?;
|
||||
let tmd = tmd::TMD::from_bytes(&wad.tmd()).map_err(TitleError::TMD)?;
|
||||
let content = content::ContentRegion::from_bytes(&wad.content(), tmd.content_records.clone()).map_err(TitleError::Content)?;
|
||||
let title = Title {
|
||||
Ok(Title {
|
||||
cert_chain,
|
||||
crl: wad.crl(),
|
||||
ticket,
|
||||
tmd,
|
||||
content,
|
||||
meta: wad.meta(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new Title instance from all of its individual components.
|
||||
pub fn from_parts(cert_chain: cert::CertificateChain, crl: Option<&[u8]>, ticket: ticket::Ticket, tmd: tmd::TMD,
|
||||
content: content::ContentRegion, meta: Option<&[u8]>) -> Result<Title, TitleError> {
|
||||
// Create empty vecs for the CRL and meta areas if we weren't supplied with any, as they're
|
||||
// optional components.
|
||||
let crl = match crl {
|
||||
Some(crl) => crl.to_vec(),
|
||||
None => Vec::new()
|
||||
};
|
||||
Ok(title)
|
||||
let meta = match meta {
|
||||
Some(meta) => meta.to_vec(),
|
||||
None => Vec::new()
|
||||
};
|
||||
Ok(Title {
|
||||
cert_chain,
|
||||
crl,
|
||||
ticket,
|
||||
tmd,
|
||||
content,
|
||||
meta
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts a Title instance into a WAD, which can be used to export the Title back to a file.
|
||||
|
||||
143
src/title/nus.rs
Normal file
143
src/title/nus.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
// title/nus.rs from rustii (c) 2025 NinjaCheetah & Contributors
|
||||
// https://github.com/NinjaCheetah/rustii
|
||||
//
|
||||
// Implements the functions required for downloading data from the NUS.
|
||||
|
||||
use std::str;
|
||||
use std::io::Write;
|
||||
use reqwest;
|
||||
use thiserror::Error;
|
||||
use crate::title::{cert, tmd, ticket, content};
|
||||
use crate::title;
|
||||
|
||||
use sha1::{Sha1, Digest};
|
||||
|
||||
const WII_NUS_ENDPOINT: &str = "http://nus.cdn.shop.wii.com/ccs/download/";
|
||||
const WII_U_NUS_ENDPOINT: &str = "http://ccs.cdn.wup.shop.nintendo.net/ccs/download/";
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NUSError {
|
||||
#[error("the data returned by the NUS is not valid")]
|
||||
InvalidData,
|
||||
#[error("the requested Title ID or version could not be found on the NUS")]
|
||||
NotFound,
|
||||
#[error("Certificate processing error")]
|
||||
Certificate(#[from] cert::CertificateError),
|
||||
#[error("TMD processing error")]
|
||||
TMD(#[from] tmd::TMDError),
|
||||
#[error("Ticket processing error")]
|
||||
Ticket(#[from] ticket::TicketError),
|
||||
#[error("Content processing error")]
|
||||
Content(#[from] content::ContentError),
|
||||
#[error("an error occurred while assembling a Title from the downloaded data")]
|
||||
Title(#[from] title::TitleError),
|
||||
#[error("data could not be downloaded from the NUS")]
|
||||
Request(#[from] reqwest::Error),
|
||||
#[error("an error occurred writing NUS data")]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Downloads the retail certificate chain from the NUS.
|
||||
pub fn download_cert_chain(wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||
// To build the certificate chain, we need to download both the TMD and Ticket of a title. For
|
||||
// the sake of simplicity, we'll use the Wii Menu 4.3U because I already found the required TMD
|
||||
// and Ticket offsets for it.
|
||||
let endpoint_url = if wiiu_endpoint {
|
||||
WII_U_NUS_ENDPOINT.to_owned()
|
||||
} else {
|
||||
WII_NUS_ENDPOINT.to_owned()
|
||||
};
|
||||
let tmd_url = format!("{}0000000100000002/tmd.513", endpoint_url);
|
||||
let tik_url = format!("{}0000000100000002/cetk", endpoint_url);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let tmd = client.get(tmd_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?.bytes()?;
|
||||
let tik = client.get(tik_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?.bytes()?;
|
||||
// Assemble the certificate chain.
|
||||
let mut cert_chain: Vec<u8> = Vec::new();
|
||||
// Certificate Authority data.
|
||||
cert_chain.write_all(&tik[0x2A4 + 768..])?;
|
||||
// Certificate Policy (TMD certificate) data.
|
||||
cert_chain.write_all(&tmd[0x328..0x328 + 768])?;
|
||||
// XS (Ticket certificate) data.
|
||||
cert_chain.write_all(&tik[0x2A4..0x2A4 + 768])?;
|
||||
Ok(cert_chain)
|
||||
}
|
||||
|
||||
/// Downloads a specified content file from the specified title from the NUS.
|
||||
pub fn download_content(title_id: [u8; 8], content_id: u32, wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||
// Build the download URL. The structure is download/<TID>/<CID>
|
||||
let endpoint_url = if wiiu_endpoint {
|
||||
WII_U_NUS_ENDPOINT.to_owned()
|
||||
} else {
|
||||
WII_NUS_ENDPOINT.to_owned()
|
||||
};
|
||||
let content_url = format!("{}{}/{:08X}", endpoint_url, &hex::encode(title_id), content_id);
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.get(content_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?;
|
||||
if !response.status().is_success() {
|
||||
return Err(NUSError::NotFound);
|
||||
}
|
||||
Ok(response.bytes()?.to_vec())
|
||||
}
|
||||
|
||||
/// Downloads all contents from the specified title from the NUS.
|
||||
pub fn download_contents(tmd: &tmd::TMD, wiiu_endpoint: bool) -> Result<Vec<Vec<u8>>, NUSError> {
|
||||
let content_ids: Vec<u32> = tmd.content_records.iter().map(|record| { record.content_id }).collect();
|
||||
let mut contents: Vec<Vec<u8>> = Vec::new();
|
||||
for id in content_ids {
|
||||
contents.push(download_content(tmd.title_id, id, wiiu_endpoint)?);
|
||||
}
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
/// Downloads the Ticket for a specified Title ID from the NUS, if it's available.
|
||||
pub fn download_ticket(title_id: [u8; 8], wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||
// Build the download URL. The structure is download/<TID>/cetk.
|
||||
let endpoint_url = if wiiu_endpoint {
|
||||
WII_U_NUS_ENDPOINT.to_owned()
|
||||
} else {
|
||||
WII_NUS_ENDPOINT.to_owned()
|
||||
};
|
||||
let tik_url = format!("{}{}/cetk", endpoint_url, &hex::encode(title_id));
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.get(tik_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?;
|
||||
if !response.status().is_success() {
|
||||
return Err(NUSError::NotFound);
|
||||
}
|
||||
let tik = ticket::Ticket::from_bytes(&response.bytes()?).map_err(|_| NUSError::InvalidData)?;
|
||||
tik.to_bytes().map_err(|_| NUSError::InvalidData)
|
||||
}
|
||||
|
||||
/// Downloads an entire title with all of its content from the NUS and returns a Title instance.
|
||||
pub fn download_title(title_id: [u8; 8], title_version: Option<u16>, wiiu_endpoint: bool) -> Result<title::Title, NUSError> {
|
||||
// Download the individual components of a title and then build a title from them.
|
||||
let cert_chain = cert::CertificateChain::from_bytes(&download_cert_chain(wiiu_endpoint)?)?;
|
||||
let tmd = tmd::TMD::from_bytes(&download_tmd(title_id, title_version, wiiu_endpoint)?)?;
|
||||
let tik = ticket::Ticket::from_bytes(&download_ticket(title_id, wiiu_endpoint)?)?;
|
||||
let content_region = content::ContentRegion::from_contents(download_contents(&tmd, wiiu_endpoint)?, tmd.content_records.clone())?;
|
||||
let title = title::Title::from_parts(cert_chain, None, tik, tmd, content_region, None)?;
|
||||
Ok(title)
|
||||
}
|
||||
|
||||
/// Downloads the TMD for a specified Title ID from the NUS.
|
||||
pub fn download_tmd(title_id: [u8; 8], title_version: Option<u16>, wiiu_endpoint: bool) -> Result<Vec<u8>, NUSError> {
|
||||
// Build the download URL. The structure is download/<TID>/tmd for latest and
|
||||
// download/<TID>/tmd.<version> for when a specific version is requested.
|
||||
let endpoint_url = if wiiu_endpoint {
|
||||
WII_U_NUS_ENDPOINT.to_owned()
|
||||
} else {
|
||||
WII_NUS_ENDPOINT.to_owned()
|
||||
};
|
||||
let tmd_url = if title_version.is_some() {
|
||||
format!("{}{}/tmd.{}", endpoint_url, &hex::encode(title_id), title_version.unwrap())
|
||||
} else {
|
||||
format!("{}{}/tmd", endpoint_url, &hex::encode(title_id))
|
||||
};
|
||||
let client = reqwest::blocking::Client::new();
|
||||
let response = client.get(tmd_url).header(reqwest::header::USER_AGENT, "wii libnup/1.0").send()?;
|
||||
if !response.status().is_success() {
|
||||
return Err(NUSError::NotFound);
|
||||
}
|
||||
let tmd = tmd::TMD::from_bytes(&response.bytes()?).map_err(|_| NUSError::InvalidData)?;
|
||||
tmd.to_bytes().map_err(|_| NUSError::InvalidData)
|
||||
}
|
||||
@@ -50,11 +50,11 @@ impl fmt::Display for TitleType {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ContentType {
|
||||
Normal,
|
||||
Development,
|
||||
HashTree,
|
||||
DLC,
|
||||
Shared,
|
||||
Normal = 1,
|
||||
Development = 2,
|
||||
HashTree = 3,
|
||||
DLC = 16385,
|
||||
Shared = 32769,
|
||||
}
|
||||
|
||||
impl fmt::Display for ContentType {
|
||||
@@ -70,8 +70,8 @@ impl fmt::Display for ContentType {
|
||||
}
|
||||
|
||||
pub enum AccessRight {
|
||||
AHB,
|
||||
DVDVideo,
|
||||
AHB = 0,
|
||||
DVDVideo = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -332,10 +332,7 @@ impl TMD {
|
||||
|
||||
/// Gets whether a specified access right is enabled in a TMD.
|
||||
pub fn check_access_right(&self, right: AccessRight) -> bool {
|
||||
match right {
|
||||
AccessRight::AHB => (self.access_rights & (1 << 0)) != 0,
|
||||
AccessRight::DVDVideo => (self.access_rights & (1 << 1)) != 0,
|
||||
}
|
||||
self.access_rights & (1 << right as u8) != 0
|
||||
}
|
||||
|
||||
/// Gets the name of the certificate used to sign a TMD as a string.
|
||||
|
||||
Reference in New Issue
Block a user