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