diff --git a/NUS Downloader/CertificateChain.cs b/NUS Downloader/CertificateChain.cs new file mode 100644 index 0000000..d9f7145 --- /dev/null +++ b/NUS Downloader/CertificateChain.cs @@ -0,0 +1,494 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace libWiiSharp +{ + public class CertificateChain : IDisposable + { + private const string certCaHash = "5B7D3EE28706AD8DA2CBD5A6B75C15D0F9B6F318"; + private const string certCpHash = "6824D6DA4C25184F0D6DAF6EDB9C0FC57522A41C"; + private const string certXsHash = "09787045037121477824BC6A3E5E076156573F8A"; + private SHA1 sha = SHA1.Create(); + private bool[] certsComplete = new bool[3]; + + private byte[] certCa = new byte[0x400]; + private byte[] certCp = new byte[0x300]; + private byte[] certXs = new byte[0x300]; + + /// + /// If false, the Certificate Chain is not complete (i.e. at least one certificate is missing). + /// + public bool CertsComplete { get { return (certsComplete[0] && certsComplete[1] && certsComplete[2]); } } + + #region IDisposable Members + private bool isDisposed = false; + + ~CertificateChain() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !isDisposed) + { + sha.Clear(); + sha = null; + + certsComplete = null; + certCa = null; + certCp = null; + certXs = null; + } + + isDisposed = true; + } + #endregion + + #region Public Functions + /// + /// Loads a cert file. + /// + /// + /// + public static CertificateChain Load(string pathToCert) + { + return Load(File.ReadAllBytes(pathToCert)); + } + + /// + /// Loads a cert file. + /// + /// + /// + public static CertificateChain Load(byte[] certFile) + { + CertificateChain c = new CertificateChain(); + MemoryStream ms = new MemoryStream(certFile); + + try { c.parseCert(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return c; + } + + /// + /// Loads a cert file. + /// + /// + /// + public static CertificateChain Load(Stream cert) + { + CertificateChain c = new CertificateChain(); + c.parseCert(cert); + return c; + } + + /// + /// Grabs certificates from Ticket and Tmd. + /// Ticket and Tmd must contain certs! (They do when they're downloaded from NUS!) + /// + /// + /// + /// + public static CertificateChain FromTikTmd(string pathToTik, string pathToTmd) + { + return FromTikTmd(File.ReadAllBytes(pathToTik), File.ReadAllBytes(pathToTmd)); + } + + /// + /// Grabs certificates from Ticket and Tmd. + /// Ticket and Tmd must contain certs! (They do when they're downloaded from NUS!) + /// + /// + /// + /// + public static CertificateChain FromTikTmd(byte[] tikFile, byte[] tmdFile) + { + CertificateChain c = new CertificateChain(); + MemoryStream ms = new MemoryStream(tikFile); + + try { c.grabFromTik(ms); } + catch { ms.Dispose(); throw; } + + ms = new MemoryStream(tmdFile); + + try { c.grabFromTmd(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + + if (!c.CertsComplete) throw new Exception("Couldn't locate all certs!"); + + return c; + } + + /// + /// Grabs certificates from Ticket and Tmd. + /// Ticket and Tmd must contain certs! (They do when they're downloaded from NUS!) + /// + /// + /// + /// + public static CertificateChain FromTikTmd(Stream tik, Stream tmd) + { + CertificateChain c = new CertificateChain(); + c.grabFromTik(tik); + c.grabFromTmd(tmd); + return c; + } + + + + /// + /// Loads a cert file. + /// + /// + public void LoadFile(string pathToCert) + { + LoadFile(File.ReadAllBytes(pathToCert)); + } + + /// + /// Loads a cert file. + /// + /// + public void LoadFile(byte[] certFile) + { + MemoryStream ms = new MemoryStream(certFile); + + try { parseCert(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + } + + /// + /// Loads a cert file. + /// + /// + public void LoadFile(Stream cert) + { + parseCert(cert); + } + + /// + /// Grabs certificates from Ticket and Tmd. + /// Ticket and Tmd must contain certs! (They do when they're downloaded from NUS!) + /// + /// + /// + /// + public void LoadFromTikTmd(string pathToTik, string pathToTmd) + { + LoadFromTikTmd(File.ReadAllBytes(pathToTik), File.ReadAllBytes(pathToTmd)); + } + + /// + /// Grabs certificates from Ticket and Tmd. + /// Ticket and Tmd must contain certs! (They do when they're downloaded from NUS!) + /// + /// + /// + public void LoadFromTikTmd(byte[] tikFile, byte[] tmdFile) + { + MemoryStream ms = new MemoryStream(tikFile); + + try { grabFromTik(ms); } + catch { ms.Dispose(); throw; } + + ms = new MemoryStream(tmdFile); + + try { grabFromTmd(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + + if (!CertsComplete) throw new Exception("Couldn't locate all certs!"); + } + + /// + /// Grabs certificates from Ticket and Tmd. + /// Ticket and Tmd must contain certs! (They do when they're downloaded from NUS!) + /// + /// + /// + public void LoadFromTikTmd(Stream tik, Stream tmd) + { + grabFromTik(tik); + grabFromTmd(tmd); + } + + + + /// + /// Saves the Certificate Chain. + /// + /// + public void Save(string savePath) + { + if (File.Exists(savePath)) File.Delete(savePath); + + using (FileStream fs = new FileStream(savePath, FileMode.Create)) + writeToStream(fs); + } + + /// + /// Returns the Certificate Chain as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the Certificate Chain as a byte array. + /// + /// + public byte[] ToByteArray() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + byte[] res = ms.ToArray(); + ms.Dispose(); + return res; + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + fireDebug("Writing Certificate Chain..."); + + if (!CertsComplete) + { fireDebug(" Certificate Chain incomplete..."); throw new Exception("At least one certificate is missing!"); } + + writeStream.Seek(0, SeekOrigin.Begin); + + fireDebug(" Writing Certificate CA... (Offset: 0x{0})", writeStream.Position.ToString("x8")); + writeStream.Write(certCa, 0, certCa.Length); + + fireDebug(" Writing Certificate CP... (Offset: 0x{0})", writeStream.Position.ToString("x8")); + writeStream.Write(certCp, 0, certCp.Length); + + fireDebug(" Writing Certificate XS... (Offset: 0x{0})", writeStream.Position.ToString("x8")); + writeStream.Write(certXs, 0, certXs.Length); + + fireDebug("Writing Certificate Chain Finished..."); + } + + private void parseCert(Stream certFile) + { + fireDebug("Parsing Certificate Chain..."); + int off = 0; + + for (int i = 0; i < 3; i++) + { + fireDebug(" Scanning at Offset 0x{0}:", off.ToString("x8")); + + try + { + certFile.Seek(off, SeekOrigin.Begin); + byte[] temp = new byte[0x400]; + + certFile.Read(temp, 0, temp.Length); + + fireDebug(" Checking for Certificate CA..."); + if (isCertCa(temp) && !certsComplete[1]) + { fireDebug(" Certificate CA detected..."); certCa = temp; certsComplete[1] = true; off += 0x400; continue; } + + fireDebug(" Checking for Certificate CP..."); + if (isCertCp(temp) && !certsComplete[2]) + { fireDebug(" Certificate CP detected..."); Array.Resize(ref temp, 0x300); certCp = temp; certsComplete[2] = true; off += 0x300; continue; } + + fireDebug(" Checking for Certificate XS..."); + if (isCertXs(temp) && !certsComplete[0]) + { fireDebug(" Certificate XS detected..."); Array.Resize(ref temp, 0x300); certXs = temp; certsComplete[0] = true; off += 0x300; continue; } + } + catch (Exception ex) { fireDebug("Error: {0}", ex.Message); } + + off += 0x300; + } + + if (!CertsComplete) + { fireDebug(" Couldn't locate all Certificates..."); throw new Exception("Couldn't locate all certs!"); } + + fireDebug("Parsing Certificate Chain Finished..."); + } + + private void grabFromTik(Stream tik) + { + fireDebug("Scanning Ticket for Certificates..."); + int off = 676; + + for (int i = 0; i < 3; i++) + { + fireDebug(" Scanning at Offset 0x{0}:", off.ToString("x8")); + + try + { + tik.Seek(off, SeekOrigin.Begin); + byte[] temp = new byte[0x400]; + + tik.Read(temp, 0, temp.Length); + + fireDebug(" Checking for Certificate CA..."); + if (isCertCa(temp) && !certsComplete[1]) + { fireDebug(" Certificate CA detected..."); certCa = temp; certsComplete[1] = true; off += 0x400; continue; } + + fireDebug(" Checking for Certificate CP..."); + if (isCertCp(temp) && !certsComplete[2]) + { fireDebug(" Certificate CP detected..."); Array.Resize(ref temp, 0x300); certCp = temp; certsComplete[2] = true; off += 0x300; continue; } + + fireDebug(" Checking for Certificate XS..."); + if (isCertXs(temp) && !certsComplete[0]) + { fireDebug(" Certificate XS detected..."); Array.Resize(ref temp, 0x300); certXs = temp; certsComplete[0] = true; off += 0x300; continue; } + } + catch { } + + off += 0x300; + } + + fireDebug("Scanning Ticket for Certificates Finished..."); + } + + private void grabFromTmd(Stream tmd) + { + fireDebug("Scanning TMD for Certificates..."); + + byte[] temp = new byte[2]; + tmd.Seek(478, SeekOrigin.Begin); + tmd.Read(temp, 0, 2); + + int numContents = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + int off = 484 + numContents * 36; + + for (int i = 0; i < 3; i++) + { + fireDebug(" Scanning at Offset 0x{0}:", off.ToString("x8")); + + try + { + tmd.Seek(off, SeekOrigin.Begin); + temp = new byte[0x400]; + + tmd.Read(temp, 0, temp.Length); + + fireDebug(" Checking for Certificate CA..."); + if (isCertCa(temp) && !certsComplete[1]) + { fireDebug(" Certificate CA detected..."); certCa = temp; certsComplete[1] = true; off += 0x400; continue; } + + fireDebug(" Checking for Certificate CP..."); + if (isCertCp(temp) && !certsComplete[2]) + { fireDebug(" Certificate CP detected..."); Array.Resize(ref temp, 0x300); certCp = temp; certsComplete[2] = true; off += 0x300; continue; } + + fireDebug(" Checking for Certificate XS..."); + if (isCertXs(temp) && !certsComplete[0]) + { fireDebug(" Certificate XS detected..."); Array.Resize(ref temp, 0x300); certXs = temp; certsComplete[0] = true; off += 0x300; continue; } + } + catch { } + + off += 0x300; + } + + fireDebug("Scanning TMD for Certificates Finished..."); + } + + private bool isCertXs(byte[] part) + { + if (part.Length < 0x300) return false; + else if (part.Length > 0x300) Array.Resize(ref part, 0x300); + + if (part[0x184] == 'X' && part[0x185] == 'S') + { + byte[] newHash = sha.ComputeHash(part); + byte[] oldHash = Shared.HexStringToByteArray(certXsHash); + + if (Shared.CompareByteArrays(newHash, oldHash)) return true; + } + + return false; + } + + private bool isCertCa(byte[] part) + { + if (part.Length < 0x400) return false; + else if (part.Length > 0x400) Array.Resize(ref part, 0x400); + + if (part[0x284] == 'C' && part[0x285] == 'A') + { + byte[] newHash = sha.ComputeHash(part); + byte[] oldHash = Shared.HexStringToByteArray(certCaHash); + + if (Shared.CompareByteArrays(newHash, oldHash)) return true; + } + + return false; + } + + private bool isCertCp(byte[] part) + { + if (part.Length < 0x300) return false; + else if (part.Length > 0x300) Array.Resize(ref part, 0x300); + + if (part[0x184] == 'C' && part[0x185] == 'P') + { + byte[] newHash = sha.ComputeHash(part); + byte[] oldHash = Shared.HexStringToByteArray(certCpHash); + + if (Shared.CompareByteArrays(newHash, oldHash)) return true; + } + + return false; + } + #endregion + + #region Events + /// + /// Fires debugging messages. You may write them into a log file or log textbox. + /// + public event EventHandler Debug; + + private void fireDebug(string debugMessage, params object[] args) + { + EventHandler debug = Debug; + if (debug != null) + debug(new object(), new MessageEventArgs(string.Format(debugMessage, args))); + } + #endregion + } +} diff --git a/NUS Downloader/CommonKey.cs b/NUS Downloader/CommonKey.cs new file mode 100644 index 0000000..fe4dfb9 --- /dev/null +++ b/NUS Downloader/CommonKey.cs @@ -0,0 +1,35 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace libWiiSharp +{ + public class CommonKey + { + private static string standardKey = "ebe42a225e8593e448d9c5457381aaf7"; + private static string koreanKey = "63b82bb4f4614e2e13f2fefbba4c9b7e"; + + public static byte[] GetStandardKey() + { + return Shared.HexStringToByteArray(standardKey); + } + + public static byte[] GetKoreanKey() + { + return Shared.HexStringToByteArray(koreanKey); + } + } +} diff --git a/NUS Downloader/ContentIndices.cs b/NUS Downloader/ContentIndices.cs new file mode 100644 index 0000000..483d778 --- /dev/null +++ b/NUS Downloader/ContentIndices.cs @@ -0,0 +1,44 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace libWiiSharp +{ + internal struct ContentIndices : IComparable + { + private int index; + private int contentIndex; + + public int Index { get { return index; } } + public int ContentIndex { get { return contentIndex; } } + + public ContentIndices(int index, int contentIndex) + { + this.index = index; + this.contentIndex = contentIndex; + } + + public int CompareTo(object obj) + { + if (obj is ContentIndices) return contentIndex.CompareTo(((ContentIndices)obj).contentIndex); + else throw new ArgumentException(); + } + } +} diff --git a/NUS Downloader/Form1.Designer.cs b/NUS Downloader/Form1.Designer.cs index 08a5b83..00e1b27 100644 --- a/NUS Downloader/Form1.Designer.cs +++ b/NUS Downloader/Form1.Designer.cs @@ -63,10 +63,6 @@ this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator(); this.proxySettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripSeparator6 = new System.Windows.Forms.ToolStripSeparator(); - //this.getCommonKeyMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - //this.commonKeykeybinToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - //this.koreanKeykkeybinToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); - this.toolStripSeparator5 = new System.Windows.Forms.ToolStripSeparator(); this.aboutNUSDToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.proxyBox = new System.Windows.Forms.GroupBox(); this.label13 = new System.Windows.Forms.Label(); @@ -339,11 +335,9 @@ this.toolStripSeparator3, this.proxySettingsToolStripMenuItem, this.toolStripSeparator6, - //this.getCommonKeyMenuItem, - this.toolStripSeparator5, this.aboutNUSDToolStripMenuItem}); this.extrasStrip.Name = "extrasStrip"; - this.extrasStrip.Size = new System.Drawing.Size(183, 110); + this.extrasStrip.Size = new System.Drawing.Size(183, 82); // // loadInfoFromTMDToolStripMenuItem // @@ -371,35 +365,6 @@ this.toolStripSeparator6.Name = "toolStripSeparator6"; this.toolStripSeparator6.Size = new System.Drawing.Size(179, 6); // - // getCommonKeyMenuItem - // - /*this.getCommonKeyMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { - this.commonKeykeybinToolStripMenuItem, - this.koreanKeykkeybinToolStripMenuItem}); - this.getCommonKeyMenuItem.Image = global::NUS_Downloader.Properties.Resources.key; - this.getCommonKeyMenuItem.Name = "getCommonKeyMenuItem"; - this.getCommonKeyMenuItem.Size = new System.Drawing.Size(182, 22); - this.getCommonKeyMenuItem.Text = "Retrieve Key"; - // - // commonKeykeybinToolStripMenuItem - // - this.commonKeykeybinToolStripMenuItem.Name = "commonKeykeybinToolStripMenuItem"; - this.commonKeykeybinToolStripMenuItem.Size = new System.Drawing.Size(196, 22); - this.commonKeykeybinToolStripMenuItem.Text = "Common Key (key.bin)"; - this.commonKeykeybinToolStripMenuItem.Click += new System.EventHandler(this.commonKeykeybinToolStripMenuItem_Click); - // - // koreanKeykkeybinToolStripMenuItem - // - this.koreanKeykkeybinToolStripMenuItem.Name = "koreanKeykkeybinToolStripMenuItem"; - this.koreanKeykkeybinToolStripMenuItem.Size = new System.Drawing.Size(196, 22); - this.koreanKeykkeybinToolStripMenuItem.Text = "Korean Key (kkey.bin)"; - this.koreanKeykkeybinToolStripMenuItem.Click += new System.EventHandler(this.koreanKeykkeybinToolStripMenuItem_Click);*/ - // - // toolStripSeparator5 - // - this.toolStripSeparator5.Name = "toolStripSeparator5"; - this.toolStripSeparator5.Size = new System.Drawing.Size(179, 6); - // // aboutNUSDToolStripMenuItem // this.aboutNUSDToolStripMenuItem.Image = global::NUS_Downloader.Properties.Resources.information; @@ -916,7 +881,6 @@ private System.Windows.Forms.ToolStripMenuItem PALMassUpdate; private System.Windows.Forms.ToolStripMenuItem NTSCMassUpdate; private System.Windows.Forms.ToolStripMenuItem KoreaMassUpdate; - private System.Windows.Forms.ToolStripSeparator toolStripSeparator5; private System.Windows.Forms.ToolStripMenuItem aboutNUSDToolStripMenuItem; private System.Windows.Forms.CheckBox checkBox1; private System.Windows.Forms.Button SaveProxyPwdPermanentBtn; diff --git a/NUS Downloader/Form1.cs b/NUS Downloader/Form1.cs index 59523a4..52df8d4 100644 --- a/NUS Downloader/Form1.cs +++ b/NUS Downloader/Form1.cs @@ -114,6 +114,7 @@ namespace NUS_Downloader // Scripts Thread private BackgroundWorker scriptsWorker; + /* // Common Key hash private static byte[] wii_commonkey = new byte[16] { @@ -124,7 +125,7 @@ namespace NUS_Downloader { 0x63, 0xb8, 0x2b, 0xb4, 0xf4, 0x61, 0x4e, 0x2e, 0x13, 0xf2, 0xfe, 0xfb, 0xba, 0x4c, 0x9b, 0x7e }; - + */ /* public struct WADHeader { @@ -262,7 +263,7 @@ namespace NUS_Downloader { WriteStatus("Korean Common Key detected."); }*/ - WriteStatus("Common keys are compiled in, key.bin/kkey.bin not read."); + //WriteStatus("Common keys are compiled in, key.bin/kkey.bin not read."); // Check for DSi common key bin file... if (NUSDFileExists("dsikey.bin") == true) @@ -1011,15 +1012,15 @@ namespace NUS_Downloader if (cetkbuf[0x01F1] == 0x01) { WriteStatus("Key Type: Korean"); - keyBytes = LoadCommonKey("kkey.bin"); + keyBytes = libWiiSharp.CommonKey.GetKoreanKey(); } else { WriteStatus("Key Type: Standard"); if (wiimode) - keyBytes = LoadCommonKey("key.bin"); + keyBytes = libWiiSharp.CommonKey.GetStandardKey(); else - keyBytes = LoadCommonKey("dsikey.bin"); + keyBytes = File.ReadAllBytes(Path.Combine(CURRENT_DIR, "dsikey.bin")); } initCrypt(iv, keyBytes); @@ -1530,6 +1531,7 @@ namespace NUS_Downloader return sha.ComputeHash(data); } + /* /// /// Loads the common key from disc. /// @@ -1552,8 +1554,9 @@ namespace NUS_Downloader } else return null; - } + }*/ + /* /// /// Writes/overwrites the common key onto disc. /// @@ -1579,7 +1582,7 @@ namespace NUS_Downloader WriteStatus(String.Format("Error: Couldn't write {0}: {1}", keyfile, e.Message)); } return false; - } + }*/ private void button4_Click(object sender, EventArgs e) { diff --git a/NUS Downloader/Headers.cs b/NUS Downloader/Headers.cs new file mode 100644 index 0000000..7faa41a --- /dev/null +++ b/NUS Downloader/Headers.cs @@ -0,0 +1,747 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace libWiiSharp +{ + public class Headers + { + private static uint imd5Magic = 0x494d4435; + private static uint imetMagic = 0x494d4554; + + /// + /// Convert HeaderType to int to get it's Length. + /// + public enum HeaderType + { + None = 0, + /// + /// Used in opening.bnr + /// + ShortIMET = 1536, + /// + /// Used in 00000000.app + /// + IMET = 1600, + /// + /// Used in banner.bin / icon.bin + /// + IMD5 = 32, + } + + #region Public Functions + /// + /// Checks a file for Headers. + /// + /// + /// + public static HeaderType DetectHeader(string pathToFile) + { + return DetectHeader(File.ReadAllBytes(pathToFile)); + } + + /// + /// Checks the byte array for Headers. + /// + /// + /// + public static HeaderType DetectHeader(byte[] file) + { + if (file.Length > 68) + if (Shared.Swap(BitConverter.ToUInt32(file, 64)) == imetMagic) + return HeaderType.ShortIMET; + if (file.Length > 132) + if (Shared.Swap(BitConverter.ToUInt32(file, 128)) == imetMagic) + return HeaderType.IMET; + if (file.Length > 4) + if (Shared.Swap(BitConverter.ToUInt32(file, 0)) == imd5Magic) + return HeaderType.IMD5; + + return HeaderType.None; + } + + /// + /// Checks the stream for Headers. + /// + /// + /// + public static HeaderType DetectHeader(Stream file) + { + byte[] tmp = new byte[4]; + + if (file.Length > 68) + { + file.Seek(64, SeekOrigin.Begin); + file.Read(tmp, 0, tmp.Length); + if (Shared.Swap(BitConverter.ToUInt32(tmp, 0)) == imetMagic) + return HeaderType.ShortIMET; + } + if (file.Length > 132) + { + file.Seek(128, SeekOrigin.Begin); + file.Read(tmp, 0, tmp.Length); + if (Shared.Swap(BitConverter.ToUInt32(tmp, 0)) == imetMagic) + return HeaderType.IMET; + } + if (file.Length > 4) + { + file.Seek(0, SeekOrigin.Begin); + file.Read(tmp, 0, tmp.Length); + if (Shared.Swap(BitConverter.ToUInt32(tmp, 0)) == imd5Magic) + return HeaderType.IMD5; + } + + return HeaderType.None; + } + #endregion + + public class IMET + { + private bool hashesMatch = true; + private bool isShortImet = false; + + private byte[] additionalPadding = new byte[64]; + private byte[] padding = new byte[64]; + private uint imetMagic = 0x494d4554; + private uint sizeOfHeader = 0x00000600; //Without additionalPadding + private uint unknown = 0x00000003; + private uint iconSize; + private uint bannerSize; + private uint soundSize; + private uint flags = 0x00000000; + private byte[] japaneseTitle = new byte[84]; + private byte[] englishTitle = new byte[84]; + private byte[] germanTitle = new byte[84]; + private byte[] frenchTitle = new byte[84]; + private byte[] spanishTitle = new byte[84]; + private byte[] italianTitle = new byte[84]; + private byte[] dutchTitle = new byte[84]; + private byte[] unknownTitle1 = new byte[84]; + private byte[] unknownTitle2 = new byte[84]; + private byte[] koreanTitle = new byte[84]; + private byte[] padding2 = new byte[588]; + private byte[] hash = new byte[16]; + + /// + /// Short IMET has a padding of 64 bytes at the beginning while Long IMET has 128. + /// + public bool IsShortIMET { get { return isShortImet; } set { isShortImet = value; } } + /// + /// The size of uncompressed icon.bin + /// + public uint IconSize { get { return iconSize; } set { iconSize = value; } } + /// + /// The size of uncompressed banner.bin + /// + public uint BannerSize { get { return bannerSize; } set { bannerSize = value; } } + /// + /// The size of uncompressed sound.bin + /// + public uint SoundSize { get { return soundSize; } set { soundSize = value; } } + /// + /// The japanese Title. + /// + public string JapaneseTitle { get { return returnTitleAsString(japaneseTitle); } set { setTitleFromString(value, 0); } } + /// + /// The english Title. + /// + public string EnglishTitle { get { return returnTitleAsString(englishTitle); } set { setTitleFromString(value, 1); } } + /// + /// The german Title. + /// + public string GermanTitle { get { return returnTitleAsString(germanTitle); } set { setTitleFromString(value, 2); } } + /// + /// The french Title. + /// + public string FrenchTitle { get { return returnTitleAsString(frenchTitle); } set { setTitleFromString(value, 3); } } + /// + /// The spanish Title. + /// + public string SpanishTitle { get { return returnTitleAsString(spanishTitle); } set { setTitleFromString(value, 4); } } + /// + /// The italian Title. + /// + public string ItalianTitle { get { return returnTitleAsString(italianTitle); } set { setTitleFromString(value, 5); } } + /// + /// The dutch Title. + /// + public string DutchTitle { get { return returnTitleAsString(dutchTitle); } set { setTitleFromString(value, 6); } } + /// + /// The korean Title. + /// + public string KoreanTitle { get { return returnTitleAsString(koreanTitle); } set { setTitleFromString(value, 7); } } + /// + /// All Titles as a string array. + /// + public string[] AllTitles { get { return new string[] { JapaneseTitle, EnglishTitle, GermanTitle, FrenchTitle, SpanishTitle, ItalianTitle, DutchTitle, KoreanTitle }; } } + /// + /// When parsing an IMET header, this value will turn false if the hash stored in the header doesn't match the headers hash. + /// + public bool HashesMatch { get { return hashesMatch; } } + + #region Public Functions + /// + /// Loads the IMET Header of a file. + /// + /// + /// + public static IMET Load(string pathToFile) + { + return Load(File.ReadAllBytes(pathToFile)); + } + + /// + /// Loads the IMET Header of a byte array. + /// + /// + /// + public static IMET Load(byte[] fileOrHeader) + { + HeaderType type = DetectHeader(fileOrHeader); + if (type != HeaderType.IMET && type != HeaderType.ShortIMET) + throw new Exception("No IMET Header found!"); + + IMET s = new IMET(); + if (type == HeaderType.ShortIMET) s.isShortImet = true; + + MemoryStream ms = new MemoryStream(fileOrHeader); + try { s.parseHeader(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return s; + } + + /// + /// Loads the IMET Header of a stream. + /// + /// + /// + public static IMET Load(Stream fileOrHeader) + { + HeaderType type = DetectHeader(fileOrHeader); + if (type != HeaderType.IMET && type != HeaderType.ShortIMET) + throw new Exception("No IMET Header found!"); + + IMET s = new IMET(); + if (type == HeaderType.ShortIMET) s.isShortImet = true; + + s.parseHeader(fileOrHeader); + return s; + } + + /// + /// Creates a new IMET Header. + /// + /// + /// + /// + /// + /// + /// + public static IMET Create(bool isShortImet, int iconSize, int bannerSize, int soundSize, params string[] titles) + { + IMET s = new IMET(); + s.isShortImet = isShortImet; + + for (int i = 0; i < titles.Length; i++) + s.setTitleFromString(titles[i], i); + + for (int i = titles.Length; i < 8; i++) + s.setTitleFromString((titles.Length > 1) ? titles[1] : titles[0], i); + + s.iconSize = (uint)iconSize; + s.bannerSize = (uint)bannerSize; + s.soundSize = (uint)soundSize; + + return s; + } + + /// + /// Removes the IMET Header of a file. + /// + /// + public static void RemoveHeader(string pathToFile) + { + byte[] fileWithoutHeader = RemoveHeader(File.ReadAllBytes(pathToFile)); + File.Delete(pathToFile); + + File.WriteAllBytes(pathToFile, fileWithoutHeader); + } + + /// + /// Removes the IMET Header of a byte array. + /// + /// + /// + public static byte[] RemoveHeader(byte[] file) + { + HeaderType type = DetectHeader(file); + if (type != HeaderType.IMET && type != HeaderType.ShortIMET) + throw new Exception("No IMET Header found!"); + + byte[] fileWithoutHeader = new byte[file.Length - (int)type]; + Array.Copy(file, (int)type, fileWithoutHeader, 0, fileWithoutHeader.Length); + + return fileWithoutHeader; + } + + + + /// + /// Sets all title to the given string. + /// + /// + public void SetAllTitles(string newTitle) + { + for (int i = 0; i < 10; i++) + setTitleFromString(newTitle, i); + } + + /// + /// Returns the Header as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the Header as a byte array. + /// + /// + public byte[] ToByteArray() + { + return ToMemoryStream().ToArray(); + } + + /// + /// Writes the Header to the given stream. + /// + /// + public void Write(Stream writeStream) + { + writeToStream(writeStream); + } + + /// + /// Changes the Titles. + /// + /// + public void ChangeTitles(params string[] newTitles) + { + for (int i = 0; i < newTitles.Length; i++) + setTitleFromString(newTitles[i], i); + + for (int i = newTitles.Length; i < 8; i++) + setTitleFromString((newTitles.Length > 1) ? newTitles[1] : newTitles[0], i); + } + + /// + /// Returns a string array with the Titles. + /// + /// + public string[] GetTitles() + { + return new string[] { JapaneseTitle, EnglishTitle, GermanTitle, FrenchTitle, SpanishTitle, ItalianTitle, DutchTitle, KoreanTitle }; + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + writeStream.Seek(0, SeekOrigin.Begin); + + if (!isShortImet) writeStream.Write(additionalPadding, 0, additionalPadding.Length); + + writeStream.Write(padding, 0, padding.Length); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(imetMagic)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(sizeOfHeader)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(unknown)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(iconSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(bannerSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(soundSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(flags)), 0, 4); + + writeStream.Write(japaneseTitle, 0, japaneseTitle.Length); + writeStream.Write(englishTitle, 0, englishTitle.Length); + writeStream.Write(germanTitle, 0, germanTitle.Length); + writeStream.Write(frenchTitle, 0, frenchTitle.Length); + writeStream.Write(spanishTitle, 0, spanishTitle.Length); + writeStream.Write(italianTitle, 0, italianTitle.Length); + writeStream.Write(dutchTitle, 0, dutchTitle.Length); + writeStream.Write(unknownTitle1, 0, unknownTitle1.Length); + writeStream.Write(unknownTitle2, 0, unknownTitle2.Length); + writeStream.Write(koreanTitle, 0, koreanTitle.Length); + writeStream.Write(padding2, 0, padding2.Length); + + int hashPos = (int)writeStream.Position; + hash = new byte[16]; + writeStream.Write(hash, 0, hash.Length); + + byte[] toHash = new byte[writeStream.Position]; + writeStream.Seek(0, SeekOrigin.Begin); + writeStream.Read(toHash, 0, toHash.Length); + + computeHash(toHash, isShortImet ? 0 : 0x40); + + writeStream.Seek(hashPos, SeekOrigin.Begin); + writeStream.Write(hash, 0, hash.Length); + } + + private void computeHash(byte[] headerBytes, int hashPos) + { + MD5 md5 = MD5.Create(); + hash = md5.ComputeHash(headerBytes, hashPos, 0x600); + md5.Clear(); + } + + private void parseHeader(Stream headerStream) + { + headerStream.Seek(0, SeekOrigin.Begin); + byte[] tmp = new byte[4]; + + if (!isShortImet) headerStream.Read(additionalPadding, 0, additionalPadding.Length); + headerStream.Read(padding, 0, padding.Length); + + headerStream.Read(tmp, 0, 4); + if (Shared.Swap(BitConverter.ToUInt32(tmp, 0)) != imetMagic) + throw new Exception("Invalid Magic!"); + + headerStream.Read(tmp, 0, 4); + if (Shared.Swap(BitConverter.ToUInt32(tmp, 0)) != sizeOfHeader) + throw new Exception("Invalid Header Size!"); + + headerStream.Read(tmp, 0, 4); + unknown = Shared.Swap(BitConverter.ToUInt32(tmp, 0)); + + headerStream.Read(tmp, 0, 4); + iconSize = Shared.Swap(BitConverter.ToUInt32(tmp, 0)); + + headerStream.Read(tmp, 0, 4); + bannerSize = Shared.Swap(BitConverter.ToUInt32(tmp, 0)); + + headerStream.Read(tmp, 0, 4); + soundSize = Shared.Swap(BitConverter.ToUInt32(tmp, 0)); + + headerStream.Read(tmp, 0, 4); + flags = Shared.Swap(BitConverter.ToUInt32(tmp, 0)); + + headerStream.Read(japaneseTitle, 0, japaneseTitle.Length); + headerStream.Read(englishTitle, 0, englishTitle.Length); + headerStream.Read(germanTitle, 0, germanTitle.Length); + headerStream.Read(frenchTitle, 0, frenchTitle.Length); + headerStream.Read(spanishTitle, 0, spanishTitle.Length); + headerStream.Read(italianTitle, 0, italianTitle.Length); + headerStream.Read(dutchTitle, 0, dutchTitle.Length); + headerStream.Read(unknownTitle1, 0, unknownTitle1.Length); + headerStream.Read(unknownTitle2, 0, unknownTitle2.Length); + headerStream.Read(koreanTitle, 0, koreanTitle.Length); + + headerStream.Read(padding2, 0, padding2.Length); + headerStream.Read(hash, 0, hash.Length); + + headerStream.Seek(-16, SeekOrigin.Current); + headerStream.Write(new byte[16], 0, 16); + + byte[] temp = new byte[headerStream.Length]; + headerStream.Seek(0, SeekOrigin.Begin); + headerStream.Read(temp, 0, temp.Length); + + MD5 m = MD5.Create(); + byte[] newHash = m.ComputeHash(temp, (isShortImet) ? 0 : 0x40, 0x600); + m.Clear(); + + hashesMatch = Shared.CompareByteArrays(newHash, hash); + } + + private string returnTitleAsString(byte[] title) + { + string tempStr = string.Empty; + + for (int i = 0; i < 84; i += 2) + { + char tempChar = BitConverter.ToChar(new byte[] { title[i + 1], title[i] }, 0); + if (tempChar != 0x00) tempStr += tempChar; + } + + return tempStr; + } + + private void setTitleFromString(string title, int titleIndex) + { + byte[] tempArray = new byte[84]; + + for (int i = 0; i < title.Length; i++) + { + byte[] tempBytes = BitConverter.GetBytes(title[i]); + tempArray[i * 2 + 1] = tempBytes[0]; + tempArray[i * 2] = tempBytes[1]; + } + + switch (titleIndex) + { + case 0: + japaneseTitle = tempArray; + break; + case 1: + englishTitle = tempArray; + break; + case 2: + germanTitle = tempArray; + break; + case 3: + frenchTitle = tempArray; + break; + case 4: + spanishTitle = tempArray; + break; + case 5: + italianTitle = tempArray; + break; + case 6: + dutchTitle = tempArray; + break; + case 7: + koreanTitle = tempArray; + break; + } + } + #endregion + } + + public class IMD5 + { + private uint imd5Magic = 0x494d4435; + private uint fileSize; + private byte[] padding = new byte[8]; + private byte[] hash = new byte[16]; + + /// + /// The size of the file without the IMD5 Header. + /// + public uint FileSize { get { return fileSize; } } + /// + /// The hash of the file without the IMD5 Header. + /// + public byte[] Hash { get { return hash; } } + + private IMD5() { } + + #region Public Functions + /// + /// Loads the IMD5 Header of a file. + /// + /// + /// + public static IMD5 Load(string pathToFile) + { + return Load(File.ReadAllBytes(pathToFile)); + } + + /// + /// Loads the IMD5 Header of a byte array. + /// + /// + /// + public static IMD5 Load(byte[] fileOrHeader) + { + HeaderType type = DetectHeader(fileOrHeader); + if (type != HeaderType.IMD5) + throw new Exception("No IMD5 Header found!"); + + IMD5 h = new IMD5(); + MemoryStream ms = new MemoryStream(fileOrHeader); + + try { h.parseHeader(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return h; + } + + /// + /// Loads the IMD5 Header of a stream. + /// + /// + /// + public static IMD5 Load(Stream fileOrHeader) + { + HeaderType type = DetectHeader(fileOrHeader); + if (type != HeaderType.IMD5) + throw new Exception("No IMD5 Header found!"); + + IMD5 h = new IMD5(); + h.parseHeader(fileOrHeader); + return h; + } + + /// + /// Creates a new IMD5 Header. + /// + /// + /// + public static IMD5 Create(byte[] file) + { + IMD5 h = new IMD5(); + + h.fileSize = (uint)file.Length; + h.computeHash(file); + + return h; + } + + /// + /// Adds an IMD5 Header to a file. + /// + /// + public static void AddHeader(string pathToFile) + { + byte[] fileWithHeader = AddHeader(File.ReadAllBytes(pathToFile)); + File.Delete(pathToFile); + + using (FileStream fs = new FileStream(pathToFile, FileMode.Create)) + fs.Write(fileWithHeader, 0, fileWithHeader.Length); + } + + /// + /// Adds an IMD5 Header to a byte array. + /// + /// + /// + public static byte[] AddHeader(byte[] file) + { + IMD5 h = IMD5.Create(file); + + MemoryStream ms = new MemoryStream(); + h.writeToStream(ms); + ms.Write(file, 0, file.Length); + + byte[] res = ms.ToArray(); + ms.Dispose(); + return res; + } + + /// + /// Removes the IMD5 Header of a file. + /// + /// + public static void RemoveHeader(string pathToFile) + { + byte[] fileWithoutHeader = RemoveHeader(File.ReadAllBytes(pathToFile)); + File.Delete(pathToFile); + + using (FileStream fs = new FileStream(pathToFile, FileMode.Create)) + fs.Write(fileWithoutHeader, 0, fileWithoutHeader.Length); + } + + /// + /// Removes the IMD5 Header of a byte array. + /// + /// + /// + public static byte[] RemoveHeader(byte[] file) + { + MemoryStream ms = new MemoryStream(); + ms.Write(file, 32, file.Length - 32); + + byte[] ret = ms.ToArray(); + ms.Dispose(); + + return ret; + } + + + + /// + /// Returns the IMD5 Header as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the IMD5 Header as a byte array. + /// + /// + public byte[] ToByteArray() + { + return ToMemoryStream().ToArray(); + } + + /// + /// Writes the IMD5 Header to the given stream. + /// + /// + public void Write(Stream writeStream) + { + writeToStream(writeStream); + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + writeStream.Seek(0, SeekOrigin.Begin); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(imd5Magic)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(fileSize)), 0, 4); + writeStream.Write(padding, 0, padding.Length); + writeStream.Write(hash, 0, hash.Length); + } + + private void computeHash(byte[] bytesToHash) + { + MD5 md5 = MD5.Create(); + hash = md5.ComputeHash(bytesToHash); + md5.Clear(); + } + + private void parseHeader(Stream headerStream) + { + headerStream.Seek(0, SeekOrigin.Begin); + byte[] tmp = new byte[4]; + + headerStream.Read(tmp, 0, 4); + if (Shared.Swap(BitConverter.ToUInt32(tmp, 0)) != imd5Magic) + throw new Exception("Invalid Magic!"); + + headerStream.Read(tmp, 0, 4); + fileSize = Shared.Swap(BitConverter.ToUInt32(tmp, 0)); + + headerStream.Read(padding, 0, padding.Length); + headerStream.Read(hash, 0, hash.Length); + } + #endregion + } + } +} diff --git a/NUS Downloader/IosPatcher.cs b/NUS Downloader/IosPatcher.cs new file mode 100644 index 0000000..00121fa --- /dev/null +++ b/NUS Downloader/IosPatcher.cs @@ -0,0 +1,313 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +//Patches based on Dop-Mii - Thanks to Arikado and Lunatik + +using System; +using System.ComponentModel; +using System.Text; + +namespace libWiiSharp +{ + /// + /// An IOS patcher which can patch fakesigning, es_identify and nand permissions. + /// + public class IosPatcher + { + private WAD wadFile; + private int esIndex = -1; + + #region Public Functions + /// + /// Loads an IOS wad to patch the es module. + /// + /// + public void LoadIOS(ref WAD iosWad) + { + if ((iosWad.TitleID >> 32) != 1 || + (iosWad.TitleID & 0xFFFFFFFF) < 3 || + (iosWad.TitleID & 0xFFFFFFFF) > 255) + throw new Exception("Only IOS WADs can be patched!"); + + wadFile = iosWad; + getEsIndex(); + } + + /// + /// Patches fakesigning. + /// Returns the number of applied patches. + /// + /// + public int PatchFakeSigning() + { + if (esIndex < 0) return -1; + return patchFakeSigning(ref wadFile.Contents[esIndex]); + } + + /// + /// Patches es_identify. + /// Returns the number of applied patches. + /// + /// + public int PatchEsIdentify() + { + if (esIndex < 0) return -1; + return patchEsIdentify(ref wadFile.Contents[esIndex]); + } + + /// + /// Patches nand permissions. + /// Returns the number of applied patches. + /// + /// + public int PatchNandPermissions() + { + if (esIndex < 0) return -1; + return patchNandPermissions(ref wadFile.Contents[esIndex]); + } + + /// + /// Patches fakesigning, es_identify and nand permissions. + /// Returns the number of applied patches. + /// + /// + public int PatchAll() + { + if (esIndex < 0) return -1; + return patchAll(ref wadFile.Contents[esIndex]); + } + + /// + /// Patches fakesigning. + /// Returns the number of applied patches. + /// + /// + public int PatchFakeSigning(ref byte[] esModule) + { + return patchFakeSigning(ref esModule); + } + + /// + /// Patches es_identify. + /// Returns the number of applied patches. + /// + /// + public int PatchEsIdentify(ref byte[] esModule) + { + return patchEsIdentify(ref esModule); + } + + /// + /// Patches nand permissions. + /// Returns the number of applied patches. + /// + /// + public int PatchNandPermissions(ref byte[] esModule) + { + return patchNandPermissions(ref esModule); + } + + /// + /// Patches fakesigning, es_identify and nand permissions. + /// Returns the number of applied patches. + /// + /// + public int PatchAll(ref byte[] esModule) + { + return patchAll(ref esModule); + } + #endregion + + #region Private Functions + private int patchFakeSigning(ref byte[] esModule) + { + fireDebug("Patching Fakesigning..."); + int patchCount = 0; + + byte[] oldHashCheck = { 0x20, 0x07, 0x23, 0xA2 }; + byte[] newHashCheck = { 0x20, 0x07, 0x4B, 0x0B }; + + for (int i = 0; i < esModule.Length - 4; i++) + { + fireProgress((i + 1) * 100 / esModule.Length); + + if (Shared.CompareByteArrays(esModule, i, oldHashCheck, 0, 4) || + Shared.CompareByteArrays(esModule, i, newHashCheck, 0, 4)) + { + fireDebug(" Patching at Offset: 0x{0}", i.ToString("x8").ToUpper()); + esModule[i + 1] = 0x00; + i += 4; + patchCount++; + } + } + + fireDebug("Patching Fakesigning Finished... (Patches applied: {0})", patchCount); + return patchCount; + } + + private int patchEsIdentify(ref byte[] esModule) + { + fireDebug("Patching ES_Identify..."); + int patchCount = 0; + + byte[] identifyCheck = { 0x28, 0x03, 0xD1, 0x23 }; + + for (int i = 0; i < esModule.Length - 4; i++) + { + fireProgress((i + 1) * 100 / esModule.Length); + + if (Shared.CompareByteArrays(esModule, i, identifyCheck, 0, 4)) + { + fireDebug(" Patching at Offset: 0x{0}", i.ToString("x8").ToUpper()); + esModule[i + 2] = 0x00; + esModule[i + 3] = 0x00; + i += 4; + patchCount++; + } + } + + fireDebug("Patching ES_Identify Finished... (Patches applied: {0})", patchCount); + return patchCount; + } + + private int patchNandPermissions(ref byte[] esModule) + { + fireDebug("Patching NAND Permissions..."); + int patchCount = 0; + + byte[] permissionTable = { 0x42, 0x8B, 0xD0, 0x01, 0x25, 0x66 }; + + for (int i = 0; i < esModule.Length - 6; i++) + { + fireProgress((i + 1) * 100 / esModule.Length); + + if (Shared.CompareByteArrays(esModule, i, permissionTable, 0, 6)) + { + fireDebug(" Patching at Offset: 0x{0}", i.ToString("x8").ToUpper()); + esModule[i + 2] = 0xE0; + i += 6; + patchCount++; + } + } + + fireDebug("Patching NAND Permissions Finished... (Patches applied: {0})", patchCount); + return patchCount; + } + + private int patchAll(ref byte[] esModule) + { + fireDebug("Patching Fakesigning, ES_Identify and NAND Permissions..."); + int patchCount = 0; + + byte[] oldHashCheck = { 0x20, 0x07, 0x23, 0xA2 }; + byte[] newHashCheck = { 0x20, 0x07, 0x4B, 0x0B }; + byte[] identifyCheck = { 0x28, 0x03, 0xD1, 0x23 }; + byte[] permissionTable = { 0x42, 0x8B, 0xD0, 0x01, 0x25, 0x66 }; + + for (int i = 0; i < esModule.Length - 6; i++) + { + fireProgress((i + 1) * 100 / esModule.Length); + + if (Shared.CompareByteArrays(esModule, i, oldHashCheck, 0, 4) || + Shared.CompareByteArrays(esModule, i, newHashCheck, 0, 4)) + { + fireDebug(" Patching Fakesigning at Offset: 0x{0}", i.ToString("x8").ToUpper()); + esModule[i + 1] = 0x00; + i += 4; + patchCount++; + } + else if (Shared.CompareByteArrays(esModule, i, identifyCheck, 0, 4)) + { + fireDebug(" Patching ES_Identify at Offset: 0x{0}", i.ToString("x8").ToUpper()); + esModule[i + 2] = 0x00; + esModule[i + 3] = 0x00; + i += 4; + patchCount++; + } + else if (Shared.CompareByteArrays(esModule, i, permissionTable, 0, 6)) + { + fireDebug(" Patching NAND Permissions at Offset: 0x{0}", i.ToString("x8").ToUpper()); + esModule[i + 2] = 0xE0; + i += 6; + patchCount++; + } + } + + fireDebug("Patching Fakesigning, ES_Identify and NAND Permissions Finished... (Patches applied: {0})", patchCount); + return patchCount; + } + + private void getEsIndex() + { + fireDebug("Scanning for ES Module..."); + string iosTag = "$IOSVersion:"; + + for (int i = wadFile.NumOfContents - 1; i >= 0; i--) + { + fireDebug(" Scanning Content #{0} of {1}...", i + 1, wadFile.NumOfContents); + fireProgress((i + 1) * 100 / wadFile.NumOfContents); + + for (int j = 0; j < wadFile.Contents[i].Length - 64; j++) + { + if (ASCIIEncoding.ASCII.GetString(wadFile.Contents[i], j, 12) == iosTag) + { + int curIndex = j + 12; + while (wadFile.Contents[i][curIndex] == ' ') { curIndex++; } + + if (ASCIIEncoding.ASCII.GetString(wadFile.Contents[i], curIndex, 3) == "ES:") + { + fireDebug(" -> ES Module found!"); + fireDebug("Scanning for ES Module Finished..."); + esIndex = i; + fireProgress(100); + return; + } + } + } + } + + fireDebug(@"/!\/!\/!\ ES Module wasn't found! /!\/!\/!\"); + throw new Exception("ES module wasn't found!"); + } + #endregion + + #region Events + /// + /// Fires the Progress of various operations + /// + public event EventHandler Progress; + /// + /// Fires debugging messages. You may write them into a log file or log textbox. + /// + public event EventHandler Debug; + + private void fireDebug(string debugMessage, params object[] args) + { + EventHandler debug = Debug; + if (debug != null) + debug(new object(), new MessageEventArgs(string.Format(debugMessage, args))); + } + + private void fireProgress(int progressPercentage) + { + EventHandler progress = Progress; + if (progress != null) + progress(new object(), new ProgressChangedEventArgs(progressPercentage, string.Empty)); + } + #endregion + } +} diff --git a/NUS Downloader/Lz77.cs b/NUS Downloader/Lz77.cs new file mode 100644 index 0000000..0433197 --- /dev/null +++ b/NUS Downloader/Lz77.cs @@ -0,0 +1,400 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +//Lz77 (de-)compression algorithm based on gbalzss by Andre Perrot (Thanks!) + +using System; +using System.IO; + +namespace libWiiSharp +{ + public class Lz77 + { + private static uint lz77Magic = 0x4c5a3737; + private const int N = 4096; + private const int F = 18; + private const int threshold = 2; + private int[] leftSon = new int[N + 1]; + private int[] rightSon = new int[N + 257]; + private int[] dad = new int[N + 1]; + private ushort[] textBuffer = new ushort[N + 17]; + private int matchPosition = 0, matchLength = 0; + + /// + /// Lz77 Magic. + /// + public static uint Lz77Magic { get { return lz77Magic; } } + + #region Public Functions + /// + /// Checks whether a file is Lz77 compressed or not. + /// + /// + /// + public static bool IsLz77Compressed(string file) + { + return IsLz77Compressed(File.ReadAllBytes(file)); + } + + /// + /// Checks whether a file is Lz77 compressed or not. + /// + /// + /// + public static bool IsLz77Compressed(byte[] file) + { + Headers.HeaderType h = Headers.DetectHeader(file); + return (Shared.Swap(BitConverter.ToUInt32(file, (int)h)) == lz77Magic) ; + } + + /// + /// Checks whether a file is Lz77 compressed or not. + /// + /// + /// + public static bool IsLz77Compressed(Stream file) + { + Headers.HeaderType h = Headers.DetectHeader(file); + byte[] temp = new byte[4]; + file.Seek((long)h, SeekOrigin.Begin); + file.Read(temp, 0, temp.Length); + return (Shared.Swap(BitConverter.ToUInt32(temp, 0)) == lz77Magic); + } + + + + /// + /// Compresses a file using the Lz77 algorithm. + /// + /// + /// + public void Compress(string inFile, string outFile) + { + Stream compressedFile; + + using (FileStream fsIn = new FileStream(inFile, FileMode.Open)) + compressedFile = compress(fsIn); + + byte[] output = new byte[compressedFile.Length]; + compressedFile.Read(output, 0, output.Length); + + if (File.Exists(outFile)) File.Delete(outFile); + + using (FileStream fs = new FileStream(outFile, FileMode.Create)) + fs.Write(output, 0, output.Length); + } + + /// + /// Compresses the byte array using the Lz77 algorithm. + /// + /// + /// + public byte[] Compress(byte[] file) + { + return ((MemoryStream)compress(new MemoryStream(file))).ToArray(); + } + + /// + /// Compresses the stream using the Lz77 algorithm. + /// + /// + /// + public Stream Compress(Stream file) + { + return compress(file); + } + + /// + /// Decompresses a file using the Lz77 algorithm. + /// + /// + /// + public void Decompress(string inFile, string outFile) + { + Stream compressedFile; + + using (FileStream fsIn = new FileStream(inFile, FileMode.Open)) + compressedFile = decompress(fsIn); + + byte[] output = new byte[compressedFile.Length]; + compressedFile.Read(output, 0, output.Length); + + if (File.Exists(outFile)) File.Delete(outFile); + + using (FileStream fs = new FileStream(outFile, FileMode.Create)) + fs.Write(output, 0, output.Length); + } + + /// + /// Decompresses the byte array using the Lz77 algorithm. + /// + /// + /// + public byte[] Decompress(byte[] file) + { + return ((MemoryStream)decompress(new MemoryStream(file))).ToArray(); + } + + public Stream Decompress(Stream file) + { + return decompress(file); + } + #endregion + + #region Private Functions + private Stream decompress(Stream inFile) + { + if (!Lz77.IsLz77Compressed(inFile)) return inFile; + inFile.Seek(0, SeekOrigin.Begin); + + int i, j, k, r, c, z; + uint flags, decompressedSize, currentSize = 0; + + Headers.HeaderType h = Headers.DetectHeader(inFile); + + byte[] temp = new byte[8]; + inFile.Seek((int)h, SeekOrigin.Begin); + inFile.Read(temp, 0, 8); + + if (Shared.Swap(BitConverter.ToUInt32(temp, 0)) != lz77Magic) + { inFile.Dispose(); throw new Exception("Invaild Magic!"); } + if (temp[4] != 0x10) + { inFile.Dispose(); throw new Exception("Unsupported Compression Type!"); } + + decompressedSize = (BitConverter.ToUInt32(temp, 4)) >> 8; + + for (i = 0; i < N - F; i++) textBuffer[i] = 0xdf; + r = N - F; flags = 7; z = 7; + + MemoryStream outFile = new MemoryStream(); + while (true) + { + flags <<= 1; + z++; + + if (z == 8) + { + if ((c = inFile.ReadByte()) == -1) break; + + flags = (uint)c; + z = 0; + } + + if ((flags & 0x80) == 0) + { + if ((c = inFile.ReadByte()) == inFile.Length - 1) break; + if (currentSize < decompressedSize) outFile.WriteByte((byte)c); + + textBuffer[r++] = (byte)c; + r &= (N - 1); + currentSize++; + } + else + { + if ((i = inFile.ReadByte()) == -1) break; + if ((j = inFile.ReadByte()) == -1) break; + + j = j | ((i << 8) & 0xf00); + i = ((i >> 4) & 0x0f) + threshold; + for (k = 0; k <= i; k++) + { + c = textBuffer[(r - j - 1) & (N - 1)]; + if (currentSize < decompressedSize) outFile.WriteByte((byte)c); textBuffer[r++] = (byte)c; r &= (N - 1); currentSize++; + } + } + } + + return outFile; + } + + private Stream compress(Stream inFile) + { + if (Lz77.IsLz77Compressed(inFile)) return inFile; + inFile.Seek(0, SeekOrigin.Begin); + + int textSize = 0; + int codeSize = 0; + + int i, c, r, s, length, lastMatchLength, codeBufferPointer, mask; + int[] codeBuffer = new int[17]; + + uint fileSize = ((Convert.ToUInt32(inFile.Length)) << 8) + 0x10; + MemoryStream outFile = new MemoryStream(); + + outFile.Write(BitConverter.GetBytes(Shared.Swap(lz77Magic)), 0, 4); + outFile.Write(BitConverter.GetBytes(fileSize), 0, 4); + + InitTree(); + codeBuffer[0] = 0; + codeBufferPointer = 1; + mask = 0x80; + s = 0; + r = N - F; + + for (i = s; i < r; i++) textBuffer[i] = 0xffff; + + for (length = 0; length < F && (c = (int)inFile.ReadByte()) != -1; length++) + textBuffer[r + length] = (ushort)c; + + if ((textSize = length) == 0) return inFile; + + for (i = 1; i <= F; i++) InsertNode(r - i); + InsertNode(r); + + do + { + if (matchLength > length) matchLength = length; + + if (matchLength <= threshold) + { + matchLength = 1; + codeBuffer[codeBufferPointer++] = textBuffer[r]; + } + else + { + codeBuffer[0] |= mask; + + codeBuffer[codeBufferPointer++] = (char) + (((r - matchPosition - 1) >> 8) & 0x0f) | + ((matchLength - (threshold + 1)) << 4); + + codeBuffer[codeBufferPointer++] = (char)((r - matchPosition - 1) & 0xff); + } + + if ((mask >>= 1) == 0) + { + for (i = 0; i < codeBufferPointer; i++) + outFile.WriteByte((byte)codeBuffer[i]); + + codeSize += codeBufferPointer; + codeBuffer[0] = 0; codeBufferPointer = 1; + mask = 0x80; + } + + lastMatchLength = matchLength; + for (i = 0; i < lastMatchLength && (c = (int)inFile.ReadByte()) != -1; i++) + { + DeleteNode(s); + + textBuffer[s] = (ushort)c; + if (s < F - 1) textBuffer[s + N] = (ushort)c; + s = (s + 1) & (N - 1); r = (r + 1) & (N - 1); + + InsertNode(r); + } + + while (i++ < lastMatchLength) + { + DeleteNode(s); + + s = (s + 1) & (N - 1); r = (r + 1) & (N - 1); + if (--length != 0) InsertNode(r); + } + } while (length > 0); + + + if (codeBufferPointer > 1) + { + for (i = 0; i < codeBufferPointer; i++) outFile.WriteByte((byte)codeBuffer[i]); + codeSize += codeBufferPointer; + } + + if (codeSize % 4 != 0) + for (i = 0; i < 4 - (codeSize % 4); i++) + outFile.WriteByte(0x00); + + return outFile; + } + + private void InitTree() + { + int i; + for (i = N + 1; i <= N + 256; i++) rightSon[i] = N; + for (i = 0; i < N; i++) dad[i] = N; + } + + private void InsertNode(int r) + { + int i, p, cmp; + cmp = 1; + p = N + 1 + (textBuffer[r] == 0xffff ? 0 : textBuffer[r]); + rightSon[r] = leftSon[r] = N; matchLength = 0; + + for (; ; ) + { + if (cmp >= 0) + { + if (rightSon[p] != N) p = rightSon[p]; + else { rightSon[p] = r; dad[r] = p; return; } + } + else + { + if (leftSon[p] != N) p = leftSon[p]; + else { leftSon[p] = r; dad[r] = p; return; } + } + + for (i = 1; i < F; i++) + if ((cmp = textBuffer[r + i] - textBuffer[p + i]) != 0) break; + + if (i > matchLength) + { + matchPosition = p; + if ((matchLength = i) >= F) break; + } + } + + dad[r] = dad[p]; leftSon[r] = leftSon[p]; rightSon[r] = rightSon[p]; + dad[leftSon[p]] = r; dad[rightSon[p]] = r; + + if (rightSon[dad[p]] == p) rightSon[dad[p]] = r; + else leftSon[dad[p]] = r; + + dad[p] = N; + } + + private void DeleteNode(int p) + { + int q; + + if (dad[p] == N) return; + + if (rightSon[p] == N) q = leftSon[p]; + else if (leftSon[p] == N) q = rightSon[p]; + else + { + q = leftSon[p]; + + if (rightSon[q] != N) + { + do { q = rightSon[q]; } while (rightSon[q] != N); + rightSon[dad[q]] = leftSon[q]; dad[leftSon[q]] = dad[q]; + leftSon[q] = leftSon[p]; dad[leftSon[p]] = q; + } + + rightSon[q] = rightSon[p]; dad[rightSon[p]] = q; + } + + dad[q] = dad[p]; + + if (rightSon[dad[p]] == p) rightSon[dad[p]] = q; + else leftSon[dad[p]] = q; + + dad[p] = N; + } + #endregion + } +} diff --git a/NUS Downloader/NUS Downloader.csproj b/NUS Downloader/NUS Downloader.csproj index fdb54bc..07b7729 100644 --- a/NUS Downloader/NUS Downloader.csproj +++ b/NUS Downloader/NUS Downloader.csproj @@ -68,7 +68,10 @@ + + + PreserveNewest Form @@ -77,6 +80,9 @@ Form1.cs PreserveNewest + + + @@ -108,6 +114,11 @@ Settings.settings True + + + + + Component diff --git a/NUS Downloader/Shared.cs b/NUS Downloader/Shared.cs new file mode 100644 index 0000000..6e01478 --- /dev/null +++ b/NUS Downloader/Shared.cs @@ -0,0 +1,272 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.Net; + +namespace libWiiSharp +{ + public static class Shared + { + /// + /// Merges two string arrays into one without double entries. + /// + /// + /// + /// + public static string[] MergeStringArrays(string[] a, string[] b) + { + List sList = new List(a); + + foreach (string currentString in b) + if (!sList.Contains(currentString)) sList.Add(currentString); + + sList.Sort(); + return sList.ToArray(); + } + + /// + /// Compares two byte arrays. + /// + /// + /// + /// + /// + /// + /// + public static bool CompareByteArrays(byte[] first, int firstIndex, byte[] second, int secondIndex, int length) + { + if (first.Length < length || second.Length < length) return false; + + for (int i = 0; i < length; i++) + if (first[firstIndex + i] != second[secondIndex + i]) return false; + + return true; + } + + /// + /// Compares two byte arrays. + /// + /// + /// + /// + public static bool CompareByteArrays(byte[] first, byte[] second) + { + if (first.Length != second.Length) return false; + else + for (int i = 0; i < first.Length; i++) + if (first[i] != second[i]) return false; + + return true; + } + + /// + /// Turns a byte array into a string, default separator is a space. + /// + /// + /// + /// + public static string ByteArrayToString(byte[] byteArray, char separator = ' ') + { + string res = string.Empty; + + foreach (byte b in byteArray) + res += b.ToString("x2").ToUpper() + separator; + + return res.Remove(res.Length - 1); + } + + /// + /// Turns a hex string into a byte array. + /// + /// + /// + public static byte[] HexStringToByteArray(string hexString) + { + byte[] ba = new byte[hexString.Length / 2]; + + for (int i = 0; i < hexString.Length / 2; i++) + ba[i] = byte.Parse(hexString.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber); + + return ba; + } + + /// + /// Counts how often the given char exists in the given string. + /// + /// + /// + /// + public static int CountCharsInString(string theString, char theChar) + { + int count = 0; + + foreach (char thisChar in theString) + if (thisChar == theChar) + count++; + + return count; + } + + /// + /// Pads the given value to a multiple of the given padding value, default padding value is 64. + /// + /// + /// + public static long AddPadding(long value) + { + return AddPadding(value, 64); + } + + /// + /// Pads the given value to a multiple of the given padding value, default padding value is 64. + /// + /// + /// + /// + public static long AddPadding(long value, int padding) + { + if (value % padding != 0) + { + value = value + (padding - (value % padding)); + } + + return value; + } + + /// + /// Pads the given value to a multiple of the given padding value, default padding value is 64. + /// + /// + /// + public static int AddPadding(int value) + { + return AddPadding(value, 64); + } + + /// + /// Pads the given value to a multiple of the given padding value, default padding value is 64. + /// + /// + /// + /// + public static int AddPadding(int value, int padding) + { + if (value % padding != 0) + { + value = value + (padding - (value % padding)); + } + + return value; + } + + /// + /// Swaps endianness. + /// + /// + /// + public static ushort Swap(ushort value) + { + return (ushort)IPAddress.HostToNetworkOrder((short)value); + } + + /// + /// Swaps endianness. + /// + /// + /// + public static uint Swap(uint value) + { + return (uint)IPAddress.HostToNetworkOrder((int)value); + } + + /// + /// Swaps endianness + /// + /// + /// + public static ulong Swap(ulong value) + { + return (ulong)IPAddress.HostToNetworkOrder((long)value); + } + + /// + /// Turns a ushort array into a byte array. + /// + /// + /// + public static byte[] UShortArrayToByteArray(ushort[] array) + { + List results = new List(); + foreach (ushort value in array) + { + byte[] converted = BitConverter.GetBytes(value); + results.AddRange(converted); + } + return results.ToArray(); + } + + /// + /// Turns a uint array into a byte array. + /// + /// + /// + public static byte[] UIntArrayToByteArray(uint[] array) + { + List results = new List(); + foreach (uint value in array) + { + byte[] converted = BitConverter.GetBytes(value); + results.AddRange(converted); + } + return results.ToArray(); + } + + /// + /// Turns a byte array into a uint array. + /// + /// + /// + public static uint[] ByteArrayToUIntArray(byte[] array) + { + UInt32[] converted = new UInt32[array.Length / 4]; + int j = 0; + + for (int i = 0; i < array.Length; i += 4) + converted[j++] = BitConverter.ToUInt32(array, i); + + return converted; + } + + /// + /// Turns a byte array into a ushort array. + /// + /// + /// + public static ushort[] ByteArrayToUShortArray(byte[] array) + { + ushort[] converted = new ushort[array.Length / 2]; + int j = 0; + + for (int i = 0; i < array.Length; i += 2) + converted[j++] = BitConverter.ToUInt16(array, i); + + return converted; + } + } +} diff --git a/NUS Downloader/TMD.cs b/NUS Downloader/TMD.cs new file mode 100644 index 0000000..32cbef5 --- /dev/null +++ b/NUS Downloader/TMD.cs @@ -0,0 +1,659 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; + +namespace libWiiSharp +{ + public enum ContentType : ushort + { + Normal = 0x0001, + DLC = 0x4001, //Seen this in a DLC wad... + Shared = 0x8001, + } + + public enum Region : ushort + { + Japan = 0, + USA = 1, + Europe = 2, + Free = 3, + } + + public class TMD : IDisposable + { + private bool fakeSign = false; + + private uint signatureExponent = 0x00010001; + private byte[] signature = new byte[256]; + private byte[] padding = new byte[60]; + private byte[] issuer = new byte[64]; + private byte version; + private byte caCrlVersion; + private byte signerCrlVersion; + private byte paddingByte; + private ulong startupIos; + private ulong titleId; + private uint titleType; + private ushort groupId; + private ushort padding2; + private ushort region; + private byte[] reserved = new byte[58]; + private uint accessRights; + private ushort titleVersion; + private ushort numOfContents; + private ushort bootIndex; + private ushort padding3; + private List contents; + + /// + /// The region of the title. + /// + public Region Region { get { return (Region)region; } set { region = (ushort)value; } } + /// + /// The IOS the title is launched with. + /// + public ulong StartupIOS { get { return startupIos; } set { startupIos = value; } } + /// + /// The Title ID. + /// + public ulong TitleID { get { return titleId; } set { titleId = value; } } + /// + /// The Title Version. + /// + public ushort TitleVersion { get { return titleVersion; } set { titleVersion = value; } } + /// + /// The Number of Contents. + /// + public ushort NumOfContents { get { return numOfContents; } } + /// + /// The boot index. Represents the index of the nand loader. + /// + public ushort BootIndex { get { return bootIndex; } set { if (value <= numOfContents) bootIndex = value; } } + /// + /// The content descriptions in the TMD. + /// + public TMD_Content[] Contents { get { return contents.ToArray(); } set { contents = new List(value); numOfContents = (ushort)value.Length; } } + /// + /// If true, the TMD will be fakesigned while saving. + /// + public bool FakeSign { get { return fakeSign; } set { fakeSign = value; } } + + #region IDisposable Members + private bool isDisposed = false; + + ~TMD() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !isDisposed) + { + signature = null; + padding = null; + issuer = null; + reserved = null; + + contents.Clear(); + contents = null; + } + + isDisposed = true; + } + #endregion + + #region Public Functions + /// + /// Loads a tmd file. + /// + /// + /// + public static TMD Load(string pathToTmd) + { + return Load(File.ReadAllBytes(pathToTmd)); + } + + /// + /// Loads a tmd file. + /// + /// + /// + public static TMD Load(byte[] tmdFile) + { + TMD t = new TMD(); + MemoryStream ms = new MemoryStream(tmdFile); + + try { t.parseTmd(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return t; + } + + /// + /// Loads a tmd file. + /// + /// + /// + public static TMD Load(Stream tmd) + { + TMD t = new TMD(); + t.parseTmd(tmd); + return t; + } + + + + /// + /// Loads a tmd file. + /// + /// + public void LoadFile(string pathToTmd) + { + LoadFile(File.ReadAllBytes(pathToTmd)); + } + + /// + /// Loads a tmd file. + /// + /// + public void LoadFile(byte[] tmdFile) + { + MemoryStream ms = new MemoryStream(tmdFile); + + try { parseTmd(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + } + + /// + /// Loads a tmd file. + /// + /// + public void LoadFile(Stream tmd) + { + parseTmd(tmd); + } + + + + /// + /// Saves the TMD. + /// + /// + public void Save(string savePath) + { + Save(savePath, false); + } + + /// + /// Saves the TMD. If fakeSign is true, the Ticket will be fakesigned. + /// + /// + /// + public void Save(string savePath, bool fakeSign) + { + if (fakeSign) this.fakeSign = true; + if (File.Exists(savePath)) File.Delete(savePath); + + using (FileStream fs = new FileStream(savePath, FileMode.Create)) + writeToStream(fs); + } + + /// + /// Returns the TMD as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + return ToMemoryStream(false); + } + + /// + /// Returns the TMD as a memory stream. If fakeSign is true, the Ticket will be fakesigned. + /// + /// + /// + public MemoryStream ToMemoryStream(bool fakeSign) + { + if (fakeSign) this.fakeSign = true; + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the TMD as a byte array. + /// + /// + public byte[] ToByteArray() + { + return ToByteArray(false); + } + + /// + /// Returns the TMD as a byte array. If fakeSign is true, the Ticket will be fakesigned. + /// + /// + /// + public byte[] ToByteArray(bool fakeSign) + { + if (fakeSign) this.fakeSign = true; + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + byte[] res = ms.ToArray(); + ms.Dispose(); + return res; + } + + /// + /// Updates the content entries. + /// + /// + /// True if you use the content ID as name (e.g. 0000008a.app). + /// False if you use the index as name (e.g. 00000000.app) + public void UpdateContents(string contentDir) + { + bool namedContentId = true; + for (int i = 0; i < contents.Count; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + contents[i].ContentID.ToString("x8") + ".app")) + { namedContentId = false; break; } + + if (!namedContentId) + for (int i = 0; i < contents.Count; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + contents[i].ContentID.ToString("x8") + ".app")) + throw new Exception("Couldn't find all content files!"); + + byte[][] conts = new byte[contents.Count][]; + + for (int i = 0; i < contents.Count; i++) + { + string file = contentDir + Path.DirectorySeparatorChar + ((namedContentId) ? contents[i].ContentID.ToString("x8") : contents[i].Index.ToString("x8")) + ".app"; + conts[i] = File.ReadAllBytes(file); + } + + updateContents(conts); + } + + /// + /// Updates the content entries. + /// + /// + /// True if you use the content ID as name (e.g. 0000008a.app). + /// False if you use the index as name (e.g. 00000000.app) + public void UpdateContents(byte[][] contents) + { + updateContents(contents); + } + + /// + /// Returns the Upper Title ID as a string. + /// + /// + public string GetUpperTitleID() + { + byte[] titleBytes = BitConverter.GetBytes(Shared.Swap((uint)titleId)); + return new string(new char[] { (char)titleBytes[0], (char)titleBytes[1], (char)titleBytes[2], (char)titleBytes[3] }); + } + + /// + /// The Number of memory blocks the content will take. + /// + /// + public string GetNandBlocks() + { + return calculateNandBlocks(); + } + + /// + /// Adds a TMD content. + /// + /// + public void AddContent(TMD_Content content) + { + contents.Add(content); + + numOfContents = (ushort)contents.Count; + } + + /// + /// Removes the content with the given index. + /// + /// + public void RemoveContent(int contentIndex) + { + for (int i = 0; i < numOfContents; i++) + if (contents[i].Index == contentIndex) + { contents.RemoveAt(i); break; } + + numOfContents = (ushort)contents.Count; + } + + /// + /// Removes the content with the given ID. + /// + /// + public void RemoveContentByID(int contentId) + { + for (int i = 0; i < numOfContents; i++) + if (contents[i].ContentID == contentId) + { contents.RemoveAt(i); break; } + + numOfContents = (ushort)contents.Count; + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + fireDebug("Writing TMD..."); + + if (fakeSign) + { fireDebug(" Clearing Signature..."); signature = new byte[256]; } //Clear Signature if we fake Sign + + MemoryStream ms = new MemoryStream(); + ms.Seek(0, SeekOrigin.Begin); + + fireDebug(" Writing Signature Exponent... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(signatureExponent)), 0, 4); + + fireDebug(" Writing Signature... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(signature, 0, signature.Length); + + fireDebug(" Writing Padding... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(padding, 0, padding.Length); + + fireDebug(" Writing Issuer... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(issuer, 0, issuer.Length); + + fireDebug(" Writing Version... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(version); + + fireDebug(" Writing CA Crl Version... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(caCrlVersion); + + fireDebug(" Writing Signer Crl Version... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(signerCrlVersion); + + fireDebug(" Writing Padding Byte... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(paddingByte); + + fireDebug(" Writing Startup IOS... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(startupIos)), 0, 8); + + fireDebug(" Writing Title ID... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(titleId)), 0, 8); + + fireDebug(" Writing Title Type... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(titleType)), 0, 4); + + fireDebug(" Writing Group ID... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(groupId)), 0, 2); + + fireDebug(" Writing Padding2... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(padding2)), 0, 2); + + fireDebug(" Writing Region... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(region)), 0, 2); + + fireDebug(" Writing Reserved... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(reserved, 0, reserved.Length); + + fireDebug(" Writing Access Rights... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(accessRights)), 0, 4); + + fireDebug(" Writing Title Version... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(titleVersion)), 0, 2); + + fireDebug(" Writing NumOfContents... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(numOfContents)), 0, 2); + + fireDebug(" Writing Boot Index... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(bootIndex)), 0, 2); + + fireDebug(" Writing Padding3... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(padding3)), 0, 2); + + //Write Contents + List contentList = new List(); + for (int i = 0; i < contents.Count; i++) + contentList.Add(new ContentIndices(i, contents[i].Index)); + + contentList.Sort(); + + for (int i = 0; i < contentList.Count; i++) + { + fireDebug(" Writing Content #{1} of {2}... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper().ToUpper(), i + 1, numOfContents); + + ms.Write(BitConverter.GetBytes(Shared.Swap(contents[contentList[i].Index].ContentID)), 0, 4); + ms.Write(BitConverter.GetBytes(Shared.Swap(contents[contentList[i].Index].Index)), 0, 2); + ms.Write(BitConverter.GetBytes(Shared.Swap((ushort)contents[contentList[i].Index].Type)), 0, 2); + ms.Write(BitConverter.GetBytes(Shared.Swap(contents[contentList[i].Index].Size)), 0, 8); + + ms.Write(contents[contentList[i].Index].Hash, 0, contents[contentList[i].Index].Hash.Length); + } + + //fake Sign + byte[] tmd = ms.ToArray(); + ms.Dispose(); + + if (fakeSign) + { + fireDebug(" Fakesigning TMD..."); + + byte[] hash = new byte[20]; + SHA1 s = SHA1.Create(); + + for (ushort i = 0; i < 0xFFFF; i++) + { + byte[] bytes = BitConverter.GetBytes(i); + tmd[482] = bytes[1]; tmd[483] = bytes[0]; + + hash = s.ComputeHash(tmd); + if (hash[0] == 0x00) + { fireDebug(" -> Signed ({0})", i); break; } //Win! It's signed... + + if (i == 0xFFFF - 1) + { fireDebug(" -> Signing Failed..."); throw new Exception("Fakesigning failed..."); } + } + + s.Clear(); + } + + writeStream.Seek(0, SeekOrigin.Begin); + writeStream.Write(tmd, 0, tmd.Length); + + fireDebug("Writing TMD Finished..."); + } + + private void updateContents(byte[][] conts) + { + SHA1 s = SHA1.Create(); + + for (int i = 0; i < contents.Count; i++) + { + contents[i].Size = (ulong)conts[i].Length; + contents[i].Hash = s.ComputeHash(conts[i]); + } + + s.Clear(); + } + + private void parseTmd(Stream tmdFile) + { + fireDebug("Pasing TMD..."); + + tmdFile.Seek(0, SeekOrigin.Begin); + byte[] temp = new byte[8]; + + fireDebug(" Reading Signature Exponent... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 4); + signatureExponent = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + fireDebug(" Reading Signature... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(signature, 0, signature.Length); + + fireDebug(" Reading Padding... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(padding, 0, padding.Length); + + fireDebug(" Reading Issuer... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(issuer, 0, issuer.Length); + + fireDebug(" Reading Version... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading CA Crl Version... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading Signer Crl Version... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading Padding Byte... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 4); + version = temp[0]; + caCrlVersion = temp[1]; + signerCrlVersion = temp[2]; + paddingByte = temp[3]; + + fireDebug(" Reading Startup IOS... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 8); + startupIos = Shared.Swap(BitConverter.ToUInt64(temp, 0)); + + fireDebug(" Reading Title ID... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 8); + titleId = Shared.Swap(BitConverter.ToUInt64(temp, 0)); + + fireDebug(" Reading Title Type... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 4); + titleType = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + fireDebug(" Reading Group ID... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 2); + groupId = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + + fireDebug(" Reading Padding2... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 2); + padding2 = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + + fireDebug(" Reading Region... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 2); + region = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + + fireDebug(" Reading Reserved... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(reserved, 0, reserved.Length); + + fireDebug(" Reading Access Rights... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 4); + accessRights = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + fireDebug(" Reading Title Version... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading NumOfContents... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading Boot Index... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading Padding3... (Offset: 0x{0})", tmdFile.Position.ToString("x8").ToUpper()); + tmdFile.Read(temp, 0, 8); + titleVersion = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + numOfContents = Shared.Swap(BitConverter.ToUInt16(temp, 2)); + bootIndex = Shared.Swap(BitConverter.ToUInt16(temp, 4)); + padding3 = Shared.Swap(BitConverter.ToUInt16(temp, 6)); + + contents = new List(); + + //Read Contents + for (int i = 0; i < numOfContents; i++) + { + fireDebug(" Reading Content #{0} of {1}... (Offset: 0x{2})", i + 1, numOfContents, tmdFile.Position.ToString("x8").ToUpper().ToUpper()); + + TMD_Content tempContent = new TMD_Content(); + tempContent.Hash = new byte[20]; + + tmdFile.Read(temp, 0, 8); + tempContent.ContentID = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + tempContent.Index = Shared.Swap(BitConverter.ToUInt16(temp, 4)); + tempContent.Type = (ContentType)Shared.Swap(BitConverter.ToUInt16(temp, 6)); + + tmdFile.Read(temp, 0, 8); + tempContent.Size = Shared.Swap(BitConverter.ToUInt64(temp, 0)); + + tmdFile.Read(tempContent.Hash, 0, tempContent.Hash.Length); + + contents.Add(tempContent); + } + + fireDebug("Pasing TMD Finished..."); + } + + private string calculateNandBlocks() + { + int nandSizeMin = 0; + int nandSizeMax = 0; + + for (int i = 0; i < numOfContents; i++) + { + nandSizeMax += (int)contents[i].Size; + if (contents[i].Type == ContentType.Normal) nandSizeMin += (int)contents[i].Size; + } + + int blocksMin = (int)Math.Ceiling((double)((double)nandSizeMin / (128 * 1024))); + int blocksMax = (int)Math.Ceiling((double)((double)nandSizeMax / (128 * 1024))); + + if (blocksMin == blocksMax) return blocksMax.ToString(); + else return string.Format("{0} - {1}", blocksMin, blocksMax); + } + #endregion + + #region Events + /// + /// Fires debugging messages. You may write them into a log file or log textbox. + /// + public event EventHandler Debug; + + private void fireDebug(string debugMessage, params object[] args) + { + EventHandler debug = Debug; + if (debug != null) + debug(new object(), new MessageEventArgs(string.Format(debugMessage, args))); + } + #endregion + } + + public class TMD_Content + { + private uint contentId; + private ushort index; + private ushort type; + private ulong size; + private byte[] hash = new byte[20]; + + public uint ContentID { get { return contentId; } set { contentId = value; } } + public ushort Index { get { return index; } set { index = value; } } + public ContentType Type { get { return (ContentType)type; } set { type = (ushort)value; } } + public ulong Size { get { return size; } set { size = value; } } + public byte[] Hash { get { return hash; } set { hash = value; } } + } +} diff --git a/NUS Downloader/Ticket.cs b/NUS Downloader/Ticket.cs new file mode 100644 index 0000000..8f9e071 --- /dev/null +++ b/NUS Downloader/Ticket.cs @@ -0,0 +1,609 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace libWiiSharp +{ + public enum CommonKeyType : byte + { + Standard = 0x00, + Korean = 0x01, + } + + public class Ticket : IDisposable + { + private byte newKeyIndex = (byte)CommonKeyType.Standard; + private byte[] decryptedTitleKey = new byte[16]; + private bool fakeSign = false; + private bool titleKeyChanged = false; + private byte[] newEncryptedTitleKey = new byte[0]; + private bool reDecrypt = false; + + private uint signatureExponent = 0x00010001; + private byte[] signature = new byte[256]; + private byte[] padding = new byte[60]; + private byte[] issuer = new byte[64]; + private byte[] unknown = new byte[63]; + private byte[] encryptedTitleKey = new byte[16]; + private byte unknown2; + private ulong ticketId; + private uint consoleId; + private ulong titleId; + private ushort unknown3 = 0xFFFF; + private ushort numOfDlc; + private ulong unknown4; + private byte padding2; + private byte commonKeyIndex = (byte)CommonKeyType.Standard; + private byte[] unknown5 = new byte[48]; + private byte[] unknown6 = new byte[32]; //0xFF + private ushort padding3; + private uint enableTimeLimit; + private uint timeLimit; + private byte[] padding4 = new byte[88]; + + /// + /// The Title Key the WADs content is encrypted with. + /// + public byte[] TitleKey { get { return decryptedTitleKey; } set { decryptedTitleKey = value; titleKeyChanged = true; reDecrypt = false; } } + /// + /// Defines which Common Key is used (Standard / Korean). + /// + public CommonKeyType CommonKeyIndex { get { return (CommonKeyType)newKeyIndex; } set { newKeyIndex = (byte)value; } } + /// + /// The Ticket ID. + /// + public ulong TicketID { get { return ticketId; } set { ticketId = value; } } + /// + /// The Console ID. + /// + public uint ConsoleID { get { return consoleId; } set { consoleId = value; } } + /// + /// The Title ID. + /// + public ulong TitleID { get { return titleId; } set { titleId = value; if (reDecrypt) reDecryptTitleKey(); } } + /// + /// Number of DLC. + /// + public ushort NumOfDLC { get { return numOfDlc; } set { numOfDlc = value; } } + /// + /// If true, the Ticket will be fakesigned while saving. + /// + public bool FakeSign { get { return fakeSign; } set { fakeSign = value; } } + /// + /// True if the Title Key was changed. + /// + public bool TitleKeyChanged { get { return titleKeyChanged; } } + + #region IDisposable Members + private bool isDisposed = false; + + ~Ticket() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !isDisposed) + { + decryptedTitleKey = null; + newEncryptedTitleKey = null; + signature = null; + padding = null; + issuer = null; + unknown = null; + encryptedTitleKey = null; + unknown5 = null; + unknown6 = null; + padding4 = null; + } + + isDisposed = true; + } + #endregion + + #region Public Functions + /// + /// Loads a tik file. + /// + /// + /// + public static Ticket Load(string pathToTicket) + { + return Load(File.ReadAllBytes(pathToTicket)); + } + + /// + /// Loads a tik file. + /// + /// + /// + public static Ticket Load(byte[] ticket) + { + Ticket tik = new Ticket(); + MemoryStream ms = new MemoryStream(ticket); + + try { tik.parseTicket(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return tik; + } + + /// + /// Loads a tik file. + /// + /// + /// + public static Ticket Load(Stream ticket) + { + Ticket tik = new Ticket(); + tik.parseTicket(ticket); + return tik; + } + + + + /// + /// Loads a tik file. + /// + /// + public void LoadFile(string pathToTicket) + { + LoadFile(File.ReadAllBytes(pathToTicket)); + } + + /// + /// Loads a tik file. + /// + /// + public void LoadFile(byte[] ticket) + { + MemoryStream ms = new MemoryStream(ticket); + + try { parseTicket(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + } + + /// + /// Loads a tik file. + /// + /// + public void LoadFile(Stream ticket) + { + parseTicket(ticket); + } + + + + /// + /// Saves the Ticket. + /// + /// + public void Save(string savePath) + { + Save(savePath, false); + } + + /// + /// Saves the Ticket. If fakeSign is true, the Ticket will be fakesigned. + /// + /// + /// + public void Save(string savePath, bool fakeSign) + { + if (fakeSign) this.fakeSign = true; + if (File.Exists(savePath)) File.Delete(savePath); + + using (FileStream fs = new FileStream(savePath, FileMode.Create)) + writeToStream(fs); + } + + /// + /// Returns the Ticket as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + return ToMemoryStream(false); + } + + /// + /// Returns the Ticket as a memory stream. If fakeSign is true, the Ticket will be fakesigned. + /// + /// + /// + public MemoryStream ToMemoryStream(bool fakeSign) + { + if (fakeSign) this.fakeSign = true; + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the Ticket as a byte array. + /// + /// + public byte[] ToByteArray() + { + return ToByteArray(false); + } + + /// + /// Returns the Ticket as a byte array. If fakeSign is true, the Ticket will be fakesigned. + /// + /// + /// + public byte[] ToByteArray(bool fakeSign) + { + if (fakeSign) this.fakeSign = true; + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + byte[] res = ms.ToArray(); + ms.Dispose(); + return res; + } + + /// + /// This will set a new encrypted Title Key (i.e. the one that you can "read" in the Ticket). + /// + /// + public void SetTitleKey(string newTitleKey) + { + SetTitleKey(newTitleKey.ToCharArray()); + } + + /// + /// This will set a new encrypted Title Key (i.e. the one that you can "read" in the Ticket). + /// + /// + public void SetTitleKey(char[] newTitleKey) + { + if (newTitleKey.Length != 16) + throw new Exception("The title key must be 16 characters long!"); + + for (int i = 0; i < 16; i++) + encryptedTitleKey[i] = (byte)newTitleKey[i]; + + decryptTitleKey(); + titleKeyChanged = true; + + reDecrypt = true; + newEncryptedTitleKey = encryptedTitleKey; + } + + /// + /// This will set a new encrypted Title Key (i.e. the one that you can "read" in the Ticket). + /// + /// + public void SetTitleKey(byte[] newTitleKey) + { + if (newTitleKey.Length != 16) + throw new Exception("The title key must be 16 characters long!"); + + encryptedTitleKey = newTitleKey; + decryptTitleKey(); + titleKeyChanged = true; + + reDecrypt = true; + newEncryptedTitleKey = newTitleKey; + } + + /// + /// Returns the Upper Title ID as a string. + /// + /// + public string GetUpperTitleID() + { + byte[] titleBytes = BitConverter.GetBytes(Shared.Swap((uint)titleId)); + return new string(new char[] { (char)titleBytes[0], (char)titleBytes[1], (char)titleBytes[2], (char)titleBytes[3] }); + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + fireDebug("Writing Ticket..."); + + fireDebug(" Encrypting Title Key..."); + encryptTitleKey(); + fireDebug(" -> Decrypted Title Key: {0}", Shared.ByteArrayToString(decryptedTitleKey)); + fireDebug(" -> Encrypted Title Key: {0}", Shared.ByteArrayToString(encryptedTitleKey)); + + if (fakeSign) + { fireDebug(" Clearing Signature..."); signature = new byte[256]; } //Clear Signature if we fake Sign + + MemoryStream ms = new MemoryStream(); + ms.Seek(0, SeekOrigin.Begin); + + fireDebug(" Writing Signature Exponent... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(signatureExponent)), 0, 4); + + fireDebug(" Writing Signature... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(signature, 0, signature.Length); + + fireDebug(" Writing Padding... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(padding, 0, padding.Length); + + fireDebug(" Writing Issuer... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(issuer, 0, issuer.Length); + + fireDebug(" Writing Unknown... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(unknown, 0, unknown.Length); + + fireDebug(" Writing Title Key... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(encryptedTitleKey, 0, encryptedTitleKey.Length); + + fireDebug(" Writing Unknown2... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(unknown2); + + fireDebug(" Writing Ticket ID... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(ticketId)), 0, 8); + + fireDebug(" Writing Console ID... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(consoleId)), 0, 4); + + fireDebug(" Writing Title ID... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(titleId)), 0, 8); + + fireDebug(" Writing Unknwon3... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(unknown3)), 0, 2); + + fireDebug(" Writing NumOfDLC... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(numOfDlc)), 0, 2); + + fireDebug(" Writing Unknwon4... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(unknown4)), 0, 8); + + fireDebug(" Writing Padding2... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(padding2); + + fireDebug(" Writing Common Key Index... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.WriteByte(commonKeyIndex); + + fireDebug(" Writing Unknown5... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(unknown5, 0, unknown5.Length); + + fireDebug(" Writing Unknown6... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(unknown6, 0, unknown6.Length); + + fireDebug(" Writing Padding3... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(padding3)), 0, 2); + + fireDebug(" Writing Enable Time Limit... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(enableTimeLimit)), 0, 4); + + fireDebug(" Writing Time Limit... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(BitConverter.GetBytes(Shared.Swap(timeLimit)), 0, 4); + + fireDebug(" Writing Padding4... (Offset: 0x{0})", ms.Position.ToString("x8").ToUpper()); + ms.Write(padding4, 0, padding4.Length); + + byte[] tik = ms.ToArray(); + ms.Dispose(); + + //fake Sign + if (fakeSign) + { + fireDebug(" Fakesigning Ticket..."); + + byte[] hash = new byte[20]; + SHA1 s = SHA1.Create(); + + for (ushort i = 0; i < 0xFFFF; i++) + { + byte[] bytes = BitConverter.GetBytes(i); + tik[498] = bytes[1]; tik[499] = bytes[0]; + + hash = s.ComputeHash(tik); + if (hash[0] == 0x00) + { fireDebug(" -> Signed ({0})", i); break; } //Win! It's signed... + + if (i == 0xFFFF - 1) + { fireDebug(" -> Signing Failed..."); throw new Exception("Fakesigning failed..."); } + } + + s.Clear(); + } + + writeStream.Seek(0, SeekOrigin.Begin); + writeStream.Write(tik, 0, tik.Length); + + fireDebug("Writing Ticket Finished..."); + } + + private void parseTicket(Stream ticketFile) + { + fireDebug("Parsing Ticket..."); + + ticketFile.Seek(0, SeekOrigin.Begin); + byte[] temp = new byte[8]; + + fireDebug(" Reading Signature Exponent... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 4); + signatureExponent = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + fireDebug(" Reading Signature... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(signature, 0, signature.Length); + + fireDebug(" Reading Padding... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(padding, 0, padding.Length); + + fireDebug(" Reading Issuer... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(issuer, 0, issuer.Length); + + fireDebug(" Reading Unknown... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(unknown, 0, unknown.Length); + + fireDebug(" Reading Title Key... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(encryptedTitleKey, 0, encryptedTitleKey.Length); + + fireDebug(" Reading Unknown2... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + unknown2 = (byte)ticketFile.ReadByte(); + + fireDebug(" Reading Ticket ID.. (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 8); + ticketId = Shared.Swap(BitConverter.ToUInt64(temp, 0)); + + fireDebug(" Reading Console ID... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 4); + consoleId = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + fireDebug(" Reading Title ID... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 8); + titleId = Shared.Swap(BitConverter.ToUInt64(temp, 0)); + + fireDebug(" Reading Unknown3... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading NumOfDLC... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 4); + unknown3 = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + numOfDlc = Shared.Swap(BitConverter.ToUInt16(temp, 2)); + + fireDebug(" Reading Unknown4... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 8); + unknown4 = Shared.Swap(BitConverter.ToUInt64(temp, 0)); + + fireDebug(" Reading Padding2... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + padding2 = (byte)ticketFile.ReadByte(); + + fireDebug(" Reading Common Key Index... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + commonKeyIndex = (byte)ticketFile.ReadByte(); + + newKeyIndex = commonKeyIndex; + + fireDebug(" Reading Unknown5... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(unknown5, 0, unknown5.Length); + + fireDebug(" Reading Unknown6... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(unknown6, 0, unknown6.Length); + + fireDebug(" Reading Padding3... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 2); + padding3 = Shared.Swap(BitConverter.ToUInt16(temp, 0)); + + fireDebug(" Reading Enable Time Limit... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + fireDebug(" Reading Time Limit... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(temp, 0, 8); + enableTimeLimit = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + timeLimit = Shared.Swap(BitConverter.ToUInt32(temp, 4)); + + fireDebug(" Reading Padding4... (Offset: 0x{0})", ticketFile.Position.ToString("x8").ToUpper()); + ticketFile.Read(padding4, 0, padding4.Length); + + fireDebug(" Decrypting Title Key..."); + decryptTitleKey(); + fireDebug(" -> Encrypted Title Key: {0}", Shared.ByteArrayToString(encryptedTitleKey)); + fireDebug(" -> Decrypted Title Key: {0}", Shared.ByteArrayToString(decryptedTitleKey)); + + fireDebug("Parsing Ticket Finished..."); + } + + private void decryptTitleKey() + { + byte[] ckey = (commonKeyIndex == 0x01) ? CommonKey.GetKoreanKey() : CommonKey.GetStandardKey(); + byte[] iv = BitConverter.GetBytes(Shared.Swap(titleId)); + Array.Resize(ref iv, 16); + + RijndaelManaged rm = new RijndaelManaged(); + rm.Mode = CipherMode.CBC; + rm.Padding = PaddingMode.None; + rm.KeySize = 128; + rm.BlockSize = 128; + rm.Key = ckey; + rm.IV = iv; + + ICryptoTransform decryptor = rm.CreateDecryptor(); + + MemoryStream ms = new MemoryStream(encryptedTitleKey); + CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); + + cs.Read(decryptedTitleKey, 0, decryptedTitleKey.Length); + + cs.Dispose(); + ms.Dispose(); + decryptor.Dispose(); + rm.Clear(); + } + + private void encryptTitleKey() + { + commonKeyIndex = newKeyIndex; + byte[] ckey = (commonKeyIndex == 0x01) ? CommonKey.GetKoreanKey() : CommonKey.GetStandardKey(); + byte[] iv = BitConverter.GetBytes(Shared.Swap(titleId)); + Array.Resize(ref iv, 16); + + RijndaelManaged rm = new RijndaelManaged(); + rm.Mode = CipherMode.CBC; + rm.Padding = PaddingMode.None; + rm.KeySize = 128; + rm.BlockSize = 128; + rm.Key = ckey; + rm.IV = iv; + + ICryptoTransform encryptor = rm.CreateEncryptor(); + + MemoryStream ms = new MemoryStream(decryptedTitleKey); + CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Read); + + cs.Read(encryptedTitleKey, 0, encryptedTitleKey.Length); + + cs.Dispose(); + ms.Dispose(); + encryptor.Dispose(); + rm.Clear(); + } + + private void reDecryptTitleKey() + { + encryptedTitleKey = newEncryptedTitleKey; + decryptTitleKey(); + } + #endregion + + #region Events + /// + /// Fires debugging messages. You may write them into a log file or log textbox. + /// + public event EventHandler Debug; + + private void fireDebug(string debugMessage, params object[] args) + { + EventHandler debug = Debug; + if (debug != null) + debug(new object(), new MessageEventArgs(string.Format(debugMessage, args))); + } + #endregion + } +} diff --git a/NUS Downloader/U8.cs b/NUS Downloader/U8.cs new file mode 100644 index 0000000..409d3af --- /dev/null +++ b/NUS Downloader/U8.cs @@ -0,0 +1,1120 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Security.Cryptography; + +namespace libWiiSharp +{ + public enum U8_NodeType : ushort + { + File = 0x0000, + Directory = 0x0100, + } + + public class U8 : IDisposable + { + private const int dataPadding = 32; + private Headers.HeaderType headerType = Headers.HeaderType.None; + private object header = null; + private U8_Header u8Header = new U8_Header(); + private U8_Node rootNode = new U8_Node(); + private List u8Nodes = new List(); + private List stringTable = new List(); + private List data = new List(); + private int iconSize = -1; + private int bannerSize = -1; + private int soundSize = -1; + private bool lz77 = false; + + /// + /// The type of the Header of the U8 file. + /// + public Headers.HeaderType HeaderType { get { return headerType; } } + /// + /// The Header of the U8 file as an object. Will be null if the file has no Header. + /// + public object Header { get { return header; } } + /// + /// The Rootnode of the U8 file. + /// + public U8_Node RootNode { get { return rootNode; } } + /// + /// The Nodes of the U8 file. + /// + public List Nodes { get { return u8Nodes; } } + /// + /// The string table of the U8 file. + /// + public string[] StringTable { get { return stringTable.ToArray(); } } + /// + /// The actual data (files) in the U8 file. Will be an empty byte array for directory entries. + /// + public byte[][] Data { get { return data.ToArray(); } } + + /// + /// The Number of Nodes WITHOUT the Rootnode. + /// + public int NumOfNodes { get { return (int)rootNode.SizeOfData - 1; } } + /// + /// The size of the icon.bin (if the U8 files contains an icon.bin). + /// + public int IconSize { get { return iconSize; } } + /// + /// The size of the banner.bin (if the U8 files contains an banner.bin). + /// + public int BannerSize { get { return bannerSize; } } + /// + /// The size of the sound.bin (if the U8 files contains an sound.bin). + /// + public int SoundSize { get { return soundSize; } } + /// + /// If true, the U8 file will be Lz77 compressed while saving. + /// + public bool Lz77Compress { get { return lz77; } set { lz77 = value; } } + + public U8() + { + rootNode.Type = U8_NodeType.Directory; + } + + #region IDisposable Members + private bool isDisposed = false; + + ~U8() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !isDisposed) + { + header = null; + u8Header = null; + rootNode = null; + + u8Nodes.Clear(); + u8Nodes = null; + + stringTable.Clear(); + stringTable = null; + + data.Clear(); + data = null; + } + + isDisposed = true; + } + #endregion + + #region Public Functions + /// + /// Checks whether a file is a U8 file or not. + /// + /// + /// + public static bool IsU8(string pathToFile) + { + return IsU8(File.ReadAllBytes(pathToFile)); + } + + /// + /// Checks whether a file is a U8 file or not. + /// + /// + /// + public static bool IsU8(byte[] file) + { + if (Lz77.IsLz77Compressed(file)) + { + byte[] partOfFile = new byte[(file.Length > 2000) ? 2000 : file.Length]; + + for (int i = 0; i < partOfFile.Length; i++) + partOfFile[i] = file[i]; + + Lz77 l = new Lz77(); + partOfFile = l.Decompress(partOfFile); + + return IsU8(partOfFile); + } + else + { + Headers.HeaderType h = Headers.DetectHeader(file); + return (Shared.Swap(BitConverter.ToUInt32(file, (int)h)) == 0x55AA382D); + } + } + + + + /// + /// Loads a U8 file. + /// + /// + /// + public static U8 Load(string pathToU8) + { + return Load(File.ReadAllBytes(pathToU8)); + } + + /// + /// Loads a U8 file. + /// + /// + /// + public static U8 Load(byte[] u8File) + { + U8 u = new U8(); + MemoryStream ms = new MemoryStream(u8File); + + try { u.parseU8(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return u; + } + + /// + /// Loads a U8 file. + /// + /// + /// + public static U8 Load(Stream u8File) + { + U8 u = new U8(); + u.parseU8(u8File); + return u; + } + + /// + /// Creates a U8 file. + /// + /// + /// + public static U8 FromDirectory(string pathToDirectory) + { + U8 u = new U8(); + u.createFromDir(pathToDirectory); + return u; + } + + + + /// + /// Loads a U8 file. + /// + /// + public void LoadFile(string pathToU8) + { + LoadFile(File.ReadAllBytes(pathToU8)); + } + + /// + /// Loads a U8 file. + /// + /// + public void LoadFile(byte[] u8File) + { + MemoryStream ms = new MemoryStream(u8File); + + try { parseU8(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + } + + /// + /// Loads a U8 file. + /// + /// + public void LoadFile(Stream u8File) + { + parseU8(u8File); + } + + /// + /// Creates a U8 file. + /// + /// + public void CreateFromDirectory(string pathToDirectory) + { + createFromDir(pathToDirectory); + } + + + + /// + /// Saves the U8 file. + /// + /// + public void Save(string savePath) + { + if (File.Exists(savePath)) File.Delete(savePath); + + using (FileStream fs = new FileStream(savePath, FileMode.Create)) + writeToStream(fs); + } + + /// + /// Returns the U8 file as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the U8 file as a byte array. + /// + /// + public byte[] ToByteArray() + { + return ToMemoryStream().ToArray(); + } + + /// + /// Unpacks the U8 file to given directory. + /// + /// + public void Unpack(string saveDir) + { + unpackToDir(saveDir); + } + + /// + /// Unpacks the U8 file to given directory. + /// + /// + public void Extract(string saveDir) + { + unpackToDir(saveDir); + } + + /// + /// Adds an IMET Header to the U8 file. + /// + /// + /// + public void AddHeaderImet(bool shortImet, params string[] titles) + { + if (iconSize == -1) + throw new Exception("icon.bin wasn't found!"); + else if (bannerSize == -1) + throw new Exception("banner.bin wasn't found!"); + else if (soundSize == -1) + throw new Exception("sound.bin wasn't found!"); + + header = Headers.IMET.Create(shortImet, iconSize, bannerSize, soundSize, titles); + headerType = (shortImet) ? Headers.HeaderType.ShortIMET : Headers.HeaderType.IMET; + } + + /// + /// Adds an IMD5 Header to the U8 file. + /// + public void AddHeaderImd5() + { + headerType = Headers.HeaderType.IMD5; + } + + /// + /// Replaces the file with the given index. + /// + /// + /// + public void ReplaceFile(int fileIndex, string pathToNewFile, bool changeFileName = false) + { + if (u8Nodes[fileIndex].Type == U8_NodeType.Directory) + throw new Exception("You can't replace a directory with a file!"); + + data[fileIndex] = File.ReadAllBytes(pathToNewFile); + if (changeFileName) stringTable[fileIndex] = Path.GetFileName(pathToNewFile); + + if (stringTable[fileIndex].ToLower() == "icon.bin") + iconSize = getRealSize(File.ReadAllBytes(pathToNewFile)); + else if (stringTable[fileIndex].ToLower() == "banner.bin") + bannerSize = getRealSize(File.ReadAllBytes(pathToNewFile)); + else if (stringTable[fileIndex].ToLower() == "sound.bin") + soundSize = getRealSize(File.ReadAllBytes(pathToNewFile)); + } + + /// + /// Replaces the file with the given index. + /// + /// + /// + public void ReplaceFile(int fileIndex, byte[] newData) + { + if (u8Nodes[fileIndex].Type == U8_NodeType.Directory) + throw new Exception("You can't replace a directory with a file!"); + + data[fileIndex] = newData; + + if (stringTable[fileIndex].ToLower() == "icon.bin") + iconSize = getRealSize(newData); + else if (stringTable[fileIndex].ToLower() == "banner.bin") + bannerSize = getRealSize(newData); + else if (stringTable[fileIndex].ToLower() == "sound.bin") + soundSize = getRealSize(newData); + } + + /// + /// Returns the index of the directory or file with the given name. + /// If no matching Node is found, -1 will be returned. + /// + /// + /// + public int GetNodeIndex(string fileOrDirName) + { + for (int i = 0; i < u8Nodes.Count; i++) + if (stringTable[i].ToLower() == fileOrDirName.ToLower()) return i; + + return -1; + } + + /// + /// Changes the name of a node. + /// + /// + /// + public void RenameNode(int index, string newName) + { + stringTable[index] = newName; + } + + /// + /// Changes the name of a node. + /// + /// + /// + public void RenameNode(string oldName, string newName) + { + stringTable[GetNodeIndex(oldName)] = newName; + } + + /// + /// Adds a directory to the U8 file. + /// Path must be like this: "/arc/timg/newFolder". + /// + /// + public void AddDirectory(string path) + { + addEntry(path, new byte[0]); + } + + /// + /// Adds a file to the U8 file. + /// Path must be like this: "/arc/timg/newFile.tpl". + /// + /// + /// + public void AddFile(string path, byte[] data) + { + addEntry(path, data); + } + + /// + /// Removes a directory from the U8 file. + /// If the directory contains files/dirs, they will also be deleted. + /// Path must be like this: "/arc/timg/folderToDelete". + /// + /// + public void RemoveDirectory(string path) + { + removeEntry(path); + } + + /// + /// Removes a file from the U8 file. + /// Path must be like this: "/arc/timg/fileToDelete.tpl". + /// + /// + public void RemoveFile(string path) + { + removeEntry(path); + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + fireDebug("Writing U8 File..."); + + //Update Rootnode + fireDebug(" Updating Rootnode..."); + rootNode.SizeOfData = (uint)u8Nodes.Count + 1; + + MemoryStream u8Stream = new MemoryStream(); + + //Write Stringtable + u8Stream.Seek(u8Header.OffsetToRootNode + ((u8Nodes.Count + 1) * 12), SeekOrigin.Begin); + + fireDebug(" Writing String Table... (Offset: 0x{0})", u8Stream.Position.ToString("x8").ToUpper()); + u8Stream.WriteByte(0x00); + + int stringTablePosition = (int)u8Stream.Position - 1; + for (int i = 0; i < u8Nodes.Count; i++) + { + fireDebug(" -> Entry #{1} of {2}: \"{3}\"... (Offset: 0x{0})", u8Stream.Position.ToString("x8").ToUpper(), i + 1, u8Nodes.Count, stringTable[i]); + + u8Nodes[i].OffsetToName = (ushort)(u8Stream.Position - stringTablePosition); + byte[] stringBytes = System.Text.ASCIIEncoding.ASCII.GetBytes(stringTable[i]); + + u8Stream.Write(stringBytes, 0, stringBytes.Length); + u8Stream.WriteByte(0x00); + } + + u8Header.HeaderSize = (uint)(u8Stream.Position - u8Header.OffsetToRootNode); + u8Header.OffsetToData = 0; + + //Write Data + for (int i = 0; i < u8Nodes.Count; i++) + { + fireProgress((i + 1) * 100 / u8Nodes.Count); + + if (u8Nodes[i].Type == U8_NodeType.File) + { + u8Stream.Seek(Shared.AddPadding((int)u8Stream.Position, dataPadding), SeekOrigin.Begin); + + fireDebug(" Writing Data #{1} of {2}... (Offset: 0x{0})", u8Stream.Position.ToString("x8").ToUpper(), i + 1, u8Nodes.Count); + + if (u8Header.OffsetToData == 0) u8Header.OffsetToData = (uint)u8Stream.Position; + u8Nodes[i].OffsetToData = (uint)u8Stream.Position; + u8Nodes[i].SizeOfData = (uint)data[i].Length; + + u8Stream.Write(data[i], 0, data[i].Length); + } + else fireDebug(" Node #{0} of {1} is a Directory...", i + 1, u8Nodes.Count); + } + + //Pad End to 16 bytes + while (u8Stream.Position % 16 != 0) + u8Stream.WriteByte(0x00); + + //Write Header + Nodes + u8Stream.Seek(0, SeekOrigin.Begin); + + fireDebug(" Writing Header... (Offset: 0x{0})", u8Stream.Position.ToString("x8").ToUpper()); + u8Header.Write(u8Stream); + + fireDebug(" Writing Rootnode... (Offset: 0x{0})", u8Stream.Position.ToString("x8").ToUpper()); + rootNode.Write(u8Stream); + + for (int i = 0; i < u8Nodes.Count; i++) + { + fireDebug(" Writing Node Entry #{1} of {2}... (Offset: 0x{0})", u8Stream.Position.ToString("x8").ToUpper(), i + 1, u8Nodes.Count); + u8Nodes[i].Write(u8Stream); + } + + byte[] u8Array = u8Stream.ToArray(); + u8Stream.Dispose(); + + if (lz77) + { + fireDebug(" Lz77 Compressing U8 File..."); + + Lz77 l = new Lz77(); + u8Array = l.Compress(u8Array); + } + + //Write File Header + if (headerType == Headers.HeaderType.IMD5) + { + fireDebug(" Adding IMD5 Header..."); + + writeStream.Seek(0, SeekOrigin.Begin); + Headers.IMD5 h = Headers.IMD5.Create(u8Array); + h.Write(writeStream); + } + else if (headerType == Headers.HeaderType.IMET || headerType == Headers.HeaderType.ShortIMET) + { + fireDebug(" Adding IMET Header..."); + + ((Headers.IMET)header).IconSize = (uint)iconSize; + ((Headers.IMET)header).BannerSize = (uint)bannerSize; + ((Headers.IMET)header).SoundSize = (uint)soundSize; + + writeStream.Seek(0, SeekOrigin.Begin); + ((Headers.IMET)header).Write(writeStream); + } + + writeStream.Write(u8Array, 0, u8Array.Length); + + fireDebug("Writing U8 File Finished..."); + } + + private void unpackToDir(string saveDir) + { + fireDebug("Unpacking U8 File to: {0}", saveDir); + + if (!Directory.Exists(saveDir)) Directory.CreateDirectory(saveDir); + + string[] dirs = new string[u8Nodes.Count]; + dirs[0] = saveDir; + int[] dirCount = new int[u8Nodes.Count]; + int dirIndex = 0; + + for (int i = 0; i < u8Nodes.Count; i++) + { + fireDebug(" Unpacking Entry #{0} of {1}", i + 1, u8Nodes.Count); + fireProgress((i + 1) * 100 / u8Nodes.Count); + + switch (u8Nodes[i].Type) + { + case U8_NodeType.Directory: + fireDebug(" -> Directory: \"{0}\"", stringTable[i]); + + if (dirs[dirIndex][dirs[dirIndex].Length - 1] != Path.DirectorySeparatorChar) { dirs[dirIndex] = dirs[dirIndex] + Path.DirectorySeparatorChar; } + Directory.CreateDirectory(dirs[dirIndex] + stringTable[i]); + dirs[dirIndex + 1] = dirs[dirIndex] + stringTable[i]; + dirIndex++; + dirCount[dirIndex] = (int)u8Nodes[i].SizeOfData; + break; + default: + fireDebug(" -> File: \"{0}\"", stringTable[i]); + fireDebug(" -> Size: {0} bytes", data[i].Length); + + using (FileStream fs = new FileStream(dirs[dirIndex] + Path.DirectorySeparatorChar + stringTable[i], FileMode.Create)) + fs.Write(data[i], 0, data[i].Length); + break; + } + + while (dirIndex > 0 && dirCount[dirIndex] == i + 2) + { dirIndex--; } + } + + fireDebug("Unpacking U8 File Finished"); + } + + private void parseU8(Stream u8File) + { + fireDebug("Pasing U8 File..."); + + u8Header = new U8_Header(); + rootNode = new U8_Node(); + u8Nodes = new List(); + stringTable = new List(); + data = new List(); + + fireDebug(" Detecting Header..."); + headerType = Headers.DetectHeader(u8File); + Headers.HeaderType tempType = headerType; + + fireDebug(" -> {0}", headerType.ToString()); + + if (headerType == Headers.HeaderType.IMD5) + { + fireDebug(" Reading IMD5 Header..."); + header = Headers.IMD5.Load(u8File); + + byte[] file = new byte[u8File.Length]; + u8File.Read(file, 0, file.Length); + + MD5 m = MD5.Create(); + byte[] newHash = m.ComputeHash(file, (int)headerType, (int)u8File.Length - (int)headerType); + m.Clear(); + + if (!Shared.CompareByteArrays(newHash, ((Headers.IMD5)header).Hash)) + { + fireDebug(@"/!\ /!\ /!\ Hashes do not match /!\ /!\ /!\"); + fireWarning(string.Format("Hashes of IMD5 header and file do not match! The content might be corrupted!")); + } + } + else if (headerType == Headers.HeaderType.IMET || headerType == Headers.HeaderType.ShortIMET) + { + fireDebug(" Reading IMET Header..."); + header = Headers.IMET.Load(u8File); + + if (!((Headers.IMET)header).HashesMatch) + { + fireDebug(@"/!\ /!\ /!\ Hashes do not match /!\ /!\ /!\"); + fireWarning(string.Format("The hash stored in the IMET header doesn't match the headers hash! The header and/or file might be corrupted!")); + } + } + + fireDebug(" Checking for Lz77 Compression..."); + if (Lz77.IsLz77Compressed(u8File)) + { + fireDebug(" -> Lz77 Compression Found..."); + fireDebug(" Decompressing U8 Data..."); + + Lz77 l = new Lz77(); + Stream decompressedFile = l.Decompress(u8File); + + tempType = Headers.DetectHeader(decompressedFile); + u8File = decompressedFile; + + lz77 = true; + } + + u8File.Seek((int)tempType, SeekOrigin.Begin); + byte[] temp = new byte[4]; + + //Read U8 Header + fireDebug(" Reading U8 Header: Magic... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + u8File.Read(temp, 0, 4); + if (Shared.Swap(BitConverter.ToUInt32(temp, 0)) != u8Header.U8Magic) + { fireDebug(" -> Invalid Magic!"); throw new Exception("U8 Header: Invalid Magic!"); } + + fireDebug(" Reading U8 Header: Offset to Rootnode... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + u8File.Read(temp, 0, 4); + if (Shared.Swap(BitConverter.ToUInt32(temp, 0)) != u8Header.OffsetToRootNode) + { fireDebug(" -> Invalid Offset to Rootnode"); throw new Exception("U8 Header: Invalid Offset to Rootnode!"); } + + fireDebug(" Reading U8 Header: Header Size... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + u8File.Read(temp, 0, 4); + u8Header.HeaderSize = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + fireDebug(" Reading U8 Header: Offset to Data... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + u8File.Read(temp, 0, 4); + u8Header.OffsetToData = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + u8File.Seek(16, SeekOrigin.Current); + + //Read Rootnode + fireDebug(" Reading Rootnode... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + u8File.Read(temp, 0, 4); + rootNode.Type = (U8_NodeType)Shared.Swap(BitConverter.ToUInt16(temp, 0)); + rootNode.OffsetToName = Shared.Swap(BitConverter.ToUInt16(temp, 2)); + + u8File.Read(temp, 0, 4); + rootNode.OffsetToData = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + u8File.Read(temp, 0, 4); + rootNode.SizeOfData = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + int stringTablePosition = (int)((int)tempType + u8Header.OffsetToRootNode + rootNode.SizeOfData * 12); + int nodePosition = (int)u8File.Position; + + //Read Nodes + for (int i = 0; i < rootNode.SizeOfData - 1; i++) + { + fireDebug(" Reading Node #{1} of {2}... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper(), i + 1, rootNode.SizeOfData - 1); + fireProgress((int)((i + 1) * 100 / (rootNode.SizeOfData - 1))); + + U8_Node tempNode = new U8_Node(); + string tempName = string.Empty; + byte[] tempData = new byte[0]; + + //Read Node Entry + u8File.Seek(nodePosition, SeekOrigin.Begin); + + fireDebug(" -> Reading Node Entry... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + u8File.Read(temp, 0, 4); + tempNode.Type = (U8_NodeType)Shared.Swap(BitConverter.ToUInt16(temp, 0)); + tempNode.OffsetToName = Shared.Swap(BitConverter.ToUInt16(temp, 2)); + + u8File.Read(temp, 0, 4); + tempNode.OffsetToData = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + u8File.Read(temp, 0, 4); + tempNode.SizeOfData = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + nodePosition = (int)u8File.Position; + + fireDebug(" -> {0}", tempNode.Type.ToString()); + + //Read Node Name + u8File.Seek(stringTablePosition + tempNode.OffsetToName, SeekOrigin.Begin); + + fireDebug(" -> Reading Node Name... (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + for (;;) + { + char tempChar = (char)u8File.ReadByte(); + if (tempChar == 0x00) break; + + tempName += tempChar; + + if (tempName.Length > 255) break; + } + + fireDebug(" -> {0}", tempName); + + //Read Node Data + if (tempNode.Type == U8_NodeType.File) + { + u8File.Seek((int)tempType + tempNode.OffsetToData, SeekOrigin.Begin); + + fireDebug(" -> Reading Node Data (Offset: 0x{0})", u8File.Position.ToString("x8").ToUpper()); + + tempData = new byte[tempNode.SizeOfData]; + u8File.Read(tempData, 0, tempData.Length); + } + + if (tempName.ToLower() == "icon.bin") iconSize = getRealSize(tempData); + else if (tempName.ToLower() == "banner.bin") bannerSize = getRealSize(tempData); + else if (tempName.ToLower() == "sound.bin") soundSize = getRealSize(tempData); + + u8Nodes.Add(tempNode); + stringTable.Add(tempName); + data.Add(tempData); + } + + fireDebug("Pasing U8 File Finished..."); + } + + private void createFromDir(string path) + { + fireDebug("Creating U8 File from: {0}", path); + + if (path[path.Length - 1] != Path.DirectorySeparatorChar) path += Path.DirectorySeparatorChar; + + fireDebug(" Collecting Content..."); + string[] dirEntries = getDirContent(path, true); + int offsetToName = 1; + int offsetToData = 0; + + fireDebug(" Creating U8 Header..."); + u8Header = new U8_Header(); + rootNode = new U8_Node(); + u8Nodes = new List(); + stringTable = new List(); + data = new List(); + + //Create Rootnode + fireDebug(" Creating Rootnode..."); + rootNode.Type = U8_NodeType.Directory; + rootNode.OffsetToName = 0; + rootNode.OffsetToData = 0; + rootNode.SizeOfData = (uint)dirEntries.Length + 1; + + //Create Nodes + for (int i = 0; i < dirEntries.Length; i++) + { + fireDebug(" Creating Node #{0} of {1}", i + 1, dirEntries.Length); + fireProgress((i + 1) * 100 / dirEntries.Length); + + U8_Node tempNode = new U8_Node(); + byte[] tempData = new byte[0]; + + string tempDirEntry = dirEntries[i].Remove(0, path.Length - 1); + + if (Directory.Exists(dirEntries[i])) //It's a dir + { + fireDebug(" -> Directory"); + + tempNode.Type = U8_NodeType.Directory; + tempNode.OffsetToData = (uint)Shared.CountCharsInString(tempDirEntry, Path.DirectorySeparatorChar); //Recursion + + int size = u8Nodes.Count + 2; + for (int j = 0; j < dirEntries.Length; j++) + if (dirEntries[j].Contains(dirEntries[i] + "\\")) size++; + + tempNode.SizeOfData = (uint)size; + } + else //It's a file + { + fireDebug(" -> File"); + fireDebug(" -> Reading File Data..."); + + tempData = File.ReadAllBytes(dirEntries[i]); + tempNode.Type = U8_NodeType.File; + tempNode.OffsetToData = (uint)offsetToData; + tempNode.SizeOfData = (uint)tempData.Length; + + offsetToData += Shared.AddPadding(offsetToData + tempData.Length, dataPadding); + } + + tempNode.OffsetToName = (ushort)offsetToName; + offsetToName += (Path.GetFileName(dirEntries[i])).Length + 1; + + fireDebug(" -> Reading Name..."); + string tempName = Path.GetFileName(dirEntries[i]); + + if (tempName.ToLower() == "icon.bin") iconSize = getRealSize(tempData); + else if (tempName.ToLower() == "banner.bin") bannerSize = getRealSize(tempData); + else if (tempName.ToLower() == "sound.bin") soundSize = getRealSize(tempData); + + u8Nodes.Add(tempNode); + stringTable.Add(tempName); + data.Add(tempData); + } + + //Update U8 Header + fireDebug(" Updating U8 Header..."); + u8Header.HeaderSize = (uint)(((u8Nodes.Count + 1) * 12) + offsetToName); + u8Header.OffsetToData = (uint)Shared.AddPadding((int)u8Header.OffsetToRootNode + (int)u8Header.HeaderSize, dataPadding); + + //Update dataOffsets + fireDebug(" Calculating Data Offsets..."); + + for (int i = 0; i < u8Nodes.Count; i++) + { + fireDebug(" -> Node #{0} of {1}...", i + 1, u8Nodes.Count); + + int tempOffset = (int)u8Nodes[i].OffsetToData; + u8Nodes[i].OffsetToData = (uint)(u8Header.OffsetToData + tempOffset); + } + + fireDebug("Creating U8 File Finished..."); + } + + private string[] getDirContent(string dir, bool root) + { + string[] files = Directory.GetFiles(dir); + string[] dirs = Directory.GetDirectories(dir); + string all = ""; + + if (!root) + all += dir + "\n"; + + for (int i = 0; i < files.Length; i++) + all += files[i] + "\n"; + + foreach (string thisDir in dirs) + { + string[] temp = getDirContent(thisDir, false); + + foreach (string thisTemp in temp) + all += thisTemp + "\n"; + } + + return all.Split(new char[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + } + + private int getRealSize(byte[] data) + { + if (data[0] == 'I' && data[1] == 'M' && data[2] == 'D' && data[3] == '5') + if (data[0x20] == 'L' && data[0x21] == 'Z' && data[0x22] == '7' && data[0x23] == '7') + return (BitConverter.ToInt32(data, 0x24)) >> 8; + else + return data.Length - 32; + + return data.Length; + } + + private void addEntry(string nodePath, byte[] fileData) + { + //Parse path + if (nodePath.StartsWith("/")) nodePath = nodePath.Remove(0, 1); + string[] path = nodePath.Split('/'); //Last entry is the filename + + int nodeIndex = -1; + int maxIndex = (u8Nodes.Count > 0) ? u8Nodes.Count - 1 : 0; + int currentIndex = 0; + List pathIndices = new List(); + + for (int i = 0; i < path.Length - 1; i++) + { + for (int j = currentIndex; j <= maxIndex; j++) + { + if (stringTable[j].ToLower() == path[i].ToLower()) + { + if (i == path.Length - 2) nodeIndex = j; + + maxIndex = (int)u8Nodes[j].SizeOfData - 1; + currentIndex = j + 1; + pathIndices.Add(j); + + break; + } + + if (j == maxIndex - 1) throw new Exception("Path wasn't found!"); + } + } + + //Get last entry in current dir + int lastEntry; + + if (nodeIndex > -1) lastEntry = (int)u8Nodes[nodeIndex].SizeOfData - 2; + else lastEntry = (rootNode.SizeOfData > 1) ? (int)rootNode.SizeOfData - 2 : -1; + + //Create and insert node + data + U8_Node tempNode = new U8_Node(); + tempNode.Type = (fileData.Length == 0) ? U8_NodeType.Directory : U8_NodeType.File; + tempNode.SizeOfData = (uint)((fileData.Length == 0) ? lastEntry + 2 : fileData.Length); + tempNode.OffsetToData = (uint)((fileData.Length == 0) ? Shared.CountCharsInString(nodePath, '/') : 0); + + stringTable.Insert(lastEntry + 1, path[path.Length - 1]); + u8Nodes.Insert(lastEntry + 1, tempNode); + data.Insert(lastEntry + 1, fileData); + + //Update rootnode and path sizes (+1) + rootNode.SizeOfData += 1; + + foreach (int index in pathIndices) + if (u8Nodes[index].Type == U8_NodeType.Directory) + u8Nodes[index].SizeOfData += 1; + + for (int i = lastEntry + 1; i < u8Nodes.Count; i++) + if (u8Nodes[i].Type == U8_NodeType.Directory) + u8Nodes[i].SizeOfData += 1; + } + + private void removeEntry(string nodePath) + { + //Parse path + if (nodePath.StartsWith("/")) nodePath = nodePath.Remove(0, 1); + string[] path = nodePath.Split('/'); //Last entry is the filename + + int nodeIndex = -1; + int maxIndex = u8Nodes.Count - 1; + int currentIndex = 0; + List pathIndices = new List(); + + for (int i = 0; i < path.Length; i++) + { + for (int j = currentIndex; j < maxIndex; j++) + { + if (stringTable[j].ToLower() == path[i].ToLower()) + { + if (i == path.Length - 1) nodeIndex = j; + else pathIndices.Add(j); + + maxIndex = (int)u8Nodes[j].SizeOfData - 1; + currentIndex = j + 1; + + break; + } + + if (j == maxIndex - 1) throw new Exception("Path wasn't found!"); + } + } + + //Remove Node (and subnodes if node is dir) + int removed = 0; + + if (u8Nodes[nodeIndex].Type == U8_NodeType.Directory) + { + for (int i = (int)u8Nodes[nodeIndex].SizeOfData - 2; i >= nodeIndex; i--) + { + stringTable.RemoveAt(i); + u8Nodes.RemoveAt(i); + data.RemoveAt(i); + + removed++; + } + } + else + { + stringTable.RemoveAt(nodeIndex); + u8Nodes.RemoveAt(nodeIndex); + data.RemoveAt(nodeIndex); + + removed++; + } + + //Update rootnode and path sizes + rootNode.SizeOfData -= (uint)removed; + + foreach (int index in pathIndices) + if (u8Nodes[index].Type == U8_NodeType.Directory) + u8Nodes[index].SizeOfData -= (uint)removed; + + for (int i = nodeIndex + 1; i < u8Nodes.Count; i++) + if (u8Nodes[i].Type == U8_NodeType.Directory) + u8Nodes[i].SizeOfData -= (uint)removed; + } + #endregion + + #region Events + /// + /// Fires the Progress of various operations + /// + public event EventHandler Progress; + /// + /// Fires warnings (e.g. when hashes do not match) + /// + public event EventHandler Warning; + /// + /// Fires debugging messages. You may write them into a log file or log textbox. + /// + public event EventHandler Debug; + + + private void fireWarning(string warningMessage) + { + EventHandler warning = Warning; + if (warning != null) + warning(new object(), new MessageEventArgs(warningMessage)); + } + + private void fireDebug(string debugMessage, params object[] args) + { + EventHandler debug = Debug; + if (debug != null) + debug(new object(), new MessageEventArgs(string.Format(debugMessage, args))); + } + + private void fireProgress(int progressPercentage) + { + EventHandler progress = Progress; + if (progress != null) + progress(new object(), new ProgressChangedEventArgs(progressPercentage, string.Empty)); + } + #endregion + } + + public class U8_Header + { + private uint u8Magic = 0x55AA382D; + private uint offsetToRootNode = 0x20; + private uint headerSize; + private uint offsetToData; + private byte[] padding = new byte[16]; + + public uint U8Magic { get { return u8Magic; } } + public uint OffsetToRootNode { get { return offsetToRootNode; } } + public uint HeaderSize { get { return headerSize; } set { headerSize = value; } } + public uint OffsetToData { get { return offsetToData; } set { offsetToData = value; } } + public byte[] Padding { get { return padding; } } + + public void Write(Stream writeStream) + { + writeStream.Write(BitConverter.GetBytes(Shared.Swap(u8Magic)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(offsetToRootNode)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(headerSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(offsetToData)), 0, 4); + writeStream.Write(padding, 0, 16); + } + } + + public class U8_Node + { + private ushort type; + private ushort offsetToName; + private uint offsetToData; + private uint sizeOfData; + + public U8_NodeType Type { get { return (U8_NodeType)type; } set { type = (ushort)value; } } + public ushort OffsetToName { get { return offsetToName; } set { offsetToName = value; } } + public uint OffsetToData { get { return offsetToData; } set { offsetToData = value; } } + public uint SizeOfData { get { return sizeOfData; } set { sizeOfData = value; } } + + public void Write(Stream writeStream) + { + writeStream.Write(BitConverter.GetBytes(Shared.Swap(type)), 0, 2); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(offsetToName)), 0, 2); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(offsetToData)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(sizeOfData)), 0, 4); + } + } +} diff --git a/NUS Downloader/WAD.cs b/NUS Downloader/WAD.cs new file mode 100644 index 0000000..93e1e71 --- /dev/null +++ b/NUS Downloader/WAD.cs @@ -0,0 +1,1269 @@ +/* This file is part of libWiiSharp + * Copyright (C) 2009 Leathl + * + * libWiiSharp is free software: you can redistribute it and/or + * modify it under the terms of the GNU General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libWiiSharp is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Security.Cryptography; + +namespace libWiiSharp +{ + public enum LowerTitleID : uint + { + SystemTitles = 0x00000001, + SystemChannels = 0x00010002, + Channel = 0x00010001, + GameChannel = 0x00010004, + DLC = 0x00010005, + HiddenChannels = 0x00010008, + } + + public class WAD : IDisposable + { + private SHA1 sha = SHA1.Create(); + private DateTime creationTimeUTC = new DateTime(1970, 1, 1); + private bool hasBanner = false; + private bool lz77CompressBannerAndIcon = true; + private bool lz77DecompressBannerAndIcon = false; + private bool keepOriginalFooter = false; + + private WAD_Header wadHeader; + private CertificateChain cert = new CertificateChain(); + private Ticket tik = new Ticket(); + private TMD tmd = new TMD(); + private List contents; + private U8 bannerApp = new U8(); + private byte[] footer = new byte[0]; + + /// + /// The region of the title. + /// + public Region Region { get { return tmd.Region; } set { tmd.Region = value; } } + /// + /// The Number of contents. + /// + public int NumOfContents { get { return tmd.NumOfContents; } } + /// + /// The content of the WAD. + /// + public byte[][] Contents { get { return contents.ToArray(); } } + /// + /// If true, the Ticket and TMD will be fakesigned. + /// + public bool FakeSign { get { return (tik.FakeSign && tmd.FakeSign); } set { tik.FakeSign = value; tmd.FakeSign = value; } } + /// + /// The banner app file (aka 00000000.app). Will be empty if HasBanner is false. + /// + public U8 BannerApp { get { return bannerApp; } set { bannerApp = value; } } + /// + /// The IOS the Title is launched with. + /// + public ulong StartupIOS { get { return tmd.StartupIOS; } set { tmd.StartupIOS = value; } } + /// + /// The Title ID. + /// + public ulong TitleID { get { return tik.TitleID; } set { tik.TitleID = value; tmd.TitleID = value; } } + /// + /// The upper Title ID as string. + /// + public string UpperTitleID { get { return tik.GetUpperTitleID(); } } + /// + /// The Title Version. + /// + public ushort TitleVersion { get { return tmd.TitleVersion; } set { tmd.TitleVersion = value; } } + /// + /// The boot index. Represents the index of the nand loader. + /// + public ushort BootIndex { get { return tmd.BootIndex; } set { tmd.BootIndex = value; } } + /// + /// The creation time of the Title. Will be 1/1/1970 if no Timestamp footer was found. + /// + public DateTime CreationTimeUTC { get { return creationTimeUTC; } } + /// + /// True if the WAD has a banner. + /// + public bool HasBanner { get { return hasBanner; } } + /// + /// If true, the banner.bin and icon.bin files will be Lz77 compressed while saving the WAD. + /// + public bool Lz77CompressBannerAndIcon { get { return lz77CompressBannerAndIcon; } set { lz77CompressBannerAndIcon = value; if (value) lz77DecompressBannerAndIcon = false; } } + /// + /// If true, the banner.bin and icon.bin files will be Lz77 decompressed while saving the WAD. + /// + public bool Lz77DecompressBannerAndIcon { get { return lz77DecompressBannerAndIcon; } set { lz77DecompressBannerAndIcon = value; if (value) lz77CompressBannerAndIcon = false; } } + /// + /// The Number of memory blocks the content will take. + /// Might be inaccurate due to Lz77 (de)compression while saving. + /// + public string NandBlocks { get { return tmd.GetNandBlocks(); } } + /// + /// All Channel Titles as a string array. Will be empty if HasBanner is false. + /// + public string[] ChannelTitles { get { if (hasBanner) return ((Headers.IMET)bannerApp.Header).AllTitles; else return new string[0]; } set { ChangeChannelTitles(value); } } + /// + /// If false, a timestamp will be added as footer (64 bytes). + /// Else, the original footer will be kept or the one you provided. + /// + public bool KeepOriginalFooter { get { return keepOriginalFooter; } set { keepOriginalFooter = value; } } + /// + /// The TMDs content entries. + /// + public TMD_Content[] TmdContents { get { return tmd.Contents; } } + + public WAD() + { + cert.Debug += new EventHandler(cert_Debug); + tik.Debug += new EventHandler(tik_Debug); + tmd.Debug += new EventHandler(tmd_Debug); + bannerApp.Debug += new EventHandler(bannerApp_Debug); + bannerApp.Warning += new EventHandler(bannerApp_Warning); + } + + #region IDisposable Members + private bool isDisposed = false; + + ~WAD() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !isDisposed) + { + sha.Clear(); + sha = null; + + wadHeader = null; + + cert.Dispose(); + tik.Dispose(); + tmd.Dispose(); + + contents.Clear(); + contents = null; + + bannerApp.Dispose(); + + footer = null; + } + + isDisposed = true; + } + #endregion + + #region Public Functions + /// + /// Loads a WAD file. + /// + /// + /// + public static WAD Load(string pathToWad) + { + return Load(File.ReadAllBytes(pathToWad)); + } + + /// + /// Loads a WAD file. + /// + /// + /// + public static WAD Load(byte[] wadFile) + { + WAD w = new WAD(); + MemoryStream ms = new MemoryStream(wadFile); + + try { w.parseWad(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + return w; + } + + /// + /// Loads a WAD file. + /// + /// + /// + public static WAD Load(Stream wad) + { + WAD w = new WAD(); + w.parseWad(wad); + return w; + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + public static WAD Create(string contentDir) + { + string[] certPath = Directory.GetFiles(contentDir, "*cert*"); + string[] tikPath = Directory.GetFiles(contentDir, "*tik*"); + string[] tmdPath = Directory.GetFiles(contentDir, "*tmd*"); + + CertificateChain _cert = CertificateChain.Load(certPath[0]); + Ticket _tik = Ticket.Load(tikPath[0]); + TMD _tmd = TMD.Load(tmdPath[0]); + + bool namedContentId = true; + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].ContentID.ToString("x8") + ".app")) + { namedContentId = false; break; } + + if (!namedContentId) + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].Index.ToString("x8") + ".app")) + throw new Exception("Couldn't find all content files!"); + + byte[][] contents = new byte[_tmd.Contents.Length][]; + + for (int i = 0; i < _tmd.Contents.Length; i++) + { + string file = contentDir + Path.DirectorySeparatorChar + ((namedContentId) ? _tmd.Contents[i].ContentID.ToString("x8") : _tmd.Contents[i].Index.ToString("x8")) + ".app"; + contents[i] = File.ReadAllBytes(file); + } + + return Create(_cert, _tik, _tmd, contents); + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + /// + /// + /// + public static WAD Create(string pathToCert, string pathToTik, string pathToTmd, string contentDir) + { + CertificateChain _cert = CertificateChain.Load(pathToCert); + Ticket _tik = Ticket.Load(pathToTik); + TMD _tmd = TMD.Load(pathToTmd); + + bool namedContentId = true; + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].ContentID.ToString("x8") + ".app")) + { namedContentId = false; break; } + + if (!namedContentId) + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].Index.ToString("x8") + ".app")) + throw new Exception("Couldn't find all content files!"); + + byte[][] contents = new byte[_tmd.Contents.Length][]; + + for (int i = 0; i < _tmd.Contents.Length; i++) + { + string file = contentDir + Path.DirectorySeparatorChar + ((namedContentId) ? _tmd.Contents[i].ContentID.ToString("x8") : _tmd.Contents[i].Index.ToString("x8")) + ".app"; + contents[i] = File.ReadAllBytes(file); + } + + return Create(_cert, _tik, _tmd, contents); + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + /// + /// + /// + public static WAD Create(byte[] cert, byte[] tik, byte[] tmd, byte[][] contents) + { + CertificateChain _cert = CertificateChain.Load(cert); + Ticket _tik = Ticket.Load(tik); + TMD _tmd = TMD.Load(tmd); + + return Create(_cert, _tik, _tmd, contents); + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + /// + /// + /// + public static WAD Create(CertificateChain cert, Ticket tik, TMD tmd, byte[][] contents) + { + WAD w = new WAD(); + w.cert = cert; + w.tik = tik; + w.tmd = tmd; + w.contents = new List(contents); + + w.wadHeader = new WAD_Header(); + w.wadHeader.TmdSize = (uint)(484 + (tmd.Contents.Length * 36)); + + int contentSize = 0; + for (int i = 0; i < contents.Length - 1; i++) + contentSize += Shared.AddPadding(contents[i].Length); + + contentSize += contents[contents.Length - 1].Length; + + w.wadHeader.ContentSize = (uint)contentSize; + + for (int i = 0; i < w.tmd.Contents.Length; i++) + if (w.tmd.Contents[i].Index == 0x0000) + { + try { w.bannerApp.LoadFile(contents[i]); w.hasBanner = true; } + catch { w.hasBanner = false; } //Probably System Wad => No Banner App... + break; + } + + return w; + } + + + + /// + /// Loads a WAD file. + /// + /// + public void LoadFile(string pathToWad) + { + LoadFile(File.ReadAllBytes(pathToWad)); + } + + /// + /// Loads a WAD file. + /// + /// + public void LoadFile(byte[] wadFile) + { + MemoryStream ms = new MemoryStream(wadFile); + + try { parseWad(ms); } + catch { ms.Dispose(); throw; } + + ms.Dispose(); + } + + /// + /// Loads a WAD file. + /// + /// + public void LoadFile(Stream wad) + { + parseWad(wad); + } + + /// + /// Creates a WAD file from contents. + /// + /// + public void CreateNew(string contentDir) + { + string[] certPath = Directory.GetFiles(contentDir, "*cert*"); + string[] tikPath = Directory.GetFiles(contentDir, "*tik*"); + string[] tmdPath = Directory.GetFiles(contentDir, "*tmd*"); + + CertificateChain _cert = CertificateChain.Load(certPath[0]); + Ticket _tik = Ticket.Load(tikPath[0]); + TMD _tmd = TMD.Load(tmdPath[0]); + + bool namedContentId = true; + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].ContentID.ToString("x8") + ".app")) + { namedContentId = false; break; } + + if (!namedContentId) + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].Index.ToString("x8") + ".app")) + throw new Exception("Couldn't find all content files!"); + + byte[][] contents = new byte[_tmd.Contents.Length][]; + + for (int i = 0; i < _tmd.Contents.Length; i++) + { + string file = contentDir + Path.DirectorySeparatorChar + ((namedContentId) ? _tmd.Contents[i].ContentID.ToString("x8") : _tmd.Contents[i].Index.ToString("x8")) + ".app"; + contents[i] = File.ReadAllBytes(file); + } + + CreateNew(_cert, _tik, _tmd, contents); + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + /// + /// + public void CreateNew(string pathToCert, string pathToTik, string pathToTmd, string contentDir) + { + CertificateChain _cert = CertificateChain.Load(pathToCert); + Ticket _tik = Ticket.Load(pathToTik); + TMD _tmd = TMD.Load(pathToTmd); + + bool namedContentId = true; + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].ContentID.ToString("x8") + ".app")) + { namedContentId = false; break; } + + if (!namedContentId) + for (int i = 0; i < _tmd.Contents.Length; i++) + if (!File.Exists(contentDir + Path.DirectorySeparatorChar + _tmd.Contents[i].Index.ToString("x8") + ".app")) + throw new Exception("Couldn't find all content files!"); + + byte[][] contents = new byte[_tmd.Contents.Length][]; + + for (int i = 0; i < _tmd.Contents.Length; i++) + { + string file = contentDir + Path.DirectorySeparatorChar + ((namedContentId) ? _tmd.Contents[i].ContentID.ToString("x8") : _tmd.Contents[i].Index.ToString("x8")) + ".app"; + contents[i] = File.ReadAllBytes(file); + } + + CreateNew(_cert, _tik, _tmd, contents); + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + /// + /// + public void CreateNew(byte[] cert, byte[] tik, byte[] tmd, byte[][] contents) + { + CertificateChain _cert = CertificateChain.Load(cert); + Ticket _tik = Ticket.Load(tik); + TMD _tmd = TMD.Load(tmd); + + CreateNew(_cert, _tik, _tmd, contents); + } + + /// + /// Creates a WAD file from contents. + /// + /// + /// + /// + /// + public void CreateNew(CertificateChain cert, Ticket tik, TMD tmd, byte[][] contents) + { + this.cert = cert; + this.tik = tik; + this.tmd = tmd; + this.contents = new List(contents); + + this.wadHeader = new WAD_Header(); + this.wadHeader.TmdSize = (uint)(484 + (tmd.Contents.Length * 36)); + + int contentSize = 0; + for (int i = 0; i < contents.Length - 1; i++) + contentSize += Shared.AddPadding(contents[i].Length); + + contentSize += contents[contents.Length - 1].Length; + + this.wadHeader.ContentSize = (uint)contentSize; + + for (int i = 0; i < this.tmd.Contents.Length; i++) + if (this.tmd.Contents[i].Index == 0x0000) + { + try { this.bannerApp.LoadFile(contents[i]); hasBanner = true; } + catch { hasBanner = false; } //Probably System Wad => No Banner App... + break; + } + } + + + /// + /// Saves the WAD file to the given location. + /// + /// + public void Save(string savePath) + { + if (File.Exists(savePath)) File.Delete(savePath); + + using (FileStream fs = new FileStream(savePath, FileMode.Create)) + writeToStream(fs); + } + + /// + /// Returns the WAD file as a memory stream. + /// + /// + public MemoryStream ToMemoryStream() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + return ms; + } + + /// + /// Returns the WAD file as a byte array. + /// + /// + public byte[] ToByteArray() + { + MemoryStream ms = new MemoryStream(); + + try { writeToStream(ms); } + catch { ms.Dispose(); throw; } + + byte[] res = ms.ToArray(); + ms.Dispose(); + return res; + } + + /// + /// Changes the Title ID of the WAD file. + /// + /// + /// + public void ChangeTitleID(LowerTitleID lowerID, string upperID) + { + if (upperID.Length != 4) throw new Exception("Upper Title ID must be 4 characters long!"); + + byte[] temp = new byte[4]; + temp[0] = (byte)upperID[3]; + temp[1] = (byte)upperID[2]; + temp[2] = (byte)upperID[1]; + temp[3] = (byte)upperID[0]; + uint upper = BitConverter.ToUInt32(temp, 0); + + ulong newId = ((ulong)lowerID << 32) | upper; + + tik.TitleID = newId; + tmd.TitleID = newId; + } + + /// + /// Changes the IOS the Title is launched with. + /// + /// + public void ChangeStartupIOS(int newIos) + { + StartupIOS = ((ulong)0x00000001 << 32) | (uint)newIos; + } + + /// + /// Changes the Title Key in the Ticket. + /// The given value will be the encrypted Key (i.e. what you can "read" in the Ticket). + /// + /// + public void ChangeTitleKey(string newTitleKey) + { + tik.SetTitleKey(newTitleKey); + } + + /// + /// Changes the Title Key in the Ticket. + /// The given value will be the encrypted Key (i.e. what you can "read" in the Ticket). + /// + /// + public void ChangeTitleKey(char[] newTitleKey) + { + tik.SetTitleKey(newTitleKey); + } + + /// + /// Changes the Title Key in the Ticket. + /// The given value will be the encrypted Key (i.e. what you can "read" in the Ticket). + /// + /// + public void ChangeTitleKey(byte[] newTitleKey) + { + tik.SetTitleKey(newTitleKey); + } + + /// + /// Returns a content by it's TMD index. + /// + /// + /// + public byte[] GetContentByIndex(int index) + { + for (int i = 0; i < tmd.NumOfContents; i++) + if (tmd.Contents[i].Index == index) + return contents[i]; + + throw new Exception(string.Format("Content with index {0} not found!", index)); + } + + /// + /// Returns a content by it's content ID. + /// + /// + /// + public byte[] GetContentByID(int contentID) + { + for (int i = 0; i < tmd.NumOfContents; i++) + if (tmd.Contents[i].Index == contentID) + return contents[i]; + + throw new Exception(string.Format("Content with content ID {0} not found!", contentID)); + } + + /// + /// Changes the Channel Titles (Only if HasBanner is true). + /// 0: Japanese, + /// 1: English, + /// 2: German, + /// 3: French, + /// 4: Spanish, + /// 5: Italian, + /// 6: Dutch, + /// 7: Korean + /// + /// + public void ChangeChannelTitles(params string[] newTitles) + { + if (hasBanner) + ((Headers.IMET)bannerApp.Header).ChangeTitles(newTitles); + } + + /// + /// Adds a content to the WAD. + /// + /// + /// + /// + /// + public void AddContent(byte[] newContent, int contentID, int index, ContentType type = ContentType.Normal) + { + TMD_Content temp = new TMD_Content(); + temp.ContentID = (uint)contentID; + temp.Index = (ushort)index; + temp.Type = type; + temp.Size = (ulong)newContent.Length; + temp.Hash = sha.ComputeHash(newContent); + + tmd.AddContent(temp); + contents.Add(newContent); + + wadHeader.TmdSize = (uint)(484 + tmd.NumOfContents * 36); + } + + /// + /// Removes a content from the WAD. + /// + /// + public void RemoveContent(int index) + { + for (int i = 0; i < tmd.Contents.Length; i++) + if (tmd.Contents[i].Index == index) + { tmd.RemoveContent(index); contents.RemoveAt(i); wadHeader.TmdSize = (uint)(484 + tmd.NumOfContents * 36); return; } + + throw new Exception(string.Format("Content with index {0} not found!", index)); + } + + /// + /// Removes a content by it's content ID. + /// + /// + public void RemoveContentByID(int contentID) + { + for (int i = 0; i < tmd.Contents.Length; i++) + if (tmd.Contents[i].Index == contentID) + { tmd.RemoveContentByID(contentID); contents.RemoveAt(i); wadHeader.TmdSize = (uint)(484 + tmd.NumOfContents * 36); return; } + + throw new Exception(string.Format("Content with content ID {0} not found!", contentID)); + } + + /// + /// Removes all contents from the WAD. If HasBanner is true, the banner content (Index 0) won't be removed! + /// + public void RemoveAllContents() + { + if (!hasBanner) + { + tmd.Contents = new TMD_Content[0]; + contents = new List(); + + wadHeader.TmdSize = (uint)(484 + tmd.NumOfContents * 36); + } + else + { + for (int i=0;i(); + + tmd.AddContent(tmpTmdCont); + contents.Add(tmpCont); + + wadHeader.TmdSize = (uint)(484 + tmd.NumOfContents * 36); + break; + } + } + } + + /// + /// Unpacks the WAD to the given directory. + /// If nameContentID is true, contents are named after their content ID, else after their index. + /// + /// + /// + public void Unpack(string unpackDir, bool nameContentID = false) + { + unpackAll(unpackDir, nameContentID); + } + + /// + /// Removes the footer. + /// + public void RemoveFooter() + { + this.footer = new byte[0]; + wadHeader.FooterSize = 0; + + this.keepOriginalFooter = true; + } + + /// + /// Adds a footer. + /// + /// + public void AddFooter(byte[] footer) + { + ChangeFooter(footer); + } + + /// + /// Changes the footer. + /// + /// + public void ChangeFooter(byte[] newFooter) + { + if (newFooter.Length % 64 != 0) + Array.Resize(ref newFooter, Shared.AddPadding(newFooter.Length)); + + this.footer = newFooter; + wadHeader.FooterSize = (uint)newFooter.Length; + + this.keepOriginalFooter = true; + } + #endregion + + #region Private Functions + private void writeToStream(Stream writeStream) + { + fireDebug("Writing Wad..."); + + //Create Footer Timestamp + if (!keepOriginalFooter) + { + fireDebug(" Building Footer Timestamp..."); + createFooterTimestamp(); + } + + //Save Banner App + if (hasBanner) + { + //Compress icon.bin and banner.bin + if (lz77CompressBannerAndIcon || lz77DecompressBannerAndIcon) + { + for (int i = 0; i < bannerApp.Nodes.Count; i++) + { + if (bannerApp.StringTable[i].ToLower() == "icon.bin" || + bannerApp.StringTable[i].ToLower() == "banner.bin") + { + if (!Lz77.IsLz77Compressed(bannerApp.Data[i]) && lz77CompressBannerAndIcon) + { + fireDebug(" Compressing {0}...", bannerApp.StringTable[i]); + + //Get the data without the IMD5 Header + byte[] fileData = new byte[bannerApp.Data[i].Length - 32]; + Array.Copy(bannerApp.Data[i], 32, fileData, 0, fileData.Length); + + //Compress the data + Lz77 l = new Lz77(); + fileData = l.Compress(fileData); + + //Add a new IMD5 Header + fileData = Headers.IMD5.AddHeader(fileData); + bannerApp.Data[i] = fileData; + + //Update the node + bannerApp.Nodes[i].SizeOfData = (uint)fileData.Length; + } + else if (Lz77.IsLz77Compressed(bannerApp.Data[i]) && lz77DecompressBannerAndIcon) + { + fireDebug(" Decompressing {0}...", bannerApp.StringTable[i]); + + //Get the data without the IMD5 Header + byte[] fileData = new byte[bannerApp.Data[i].Length - 32]; + Array.Copy(bannerApp.Data[i], 32, fileData, 0, fileData.Length); + + //Decompress the data + Lz77 l = new Lz77(); + fileData = l.Decompress(fileData); + + //Add a new IMD5 Header + fileData = Headers.IMD5.AddHeader(fileData); + bannerApp.Data[i] = fileData; + + //Update the node + bannerApp.Nodes[i].SizeOfData = (uint)fileData.Length; + } + } + } + } + + for (int i = 0; i < contents.Count; i++) + if (tmd.Contents[i].Index == 0x0000) + { fireDebug(" Saving Banner App..."); contents[i] = bannerApp.ToByteArray(); break; } + } + + //Update Header (Content Size) + fireDebug(" Updating Header..."); + int contentSize = 0; + for (int i = 0; i < contents.Count - 1; i++) + contentSize += Shared.AddPadding(contents[i].Length); + + contentSize += contents[contents.Count - 1].Length; + + wadHeader.ContentSize = (uint)contentSize; + wadHeader.TmdSize = (uint)(484 + tmd.NumOfContents * 36); + + //Update Contents + fireDebug(" Updating TMD Contents..."); + tmd.UpdateContents(contents.ToArray()); + + //Write Header + fireDebug(" Writing Wad Header... (Offset: 0x{0})", writeStream.Position.ToString("x8").ToUpper()); + writeStream.Seek(0, SeekOrigin.Begin); + wadHeader.Write(writeStream); + + //Write Cert + fireDebug(" Writing Certificate Chain... (Offset: 0x{0})", writeStream.Position.ToString("x8").ToUpper()); + writeStream.Seek(Shared.AddPadding((int)writeStream.Position), SeekOrigin.Begin); + byte[] temp = cert.ToByteArray(); + writeStream.Write(temp, 0, temp.Length); + + //Write Tik + fireDebug(" Writing Ticket... (Offset: 0x{0})", writeStream.Position.ToString("x8").ToUpper()); + writeStream.Seek(Shared.AddPadding((int)writeStream.Position), SeekOrigin.Begin); + temp = tik.ToByteArray(); + writeStream.Write(temp, 0, temp.Length); + + //Write TMD + fireDebug(" Writing TMD... (Offset: 0x{0})", writeStream.Position.ToString("x8").ToUpper()); + writeStream.Seek(Shared.AddPadding((int)writeStream.Position), SeekOrigin.Begin); + temp = tmd.ToByteArray(); + writeStream.Write(temp, 0, temp.Length); + + //Write Contents + List contentList = new List(); + for (int i = 0; i < tmd.Contents.Length; i++) + contentList.Add(new ContentIndices(i, tmd.Contents[i].Index)); + + contentList.Sort(); + + for (int i = 0; i < contentList.Count; i++) + { + writeStream.Seek(Shared.AddPadding((int)writeStream.Position), SeekOrigin.Begin); + + fireProgress((i + 1) * 100 / contents.Count); + + fireDebug(" Writing Content #{1} of {2}... (Offset: 0x{0})", writeStream.Position.ToString("x8").ToUpper(), i + 1, contents.Count); + fireDebug(" -> Content ID: 0x{0}", tmd.Contents[contentList[i].Index].ContentID.ToString("x8")); + fireDebug(" -> Index: 0x{0}", tmd.Contents[contentList[i].Index].Index.ToString("x4")); + fireDebug(" -> Type: 0x{0} ({1})", ((ushort)tmd.Contents[contentList[i].Index].Type).ToString("x4"), tmd.Contents[contentList[i].Index].Type.ToString()); + fireDebug(" -> Size: {0} bytes", tmd.Contents[contentList[i].Index].Size); + fireDebug(" -> Hash: {0}", Shared.ByteArrayToString(tmd.Contents[contentList[i].Index].Hash)); + + temp = encryptContent(contents[contentList[i].Index], contentList[i].Index); + writeStream.Write(temp, 0, temp.Length); + } + + //Write Footer + if (wadHeader.FooterSize > 0) + { + fireDebug(" Writing Footer... (Offset: 0x{0})", writeStream.Position.ToString("x8").ToUpper()); + writeStream.Seek(Shared.AddPadding((int)writeStream.Position), SeekOrigin.Begin); + writeStream.Write(footer, 0, footer.Length); + } + + //Padding + while (writeStream.Position % 64 != 0) + writeStream.WriteByte(0x00); + + fireDebug("Writing Wad Finished... (Written Bytes: {0})", writeStream.Position); + } + + private void unpackAll(string unpackDir, bool nameContentId) + { + fireDebug("Unpacking Wad to: {0}", unpackDir); + + if (!Directory.Exists(unpackDir)) Directory.CreateDirectory(unpackDir); + string titleID = tik.TitleID.ToString("x16"); + + //Save Cert + fireDebug(" Saving Certificate Chain: {0}.cert", titleID); + cert.Save(unpackDir + Path.DirectorySeparatorChar + titleID + ".cert"); + + //Save Tik + fireDebug(" Saving Ticket: {0}.tik", titleID); + tik.Save(unpackDir + Path.DirectorySeparatorChar + titleID + ".tik"); + + //Save TMD + fireDebug(" Saving TMD: {0}.tmd", titleID); + tmd.Save(unpackDir + Path.DirectorySeparatorChar + titleID + ".tmd"); + + //Save Contents + for (int i = 0; i < tmd.NumOfContents; i++) + { + fireProgress((i + 1) * 100 / tmd.NumOfContents); + + fireDebug(" Saving Content #{0} of {1}: {2}.app", i + 1, tmd.NumOfContents, (nameContentId ? tmd.Contents[i].ContentID.ToString("x8") : tmd.Contents[i].Index.ToString("x8"))); + fireDebug(" -> Content ID: 0x{0}", tmd.Contents[i].ContentID.ToString("x8")); + fireDebug(" -> Index: 0x{0}", tmd.Contents[i].Index.ToString("x4")); + fireDebug(" -> Type: 0x{0} ({1})", ((ushort)tmd.Contents[i].Type).ToString("x4"), tmd.Contents[i].Type.ToString()); + fireDebug(" -> Size: {0} bytes", tmd.Contents[i].Size); + fireDebug(" -> Hash: {0}", Shared.ByteArrayToString(tmd.Contents[i].Hash)); + + using (FileStream fs = new FileStream(unpackDir + Path.DirectorySeparatorChar + + (nameContentId ? tmd.Contents[i].ContentID.ToString("x8") : tmd.Contents[i].Index.ToString("x8")) + ".app", + FileMode.Create)) + fs.Write(contents[i], 0, contents[i].Length); + } + + //Save Footer + fireDebug(" Saving Footer: {0}.footer", titleID); + using (FileStream fs = new FileStream(unpackDir + Path.DirectorySeparatorChar + titleID + ".footer", FileMode.Create)) + fs.Write(footer, 0, footer.Length); + + fireDebug("Unpacking Wad Finished..."); + } + + private void parseWad(Stream wadFile) + { + fireDebug("Parsing Wad..."); + + wadFile.Seek(0, SeekOrigin.Begin); + byte[] temp = new byte[4]; + + wadHeader = new WAD_Header(); + contents = new List(); + + //Read Header + fireDebug(" Parsing Header... (Offset: 0x{0})", wadFile.Position.ToString("x8").ToUpper()); + wadFile.Read(temp, 0, 4); + if (Shared.Swap(BitConverter.ToUInt32(temp, 0)) != wadHeader.HeaderSize) + throw new Exception("Invalid Headersize!"); + + wadFile.Read(temp, 0, 4); + wadHeader.WadType = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + wadFile.Seek(12, SeekOrigin.Current); + + wadFile.Read(temp, 0, 4); + wadHeader.TmdSize = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + wadFile.Read(temp, 0, 4); + wadHeader.ContentSize = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + wadFile.Read(temp, 0, 4); + wadHeader.FooterSize = Shared.Swap(BitConverter.ToUInt32(temp, 0)); + + //Read Cert + fireDebug(" Parsing Certificate Chain... (Offset: 0x{0})", wadFile.Position.ToString("x8").ToUpper()); + wadFile.Seek(Shared.AddPadding((int)wadFile.Position), SeekOrigin.Begin); + + temp = new byte[wadHeader.CertSize]; + wadFile.Read(temp, 0, temp.Length); + cert.LoadFile(temp); + + //Read Tik + fireDebug(" Parsing Ticket... (Offset: 0x{0})", wadFile.Position.ToString("x8").ToUpper()); + wadFile.Seek(Shared.AddPadding((int)wadFile.Position), SeekOrigin.Begin); + + temp = new byte[wadHeader.TicketSize]; + wadFile.Read(temp, 0, temp.Length); + tik.LoadFile(temp); + + //Read Tmd + fireDebug(" Parsing TMD... (Offset: 0x{0})", wadFile.Position.ToString("x8").ToUpper()); + wadFile.Seek(Shared.AddPadding((int)wadFile.Position), SeekOrigin.Begin); + + temp = new byte[wadHeader.TmdSize]; + wadFile.Read(temp, 0, temp.Length); + tmd.LoadFile(temp); + + if (tmd.TitleID != tik.TitleID) + fireWarning("The Title ID in the Ticket doesn't match the one in the TMD!"); + + //Read Content + for (int i = 0; i < tmd.NumOfContents; i++) + { + fireProgress((i + 1) * 100 / tmd.NumOfContents); + + fireDebug(" Reading Content #{0} of {1}... (Offset: 0x{2})", i + 1, tmd.NumOfContents, wadFile.Position.ToString("x8").ToUpper()); + fireDebug(" -> Content ID: 0x{0}", tmd.Contents[i].ContentID.ToString("x8")); + fireDebug(" -> Index: 0x{0}", tmd.Contents[i].Index.ToString("x4")); + fireDebug(" -> Type: 0x{0} ({1})", ((ushort)tmd.Contents[i].Type).ToString("x4"), tmd.Contents[i].Type.ToString()); + fireDebug(" -> Size: {0} bytes", tmd.Contents[i].Size); + fireDebug(" -> Hash: {0}", Shared.ByteArrayToString(tmd.Contents[i].Hash)); + + wadFile.Seek(Shared.AddPadding((int)wadFile.Position), SeekOrigin.Begin); + + temp = new byte[Shared.AddPadding((int)tmd.Contents[i].Size, 16)]; + wadFile.Read(temp, 0, temp.Length); + + //Decrypt Content + temp = decryptContent(temp, i); + Array.Resize(ref temp, (int)tmd.Contents[i].Size); + + byte[] tmdHash = tmd.Contents[i].Hash; + byte[] newHash = sha.ComputeHash(temp, 0, (int)tmd.Contents[i].Size); + + if (!Shared.CompareByteArrays(tmdHash, newHash)) + { + fireDebug(@"/!\ /!\ /!\ Hashes do not match /!\ /!\ /!\"); + fireWarning(string.Format("Content #{0} (Content ID: 0x{1}; Index: 0x{2}): Hashes do not match! The content might be corrupted!", i + 1, tmd.Contents[i].ContentID.ToString("x8"), tmd.Contents[i].Index.ToString("x4"))); + } + + contents.Add(temp); + + if (tmd.Contents[i].Index == 0x0000) + { + try { bannerApp.LoadFile(temp); hasBanner = true; } + catch { hasBanner = false; } //Probably System Wad => No Banner App... + } + } + + //Read Footer + if (wadHeader.FooterSize > 0) + { + fireDebug(" Reading Footer... (Offset: 0x{0})", wadFile.Position.ToString("x8").ToUpper()); + footer = new byte[wadHeader.FooterSize]; + + wadFile.Seek(Shared.AddPadding((int)wadFile.Position), SeekOrigin.Begin); + wadFile.Read(footer, 0, footer.Length); + + parseFooterTimestamp(); + } + + fireDebug("Parsing Wad Finished..."); + } + + private byte[] decryptContent(byte[] content, int contentIndex) + { + int originalLength = content.Length; + Array.Resize(ref content, Shared.AddPadding(content.Length, 16)); + byte[] titleKey = tik.TitleKey; + byte[] iv = new byte[16]; + + byte[] tmp = BitConverter.GetBytes(tmd.Contents[contentIndex].Index); + iv[0] = tmp[1]; + iv[1] = tmp[0]; + + RijndaelManaged rm = new RijndaelManaged(); + rm.Mode = CipherMode.CBC; + rm.Padding = PaddingMode.None; + rm.KeySize = 128; + rm.BlockSize = 128; + rm.Key = titleKey; + rm.IV = iv; + + ICryptoTransform decryptor = rm.CreateDecryptor(); + + MemoryStream ms = new MemoryStream(content); + CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read); + + byte[] decCont = new byte[originalLength]; + cs.Read(decCont, 0, decCont.Length); + + cs.Dispose(); + ms.Dispose(); + + return decCont; + } + + private byte[] encryptContent(byte[] content, int contentIndex) + { + Array.Resize(ref content, Shared.AddPadding(content.Length, 16)); + byte[] titleKey = tik.TitleKey; + byte[] iv = new byte[16]; + + byte[] tmp = BitConverter.GetBytes(tmd.Contents[contentIndex].Index); + iv[0] = tmp[1]; + iv[1] = tmp[0]; + + RijndaelManaged encrypt = new RijndaelManaged(); + encrypt.Mode = CipherMode.CBC; + encrypt.Padding = PaddingMode.None; + encrypt.KeySize = 128; + encrypt.BlockSize = 128; + encrypt.Key = titleKey; + encrypt.IV = iv; + + ICryptoTransform encryptor = encrypt.CreateEncryptor(); + + MemoryStream ms = new MemoryStream(content); + CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Read); + + byte[] encCont = new byte[content.Length]; + cs.Read(encCont, 0, encCont.Length); + + cs.Dispose(); + ms.Dispose(); + + return encCont; + } + + private void createFooterTimestamp() + { + DateTime dtNow = DateTime.UtcNow; + TimeSpan tsTimestamp = (dtNow - new DateTime(1970, 1, 1, 0, 0, 0)); + + int timeStamp = (int)tsTimestamp.TotalSeconds; + System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding(); + + byte[] timeStampBytes = enc.GetBytes("TmStmp" + timeStamp.ToString()); + Array.Resize(ref timeStampBytes, 64); + + wadHeader.FooterSize = (uint)timeStampBytes.Length; + footer = timeStampBytes; + } + + private void parseFooterTimestamp() + { + creationTimeUTC = new DateTime(1970, 1, 1); + + if ((footer[0] == 'C' && footer[1] == 'M' && footer[2] == 'i' && + footer[3] == 'i' && footer[4] == 'U' && footer[5] == 'T') || + (footer[0] == 'T' && footer[1] == 'm' && footer[2] == 'S' && + footer[3] == 't' && footer[4] == 'm' && footer[5] == 'p')) + { + System.Text.ASCIIEncoding enc = new System.Text.ASCIIEncoding(); + string stringSeconds = enc.GetString(footer, 6, 10); + int seconds = 0; + + if (int.TryParse(stringSeconds, out seconds)) + creationTimeUTC = creationTimeUTC.AddSeconds((double)seconds); + } + } + #endregion + + #region Events + /// + /// Fires the Progress of various operations + /// + public event EventHandler Progress; + /// + /// Fires warnings (e.g. when hashes don't match) + /// + public event EventHandler Warning; + /// + /// Fires debugging messages. You may write them into a log file or log textbox. + /// + public event EventHandler Debug; + + private void fireDebug(string debugMessage, params object[] args) + { + EventHandler debug = Debug; + if (debug != null) + debug(new object(), new MessageEventArgs(string.Format(debugMessage, args))); + } + + private void fireWarning(string warningMessage) + { + EventHandler warning = Warning; + if (warning != null) + warning(new object(), new MessageEventArgs(warningMessage)); + } + + private void fireProgress(int progressPercentage) + { + EventHandler progress = Progress; + if (progress != null) + progress(new object(), new ProgressChangedEventArgs(progressPercentage, string.Empty)); + } + + private void cert_Debug(object sender, MessageEventArgs e) + { + fireDebug(" Certificate Chain: {0}", e.Message); + } + + private void tik_Debug(object sender, MessageEventArgs e) + { + fireDebug(" Ticket: {0}", e.Message); + } + + private void tmd_Debug(object sender, MessageEventArgs e) + { + fireDebug(" TMD: {0}", e.Message); + } + + void bannerApp_Debug(object sender, MessageEventArgs e) + { + fireDebug(" BannerApp: {0}", e.Message); + } + + void bannerApp_Warning(object sender, MessageEventArgs e) + { + fireWarning(e.Message); + } + #endregion + } + + public class WAD_Header + { + private uint headerSize = 0x20; + private uint wadType = 0x49730000; + private uint certSize = 0xA00; + private uint reserved = 0x00; + private uint tikSize = 0x2A4; + private uint tmdSize; + private uint contentSize; + private uint footerSize = 0x00; + + public uint HeaderSize { get { return headerSize; } } + public uint WadType { get { return wadType; } set { wadType = value; } } + public uint CertSize { get { return certSize; } } + public uint Reserved { get { return reserved; } } + public uint TicketSize { get { return tikSize; } } + public uint TmdSize { get { return tmdSize; } set { tmdSize = value; } } + public uint ContentSize { get { return contentSize; } set { contentSize = value; } } + public uint FooterSize { get { return footerSize; } set { footerSize = value; } } + + public void Write(Stream writeStream) + { + writeStream.Seek(0, SeekOrigin.Begin); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(headerSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(wadType)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(certSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(reserved)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(tikSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(tmdSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(contentSize)), 0, 4); + writeStream.Write(BitConverter.GetBytes(Shared.Swap(footerSize)), 0, 4); + } + } +}