diff --git a/src/libWiiPy/__init__.py b/src/libWiiPy/__init__.py index 7530d43..b607d16 100644 --- a/src/libWiiPy/__init__.py +++ b/src/libWiiPy/__init__.py @@ -1,5 +1,7 @@ # "__init__.py" from libWiiPy by NinjaCheetah & Contributors # https://github.com/NinjaCheetah/libWiiPy +# +# These are the essential modules from libWiiPy that you'd probably want imported by default. from .commonkeys import * from .content import * diff --git a/src/libWiiPy/commonkeys.py b/src/libWiiPy/commonkeys.py index b1fd7d2..04755e2 100644 --- a/src/libWiiPy/commonkeys.py +++ b/src/libWiiPy/commonkeys.py @@ -8,8 +8,9 @@ korean_key = '63b82bb4f4614e2e13f2fefbba4c9b7e' vwii_key = '30bfc76e7c19afbb23163330ced7c28d' -def get_common_key(common_key_index): - """Gets the specified Wii Common Key based on the index provided. +def get_common_key(common_key_index) -> bytes: + """ + Gets the specified Wii Common Key based on the index provided. Possible values for common_key_index: 0: Common Key, 1: Korean Key, 2: vWii Key diff --git a/src/libWiiPy/content.py b/src/libWiiPy/content.py index 97851f5..ddb6476 100644 --- a/src/libWiiPy/content.py +++ b/src/libWiiPy/content.py @@ -12,7 +12,16 @@ from .crypto import decrypt_content, encrypt_content class ContentRegion: - """Creates a ContentRegion object to parse the continuous content region of a WAD. + """ + A ContentRegion object to parse the continuous content region of a WAD. Allows for retrieving content from the + region in both encrypted or decrypted form, and setting new content. + + Attributes + ---------- + content_records : List[ContentRecord] + The content records for the content stored in the region. + num_contents : int + The total number of contents stored in the region. """ def __init__(self): @@ -23,7 +32,8 @@ class ContentRegion: self.content_list: List[bytes] = [] def load(self, content_region: bytes, content_records: List[ContentRecord]) -> None: - """Loads the raw content region and builds a list of all the contents. + """ + Loads the raw content region and builds a list of all the contents. Parameters ---------- @@ -59,7 +69,8 @@ class ContentRegion: self.content_list.append(content_enc) def dump(self) -> bytes: - """Takes the list of contents and assembles them back into one content region. Returns this content region as a + """ + Takes the list of contents and assembles them back into one content region. Returns this content region as a bytes object and sets the raw content region variable to this result, then calls load() again to make sure the content list matches the raw data. @@ -85,7 +96,8 @@ class ContentRegion: return content_region_raw def get_enc_content_by_index(self, index: int) -> bytes: - """Gets an individual content from the content region based on the provided index, in encrypted form. + """ + Gets an individual content from the content region based on the provided index, in encrypted form. Parameters ---------- @@ -101,7 +113,8 @@ class ContentRegion: return content_enc def get_enc_content_by_cid(self, cid: int) -> bytes: - """Gets an individual content from the content region based on the provided Content ID, in encrypted form. + """ + Gets an individual content from the content region based on the provided Content ID, in encrypted form. Parameters ---------- @@ -127,7 +140,8 @@ class ContentRegion: return content_enc def get_enc_contents(self) -> List[bytes]: - """Gets a list of all encrypted contents from the content region. + """ + Gets a list of all encrypted contents from the content region. Returns ------- @@ -137,7 +151,8 @@ class ContentRegion: return self.content_list def get_content_by_index(self, index: int, title_key: bytes) -> bytes: - """Gets an individual content from the content region based on the provided index, in decrypted form. + """ + Gets an individual content from the content region based on the provided index, in decrypted form. Parameters ---------- @@ -168,7 +183,8 @@ class ContentRegion: return content_dec def get_content_by_cid(self, cid: int, title_key: bytes) -> bytes: - """Gets an individual content from the content region based on the provided Content ID, in decrypted form. + """ + Gets an individual content from the content region based on the provided Content ID, in decrypted form. Parameters ---------- @@ -196,7 +212,8 @@ class ContentRegion: return content_dec def get_contents(self, title_key: bytes) -> List[bytes]: - """Gets a list of all contents from the content region, in decrypted form. + """ + Gets a list of all contents from the content region, in decrypted form. Parameters ---------- @@ -216,7 +233,8 @@ class ContentRegion: def set_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int, content_hash: bytes) -> None: - """Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are + """ + Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are set in the content record, with a new record being added if necessary. Parameters @@ -257,8 +275,9 @@ class ContentRegion: self.content_list[index] = enc_content def set_content(self, dec_content: bytes, cid: int, index: int, content_type: int, title_key: bytes) -> None: - """Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are - set in the content record, with a new record being added if necessary. + """ + Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are + set in the content record, with a new record being added if necessary. Parameters ---------- @@ -283,7 +302,8 @@ class ContentRegion: self.set_enc_content(enc_content, cid, index, content_type, dec_content_size, dec_content_hash) def load_enc_content(self, enc_content: bytes, index: int) -> None: - """Loads the provided encrypted content into the content region at the specified index, with the assumption that + """ + Loads the provided encrypted content into the content region at the specified index, with the assumption that it matches the record at that index. Not recommended for most use cases, use decrypted content and load_content() instead. @@ -303,7 +323,8 @@ class ContentRegion: self.content_list[index] = enc_content def load_content(self, dec_content: bytes, index: int, title_key: bytes) -> None: - """Loads the provided decrypted content into the content region at the specified index, but first checks to make + """ + Loads the provided decrypted content into the content region at the specified index, but first checks to make sure it matches the record at that index before loading. This content will be encrypted when loaded. Parameters diff --git a/src/libWiiPy/crypto.py b/src/libWiiPy/crypto.py index 4314508..1a6b57a 100644 --- a/src/libWiiPy/crypto.py +++ b/src/libWiiPy/crypto.py @@ -7,7 +7,8 @@ from Crypto.Cipher import AES def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes: - """Gets the decrypted version of the encrypted Title Key provided. + """ + Gets the decrypted version of the encrypted Title Key provided. Requires the index of the common key to use, and the Title ID of the title that the Title Key is for. @@ -37,7 +38,8 @@ def decrypt_title_key(title_key_enc, common_key_index, title_id) -> bytes: def decrypt_content(content_enc, title_key, content_index, content_length) -> bytes: - """Gets the decrypted version of the encrypted content. + """ + Gets the decrypted version of the encrypted content. This requires the index of the content to decrypt as it is used as the IV, as well as the content length to adjust padding as necessary. @@ -76,7 +78,8 @@ def decrypt_content(content_enc, title_key, content_index, content_length) -> by def encrypt_content(content_dec, title_key, content_index) -> bytes: - """Gets the encrypted version of the decrypted content. + """ + Gets the encrypted version of the decrypted content. This requires the index of the content to encrypt as it is used as the IV, as well as the content length to adjust padding as necessary. diff --git a/src/libWiiPy/shared.py b/src/libWiiPy/shared.py index e2ed428..dd00602 100644 --- a/src/libWiiPy/shared.py +++ b/src/libWiiPy/shared.py @@ -1,8 +1,12 @@ # "shared.py" from libWiiPy by NinjaCheetah & Contributors # https://github.com/NinjaCheetah/libWiiPy +# +# This file defines general functions that may be useful in other modules of libWiiPy. Putting them here cuts down on +# clutter in other files. -def align_value(value, alignment=64): - """Aligns the provided value to the set alignment (defaults to 64). +def align_value(value, alignment=64) -> int: + """ + Aligns the provided value to the set alignment (defaults to 64). Parameters ---------- @@ -22,8 +26,9 @@ def align_value(value, alignment=64): return value -def pad_bytes_stream(data, alignment=64): - """Pads the provided bytes stream to the provided alignment (defaults to 64). +def pad_bytes_stream(data, alignment=64) -> bytes: + """ + Pads the provided bytes stream to the provided alignment (defaults to 64). Parameters ---------- diff --git a/src/libWiiPy/ticket.py b/src/libWiiPy/ticket.py index 39cd53c..2cefbdc 100644 --- a/src/libWiiPy/ticket.py +++ b/src/libWiiPy/ticket.py @@ -12,8 +12,7 @@ from typing import List class Ticket: """ - Creates a Ticket object that allows for either loading and editing an existing Ticket or creating one manually if - desired. + A Ticket object that allows for either loading and editing an existing Ticket or creating one manually if desired. Attributes ---------- @@ -58,7 +57,8 @@ class Ticket: # TODO: Write in v1 ticket attributes here. This code can currently only handle v0 tickets, and will reject v1. def load(self, ticket: bytes) -> None: - """Loads raw Ticket data and sets all attributes of the WAD object. This allows for manipulating an already + """ + Loads raw Ticket data and sets all attributes of the WAD object. This allows for manipulating an already existing Ticket. Parameters @@ -138,7 +138,8 @@ class Ticket: self.title_limits_list.append(TitleLimit(limit_type, limit_value)) def dump(self) -> bytes: - """Dumps the Ticket object back into bytes. This also sets the raw Ticket attribute of Ticket object to the + """ + Dumps the Ticket object back into bytes. This also sets the raw Ticket attribute of Ticket object to the dumped data, and triggers load() again to ensure that the raw data and object match. Returns @@ -209,8 +210,9 @@ class Ticket: # Return the raw TMD for the data contained in the object. return ticket_data_raw - def get_title_id(self): - """Gets the Title ID of the ticket's associated title. + def get_title_id(self) -> str: + """ + Gets the Title ID of the ticket's associated title. Returns ------- @@ -220,8 +222,9 @@ class Ticket: title_id_str = str(self.title_id.decode()) return title_id_str - def get_common_key_type(self): - """Gets the name of the common key used to encrypt the Title Key contained in the ticket. + def get_common_key_type(self) -> str: + """ + Gets the name of the common key used to encrypt the Title Key contained in the ticket. Returns ------- @@ -240,8 +243,9 @@ class Ticket: case 2: return "vWii" - def get_title_key(self): - """Gets the decrypted title key contained in the ticket. + def get_title_key(self) -> bytes: + """ + Gets the decrypted title key contained in the ticket. Returns ------- @@ -251,8 +255,9 @@ class Ticket: title_key = decrypt_title_key(self.title_key_enc, self.common_key_index, self.title_id) return title_key - def set_title_id(self, title_id): - """Sets the Title ID of the title in the Ticket. + def set_title_id(self, title_id) -> None: + """ + Sets the Title ID of the title in the Ticket. Parameters ---------- diff --git a/src/libWiiPy/title.py b/src/libWiiPy/title.py index 69e9a52..081bbc7 100644 --- a/src/libWiiPy/title.py +++ b/src/libWiiPy/title.py @@ -10,10 +10,15 @@ from .wad import WAD class Title: - """Creates a Title object that contains all components of a title, and allows altering them. + """ + A Title object that contains all components of a title, and allows altering them. Provides higher-level access + than manually creating WAD, TMD, Ticket, and ContentRegion objects and ensures that any data that needs to match + between files matches. Attributes ---------- + wad : WAD + A WAD object of a WAD containing the title's data. tmd : TMD A TMD object of the title's TMD. ticket : Ticket @@ -28,7 +33,8 @@ class Title: self.content: ContentRegion = ContentRegion() def load_wad(self, wad: bytes) -> None: - """Load existing WAD data into the title and create WAD, TMD, Ticket, and ContentRegion objects based off of it + """ + Load existing WAD data into the title and create WAD, TMD, Ticket, and ContentRegion objects based off of it to allow you to modify that data. Note that this will overwrite any existing data for this title. Parameters @@ -55,7 +61,8 @@ class Title: "invalid.") def dump_wad(self) -> bytes: - """Dumps all title components (TMD, Ticket, and contents) back into the WAD object, and then dumps the WAD back + """ + Dumps all title components (TMD, Ticket, and contents) back into the WAD object, and then dumps the WAD back into raw data and returns it. Returns @@ -74,7 +81,8 @@ class Title: return wad_data def load_tmd(self, tmd: bytes) -> None: - """Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title. + """ + Load existing TMD data into the title. Note that this will overwrite any existing TMD data for this title. Parameters ---------- @@ -85,7 +93,8 @@ class Title: self.tmd.load(tmd) def load_ticket(self, ticket: bytes) -> None: - """Load existing Ticket data into the title. Note that this will overwrite any existing Ticket data for this + """ + Load existing Ticket data into the title. Note that this will overwrite any existing Ticket data for this title. Parameters @@ -97,7 +106,8 @@ class Title: self.ticket.load(ticket) def load_content_records(self) -> None: - """Load content records from the TMD into the ContentRegion to allow loading content files based on the records. + """ + Load content records from the TMD into the ContentRegion to allow loading content files based on the records. This requires that a TMD has already been loaded and will throw an exception if it isn't. """ if not self.tmd.content_records: @@ -106,7 +116,8 @@ class Title: self.content.content_records = self.tmd.content_records def set_title_id(self, title_id: str) -> None: - """Sets the Title ID of the title in both the TMD and Ticket. + """ + Sets the Title ID of the title in both the TMD and Ticket. Parameters ---------- @@ -119,7 +130,8 @@ class Title: self.ticket.set_title_id(title_id) def get_content_by_index(self, index: id) -> bytes: - """Gets an individual content from the content region based on the provided index, in decrypted form. + """ + Gets an individual content from the content region based on the provided index, in decrypted form. Parameters ---------- @@ -138,7 +150,8 @@ class Title: return dec_content def get_content_by_cid(self, cid: int) -> bytes: - """Gets an individual content from the content region based on the provided Content ID, in decrypted form. + """ + Gets an individual content from the content region based on the provided Content ID, in decrypted form. Parameters ---------- @@ -158,7 +171,8 @@ class Title: def set_enc_content(self, enc_content: bytes, cid: int, index: int, content_type: int, content_size: int, content_hash: bytes) -> None: - """Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are + """ + Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are set in the content record, with a new record being added if necessary. The TMD is also updated to match the new records. @@ -183,7 +197,8 @@ class Title: self.tmd.content_records = self.content.content_records def set_content(self, dec_content: bytes, cid: int, index: int, content_type: int) -> None: - """Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are + """ + Sets the provided index to a new content with the provided Content ID. Hashes and size of the content are set in the content record, with a new record being added if necessary. The Title Key is sourced from this title's loaded ticket. The TMD is also updated to match the new records. @@ -204,7 +219,8 @@ class Title: self.tmd.content_records = self.content.content_records def load_content(self, dec_content: bytes, index: int) -> None: - """Loads the provided decrypted content into the content region at the specified index, but first checks to make + """ + Loads the provided decrypted content into the content region at the specified index, but first checks to make sure it matches the record at that index before loading. This content will be encrypted when loaded. Parameters diff --git a/src/libWiiPy/tmd.py b/src/libWiiPy/tmd.py index 7ead8c0..54cbb07 100644 --- a/src/libWiiPy/tmd.py +++ b/src/libWiiPy/tmd.py @@ -12,7 +12,7 @@ from .types import ContentRecord class TMD: """ - Creates a TMD object that allows for either loading and editing an existing TMD or creating one manually if desired. + A TMD object that allows for either loading and editing an existing TMD or creating one manually if desired. Attributes ---------- @@ -53,7 +53,8 @@ class TMD: self.content_records: List[ContentRecord] = [] def load(self, tmd: bytes) -> None: - """Loads raw TMD data and sets all attributes of the WAD object. This allows for manipulating an already + """ + Loads raw TMD data and sets all attributes of the WAD object. This allows for manipulating an already existing TMD. Parameters @@ -138,7 +139,8 @@ class TMD: binascii.hexlify(content_record_hdr[4]))) def dump(self) -> bytes: - """Dumps the TMD object back into bytes. This also sets the raw TMD attribute of TMD object to the dumped data, + """ + Dumps the TMD object back into bytes. This also sets the raw TMD attribute of TMD object to the dumped data, and triggers load() again to ensure that the raw data and object match. Returns @@ -212,8 +214,9 @@ class TMD: # Return the raw TMD for the data contained in the object. return tmd_data_raw - def get_title_region(self): - """Gets the region of the TMD's associated title. + def get_title_region(self) -> str: + """ + Gets the region of the TMD's associated title. Can be one of several possible values: 'JAP', 'USA', 'EUR', 'NONE', or 'KOR'. @@ -235,8 +238,9 @@ class TMD: case 4: return "KOR" - def get_is_vwii_title(self): - """Gets whether the TMD is designed for the vWii or not. + def get_is_vwii_title(self) -> bool: + """ + Gets whether the TMD is designed for the vWii or not. Returns ------- @@ -248,8 +252,9 @@ class TMD: else: return False - def get_title_type(self): - """Gets the type of the TMD's associated title. + def get_title_type(self) -> str: + """ + Gets the type of the TMD's associated title. Can be one of several possible values: 'System', 'Game', 'Channel', 'SystemChannel', 'GameWithChannel', or 'HiddenChannel' @@ -279,7 +284,8 @@ class TMD: return "Unknown" def get_content_type(self): - """Gets the type of content contained in the TMD's associated title. + """ + Gets the type of content contained in the TMD's associated title. Can be one of several possible values: 'Normal', 'Development/Unknown', 'Hash Tree', 'DLC', or 'Shared' @@ -303,8 +309,9 @@ class TMD: case _: return "Unknown" - def get_content_record(self, record): - """Gets the content record at the specified index. + def get_content_record(self, record) -> ContentRecord: + """ + Gets the content record at the specified index. Parameters ---------- @@ -322,8 +329,9 @@ class TMD: raise IndexError("Invalid content record! TMD lists '" + str(self.num_contents - 1) + "' contents but index was '" + str(record) + "'!") - def set_title_id(self, title_id): - """Sets the Title ID of the title in the ticket. + def set_title_id(self, title_id) -> None: + """ + Sets the Title ID of the title in the ticket. Parameters ---------- diff --git a/src/libWiiPy/types.py b/src/libWiiPy/types.py index 3a9f0ff..6080dbc 100644 --- a/src/libWiiPy/types.py +++ b/src/libWiiPy/types.py @@ -7,7 +7,9 @@ from dataclasses import dataclass @dataclass class ContentRecord: """ - Creates a content record object that contains the details of a content contained in a title. + A content record object that contains the details of a content contained in a title. This information must match + the content stored at the index in the record, or else the content will not decrypt properly, as the hash of the + decrypted data will not match the hash in the content record. Attributes ---------- @@ -31,7 +33,10 @@ class ContentRecord: @dataclass class TitleLimit: - """Creates a TitleLimit object that contains the type of restriction and the limit. + """ + A TitleLimit object that contains the type of restriction and the limit. The limit type can be one of the following: + 0 = None, 1 = Time Limit, 3 = None, or 4 = Launch Count. The maximum usage is then either the time in minutes the + title can be played or the maximum number of launches allowed for that title, based on the type of limit applied. Attributes ---------- @@ -40,9 +45,8 @@ class TitleLimit: maximum_usage : int The maximum value for the type of play limit applied. """ - # The type of play limit applied. The following types exist: + # The type of play limit applied. # 0 = None, 1 = Time Limit, 3 = None, 4 = Launch Count limit_type: int # The maximum value of the limit applied. - # This is either the number of minutes for a time limit, or the number of launches for a launch limit. maximum_usage: int diff --git a/src/libWiiPy/wad.py b/src/libWiiPy/wad.py index f98c323..cd8423b 100644 --- a/src/libWiiPy/wad.py +++ b/src/libWiiPy/wad.py @@ -10,7 +10,24 @@ from .shared import align_value, pad_bytes_stream class WAD: """ - Creates a WAD object that allows for either loading and editing an existing WAD or creating a new WAD from raw data. + A WAD object that allows for either loading and editing an existing WAD or creating a new WAD from raw data. + + Attributes + ---------- + wad_type : str + The type of WAD, either ib for boot2 or Is for normal installable WADs. libWiiPy only supports Is currently. + wad_cert_size : int + The size of the WAD's certificate. + wad_crl_size : int + The size of the WAD's crl. + wad_tik_size : int + The size of the WAD's Ticket. + wad_tmd_size : int + The size of the WAD's TMD. + wad_content_size : int + The size of WAD's total content region. + wad_meta_size : int + The size of the WAD's meta/footer. """ def __init__(self): self.wad_hdr_size: int = 64 @@ -33,7 +50,8 @@ class WAD: self.wad_meta_data: bytes = b'' def load(self, wad_data) -> None: - """Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already + """ + Loads raw WAD data and sets all attributes of the WAD object. This allows for manipulating an already existing WAD file. Parameters @@ -114,7 +132,8 @@ class WAD: self.wad_meta_data = wad_data.read(self.wad_meta_size) def dump(self) -> bytes: - """Dumps the WAD object into the raw WAD file. This allows for creating a WAD file from the data contained in + """ + Dumps the WAD object into the raw WAD file. This allows for creating a WAD file from the data contained in the WAD object. Returns @@ -167,8 +186,9 @@ class WAD: # Return the raw WAD file for the data contained in the object. return wad_data_raw - def get_wad_type(self): - """Gets the type of the WAD. + def get_wad_type(self) -> str: + """ + Gets the type of the WAD. Returns ------- @@ -178,7 +198,8 @@ class WAD: return self.wad_type def get_cert_data(self) -> bytes: - """Gets the certificate data from the WAD. + """ + Gets the certificate data from the WAD. Returns ------- @@ -188,7 +209,8 @@ class WAD: return self.wad_cert_data def get_crl_data(self) -> bytes: - """Gets the crl data from the WAD, if it exists. + """ + Gets the crl data from the WAD, if it exists. Returns ------- @@ -198,7 +220,8 @@ class WAD: return self.wad_crl_data def get_ticket_data(self) -> bytes: - """Gets the ticket data from the WAD. + """ + Gets the ticket data from the WAD. Returns ------- @@ -208,7 +231,8 @@ class WAD: return self.wad_tik_data def get_tmd_data(self) -> bytes: - """Returns the TMD data from the WAD. + """ + Returns the TMD data from the WAD. Returns ------- @@ -218,7 +242,8 @@ class WAD: return self.wad_tmd_data def get_content_data(self) -> bytes: - """Gets the content of the WAD. + """ + Gets the content of the WAD. Returns ------- @@ -228,7 +253,8 @@ class WAD: return self.wad_content_data def get_meta_data(self) -> bytes: - """Gets the meta region of the WAD, which is typically unused. + """ + Gets the meta region of the WAD, which is typically unused. Returns ------- @@ -238,7 +264,8 @@ class WAD: return self.wad_meta_data def set_cert_data(self, cert_data) -> None: - """Sets the certificate data of the WAD. Also calculates the new size. + """ + Sets the certificate data of the WAD. Also calculates the new size. Parameters ---------- @@ -250,7 +277,8 @@ class WAD: self.wad_cert_size = len(cert_data) def set_crl_data(self, crl_data) -> None: - """Sets the crl data of the WAD. Also calculates the new size. + """ + Sets the crl data of the WAD. Also calculates the new size. Parameters ---------- @@ -262,7 +290,8 @@ class WAD: self.wad_crl_size = len(crl_data) def set_tmd_data(self, tmd_data) -> None: - """Sets the TMD data of the WAD. Also calculates the new size. + """ + Sets the TMD data of the WAD. Also calculates the new size. Parameters ---------- @@ -274,7 +303,8 @@ class WAD: self.wad_tmd_size = len(tmd_data) def set_ticket_data(self, tik_data) -> None: - """Sets the Ticket data of the WAD. Also calculates the new size. + """ + Sets the Ticket data of the WAD. Also calculates the new size. Parameters ---------- @@ -286,7 +316,8 @@ class WAD: self.wad_tik_size = len(tik_data) def set_content_data(self, content_data) -> None: - """Sets the content data of the WAD. Also calculates the new size. + """ + Sets the content data of the WAD. Also calculates the new size. Parameters ---------- @@ -298,7 +329,8 @@ class WAD: self.wad_content_size = len(content_data) def set_meta_data(self, meta_data) -> None: - """Sets the meta data of the WAD. Also calculates the new size. + """ + Sets the meta data of the WAD. Also calculates the new size. Parameters ----------