/* 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); } } }