mirror of
https://github.com/WiiDatabase/nusdownloader.git
synced 2024-11-27 12:44:13 +01:00
445 lines
18 KiB
C#
445 lines
18 KiB
C#
// Decompiled with JetBrains decompiler
|
|
// Type: libWiiSharp.NusClient
|
|
// Assembly: NUS Downloader, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
|
|
// MVID: DDAF9FEC-76DE-4BD8-8A6D-D7CAD5827AC6
|
|
// Assembly location: C:\dotpeek\NUS Downloader.exe
|
|
|
|
using System;
|
|
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Net;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace libWiiSharp
|
|
{
|
|
public class NusClient : IDisposable
|
|
{
|
|
private const string WII_NUS_URL = "http://nus.cdn.shop.wii.com/ccs/download/";
|
|
private const string DSI_NUS_URL = "http://nus.cdn.t.shop.nintendowifi.net/ccs/download/";
|
|
private const string WII_USER_AGENT = "wii libnup/1.0";
|
|
private const string DSI_USER_AGENT = "Opera/9.50 (Nintendo; Opera/154; U; Nintendo DS; en)";
|
|
private string nusUrl = "http://nus.cdn.shop.wii.com/ccs/download/";
|
|
private WebClient wcNus = new WebClient();
|
|
private bool useLocalFiles;
|
|
private bool continueWithoutTicket;
|
|
private int titleversion;
|
|
private bool isDisposed;
|
|
|
|
public int TitleVersion => this.titleversion;
|
|
|
|
public bool UseLocalFiles
|
|
{
|
|
get => this.useLocalFiles;
|
|
set => this.useLocalFiles = value;
|
|
}
|
|
|
|
public bool ContinueWithoutTicket
|
|
{
|
|
get => this.continueWithoutTicket;
|
|
set => this.continueWithoutTicket = value;
|
|
}
|
|
|
|
~NusClient() => this.Dispose(false);
|
|
|
|
public void Dispose()
|
|
{
|
|
this.Dispose(true);
|
|
GC.SuppressFinalize((object) this);
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing && !this.isDisposed)
|
|
this.wcNus.Dispose();
|
|
this.isDisposed = true;
|
|
}
|
|
|
|
public void ConfigureNusClient(WebClient wcReady) => this.wcNus = wcReady;
|
|
|
|
public void SetToWiiServer()
|
|
{
|
|
this.nusUrl = "http://nus.cdn.shop.wii.com/ccs/download/";
|
|
this.wcNus.Headers.Add("User-Agent", "wii libnup/1.0");
|
|
}
|
|
|
|
public void SetToDSiServer()
|
|
{
|
|
this.nusUrl = "http://nus.cdn.t.shop.nintendowifi.net/ccs/download/";
|
|
this.wcNus.Headers.Add("User-Agent", "Opera/9.50 (Nintendo; Opera/154; U; Nintendo DS; en)");
|
|
}
|
|
|
|
public void DownloadTitle(
|
|
string titleId,
|
|
string titleVersion,
|
|
string outputDir,
|
|
bool alternative_path,
|
|
string wadName,
|
|
params StoreType[] storeTypes)
|
|
{
|
|
if (titleId.Length != 16)
|
|
throw new Exception("Title ID must be 16 characters long!");
|
|
this.downloadTitle(titleId, titleVersion, outputDir, alternative_path, wadName, storeTypes);
|
|
}
|
|
|
|
public TMD DownloadTMD(string titleId, string titleVersion) => titleId.Length == 16 ? this.downloadTmd(titleId, titleVersion) : throw new Exception("Title ID must be 16 characters long!");
|
|
|
|
public Ticket DownloadTicket(string titleId) => titleId.Length == 16 ? this.downloadTicket(titleId) : throw new Exception("Title ID must be 16 characters long!");
|
|
|
|
public byte[] DownloadSingleContent(string titleId, string titleVersion, string contentId)
|
|
{
|
|
if (titleId.Length != 16)
|
|
throw new Exception("Title ID must be 16 characters long!");
|
|
return this.downloadSingleContent(titleId, titleVersion, contentId);
|
|
}
|
|
|
|
public void DownloadSingleContent(
|
|
string titleId,
|
|
string titleVersion,
|
|
string contentId,
|
|
string savePath)
|
|
{
|
|
if (titleId.Length != 16)
|
|
throw new Exception("Title ID must be 16 characters long!");
|
|
if (!Directory.Exists(Path.GetDirectoryName(savePath)))
|
|
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
|
|
if (System.IO.File.Exists(savePath))
|
|
System.IO.File.Delete(savePath);
|
|
byte[] bytes = this.downloadSingleContent(titleId, titleVersion, contentId);
|
|
System.IO.File.WriteAllBytes(savePath, bytes);
|
|
}
|
|
|
|
private byte[] downloadSingleContent(string titleId, string titleVersion, string contentId)
|
|
{
|
|
uint num = uint.Parse(contentId, NumberStyles.HexNumber);
|
|
contentId = num.ToString("x8");
|
|
this.fireDebug("Downloading Content (Content ID: {0}) of Title {1} v{2}...", (object) contentId, (object) titleId, string.IsNullOrEmpty(titleVersion) ? (object) "[Latest]" : (object) titleVersion);
|
|
this.fireDebug(" Checking for Internet connection...");
|
|
if (!this.CheckInet())
|
|
{
|
|
this.fireDebug(" Connection not found...");
|
|
throw new Exception("You're not connected to the internet!");
|
|
}
|
|
this.fireProgress(0);
|
|
string str1 = "tmd" + (string.IsNullOrEmpty(titleVersion) ? string.Empty : string.Format(".{0}", (object) titleVersion));
|
|
string str2 = string.Format("{0}{1}/", (object) this.nusUrl, (object) titleId);
|
|
string empty = string.Empty;
|
|
int contentIndex = 0;
|
|
this.fireDebug(" Downloading TMD...");
|
|
byte[] tmdFile = this.wcNus.DownloadData(str2 + str1);
|
|
this.fireDebug(" Parsing TMD...");
|
|
TMD tmd = TMD.Load(tmdFile);
|
|
this.fireProgress(20);
|
|
this.fireDebug(" Looking for Content ID {0} in TMD...", (object) contentId);
|
|
bool flag = false;
|
|
for (int index = 0; index < tmd.Contents.Length; ++index)
|
|
{
|
|
if ((int) tmd.Contents[index].ContentID == (int) num)
|
|
{
|
|
this.fireDebug(" Content ID {0} found in TMD...", (object) contentId);
|
|
flag = true;
|
|
empty = tmd.Contents[index].ContentID.ToString("x8");
|
|
contentIndex = index;
|
|
break;
|
|
}
|
|
}
|
|
if (!flag)
|
|
{
|
|
this.fireDebug(" Content ID {0} wasn't found in TMD...", (object) contentId);
|
|
throw new Exception("Content ID wasn't found in the TMD!");
|
|
}
|
|
this.fireDebug(" Downloading Ticket...");
|
|
byte[] ticket = this.wcNus.DownloadData(str2 + "cetk");
|
|
this.fireDebug(" Parsing Ticket...");
|
|
Ticket tik = Ticket.Load(ticket);
|
|
this.fireProgress(40);
|
|
this.fireDebug(" Downloading Content... ({0} bytes)", (object) tmd.Contents[contentIndex].Size);
|
|
byte[] content = this.wcNus.DownloadData(str2 + empty);
|
|
this.fireProgress(80);
|
|
this.fireDebug(" Decrypting Content...");
|
|
byte[] array = this.decryptContent(content, contentIndex, tik, tmd);
|
|
Array.Resize<byte>(ref array, (int) tmd.Contents[contentIndex].Size);
|
|
if (!Shared.CompareByteArrays(SHA1.Create().ComputeHash(array), tmd.Contents[contentIndex].Hash))
|
|
{
|
|
this.fireDebug("/!\\ /!\\ /!\\ Hashes do not match /!\\ /!\\ /!\\");
|
|
throw new Exception("Hashes do not match!");
|
|
}
|
|
this.fireProgress(100);
|
|
this.fireDebug("Downloading Content (Content ID: {0}) of Title {1} v{2} Finished...", (object) contentId, (object) titleId, string.IsNullOrEmpty(titleVersion) ? (object) "[Latest]" : (object) titleVersion);
|
|
return array;
|
|
}
|
|
|
|
private Ticket downloadTicket(string titleId)
|
|
{
|
|
if (!this.CheckInet())
|
|
throw new Exception("You're not connected to the internet!");
|
|
return Ticket.Load(this.wcNus.DownloadData(string.Format("{0}{1}/", (object) this.nusUrl, (object) titleId) + "cetk"));
|
|
}
|
|
|
|
private TMD downloadTmd(string titleId, string titleVersion)
|
|
{
|
|
if (!this.CheckInet())
|
|
throw new Exception("You're not connected to the internet!");
|
|
return TMD.Load(this.wcNus.DownloadData(string.Format("{0}{1}/", (object) this.nusUrl, (object) titleId) + ("tmd" + (string.IsNullOrEmpty(titleVersion) ? string.Empty : string.Format(".{0}", (object) titleVersion)))));
|
|
}
|
|
|
|
private void downloadTitle(
|
|
string titleId,
|
|
string titleVersion,
|
|
string outputDir,
|
|
bool alternative_path,
|
|
string wadName,
|
|
StoreType[] storeTypes)
|
|
{
|
|
this.fireDebug("Downloading Title {0} v{1}...", (object) titleId, string.IsNullOrEmpty(titleVersion) ? (object) "[Latest]" : (object) titleVersion);
|
|
if (storeTypes.Length < 1)
|
|
{
|
|
this.fireDebug(" No store types were defined...");
|
|
throw new Exception("You must at least define one store type!");
|
|
}
|
|
string path1 = string.Format("{0}{1}/", (object) this.nusUrl, (object) titleId);
|
|
bool flag1 = false;
|
|
bool flag2 = false;
|
|
bool flag3 = false;
|
|
this.fireProgress(0);
|
|
foreach (int storeType in storeTypes)
|
|
{
|
|
switch (storeType)
|
|
{
|
|
case 0:
|
|
this.fireDebug(" [=] Storing Encrypted Content...");
|
|
flag1 = true;
|
|
break;
|
|
case 1:
|
|
this.fireDebug(" [=] Storing Decrypted Content...");
|
|
flag2 = true;
|
|
break;
|
|
case 2:
|
|
this.fireDebug(" [=] Storing WAD...");
|
|
flag3 = true;
|
|
break;
|
|
case 3:
|
|
this.fireDebug(" [=] Storing Decrypted Content...");
|
|
this.fireDebug(" [=] Storing Encrypted Content...");
|
|
this.fireDebug(" [=] Storing WAD...");
|
|
flag2 = true;
|
|
flag1 = true;
|
|
flag3 = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!Directory.Exists(outputDir))
|
|
Directory.CreateDirectory(outputDir);
|
|
string path2 = "tmd" + (string.IsNullOrEmpty(titleVersion) ? string.Empty : string.Format(".{0}", (object) titleVersion));
|
|
this.fireDebug(" - Downloading TMD...");
|
|
byte[] tmdFile;
|
|
TMD tmd;
|
|
try
|
|
{
|
|
tmdFile = this.wcNus.DownloadData(path1 + path2);
|
|
tmd = TMD.Load(tmdFile);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.fireDebug(" + Downloading TMD Failed...");
|
|
throw new Exception("Downloading TMD Failed:\n" + ex.Message);
|
|
}
|
|
if (alternative_path)
|
|
{
|
|
if (!Directory.Exists(Path.Combine(outputDir, titleId + "v" + (object) tmd.TitleVersion)))
|
|
Directory.CreateDirectory(Path.Combine(outputDir, titleId + "v" + (object) tmd.TitleVersion));
|
|
outputDir = Path.Combine(outputDir, titleId + "v" + (object) tmd.TitleVersion);
|
|
}
|
|
else
|
|
{
|
|
if (!Directory.Exists(Path.Combine(outputDir, titleId)))
|
|
Directory.CreateDirectory(Path.Combine(outputDir, titleId));
|
|
outputDir = Path.Combine(outputDir, titleId);
|
|
}
|
|
this.fireDebug(" - Parsing TMD...");
|
|
if (string.IsNullOrEmpty(titleVersion))
|
|
this.fireDebug(" + Title Version: {0}", (object) tmd.TitleVersion);
|
|
this.fireDebug(" + {0} Contents", (object) tmd.NumOfContents);
|
|
if (!alternative_path)
|
|
{
|
|
if (!Directory.Exists(Path.Combine(outputDir, tmd.TitleVersion.ToString())))
|
|
Directory.CreateDirectory(Path.Combine(outputDir, tmd.TitleVersion.ToString()));
|
|
outputDir = Path.Combine(outputDir, tmd.TitleVersion.ToString());
|
|
}
|
|
this.titleversion = (int) tmd.TitleVersion;
|
|
System.IO.File.WriteAllBytes(Path.Combine(outputDir, path2), tmd.ToByteArray());
|
|
this.fireProgress(5);
|
|
this.fireDebug(" - Downloading Ticket...");
|
|
try
|
|
{
|
|
this.wcNus.DownloadFile(Path.Combine(path1, "cetk"), Path.Combine(outputDir, "cetk"));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (!this.continueWithoutTicket || !flag1)
|
|
{
|
|
this.fireDebug(" + Downloading Ticket Failed...");
|
|
throw new Exception("Downloading Ticket Failed:\n" + ex.Message);
|
|
}
|
|
if (!System.IO.File.Exists(Path.Combine(outputDir, "cetk")))
|
|
{
|
|
flag2 = false;
|
|
flag3 = false;
|
|
}
|
|
}
|
|
this.fireProgress(10);
|
|
Ticket tik = new Ticket();
|
|
if (System.IO.File.Exists(Path.Combine(outputDir, "cetk")))
|
|
{
|
|
this.fireDebug(" + Parsing Ticket...");
|
|
tik = Ticket.Load(Path.Combine(outputDir, "cetk"));
|
|
if (this.nusUrl == "http://nus.cdn.t.shop.nintendowifi.net/ccs/download/")
|
|
tik.DSiTicket = true;
|
|
}
|
|
else
|
|
this.fireDebug(" + Ticket Unavailable...");
|
|
string[] strArray = new string[(int) tmd.NumOfContents];
|
|
for (int index = 0; index < (int) tmd.NumOfContents; ++index)
|
|
{
|
|
this.fireDebug(" - Downloading Content #{0} of {1}... ({2} bytes)", (object) (index + 1), (object) tmd.NumOfContents, (object) tmd.Contents[index].Size);
|
|
this.fireProgress((index + 1) * 60 / (int) tmd.NumOfContents + 10);
|
|
if (this.useLocalFiles)
|
|
{
|
|
if (System.IO.File.Exists(Path.Combine(outputDir, tmd.Contents[index].ContentID.ToString("x8"))))
|
|
{
|
|
this.fireDebug(" + Using Local File, Skipping...");
|
|
continue;
|
|
}
|
|
}
|
|
try
|
|
{
|
|
this.wcNus.DownloadFile(path1 + tmd.Contents[index].ContentID.ToString("x8"), Path.Combine(outputDir, tmd.Contents[index].ContentID.ToString("x8")));
|
|
strArray[index] = tmd.Contents[index].ContentID.ToString("x8");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.fireDebug(" - Downloading Content #{0} of {1} failed...", (object) (index + 1), (object) tmd.NumOfContents);
|
|
throw new Exception("Downloading Content Failed:\n" + ex.Message);
|
|
}
|
|
}
|
|
if (flag2 || flag3)
|
|
{
|
|
SHA1 shA1 = SHA1.Create();
|
|
for (int contentIndex = 0; contentIndex < (int) tmd.NumOfContents; ++contentIndex)
|
|
{
|
|
this.fireDebug(" - Decrypting Content #{0} of {1}...", (object) (contentIndex + 1), (object) tmd.NumOfContents);
|
|
this.fireProgress((contentIndex + 1) * 20 / (int) tmd.NumOfContents + 75);
|
|
byte[] array = this.decryptContent(System.IO.File.ReadAllBytes(Path.Combine(outputDir, tmd.Contents[contentIndex].ContentID.ToString("x8"))), contentIndex, tik, tmd);
|
|
Array.Resize<byte>(ref array, (int) tmd.Contents[contentIndex].Size);
|
|
if (!Shared.CompareByteArrays(shA1.ComputeHash(array), tmd.Contents[contentIndex].Hash))
|
|
this.fireDebug(" + Hashes do not match! (Invalid Output)");
|
|
System.IO.File.WriteAllBytes(Path.Combine(outputDir, tmd.Contents[contentIndex].ContentID.ToString("x8") + ".app"), array);
|
|
}
|
|
shA1.Clear();
|
|
}
|
|
if (flag3)
|
|
{
|
|
this.fireDebug(" - Building Certificate Chain...");
|
|
CertificateChain cert = CertificateChain.FromTikTmd(Path.Combine(outputDir, "cetk"), tmdFile);
|
|
byte[][] contents = new byte[(int) tmd.NumOfContents][];
|
|
for (int index = 0; index < (int) tmd.NumOfContents; ++index)
|
|
contents[index] = System.IO.File.ReadAllBytes(Path.Combine(outputDir, tmd.Contents[index].ContentID.ToString("x8") + ".app"));
|
|
this.fireDebug(" - Creating WAD...");
|
|
WAD wad = WAD.Create(cert, tik, tmd, contents);
|
|
wad.RemoveFooter();
|
|
wadName = wadName.Replace("[v]", "v" + this.TitleVersion.ToString());
|
|
if (Path.DirectorySeparatorChar.ToString() != "/" && Path.AltDirectorySeparatorChar.ToString() != "/")
|
|
wadName = wadName.Replace("/", "");
|
|
if (wadName.Contains(Path.DirectorySeparatorChar.ToString()) || wadName.Contains(Path.AltDirectorySeparatorChar.ToString()))
|
|
wad.Save(wadName);
|
|
else
|
|
wad.Save(Path.Combine(outputDir, wadName));
|
|
}
|
|
if (!flag1)
|
|
{
|
|
this.fireDebug(" - Deleting Encrypted Contents...");
|
|
for (int index = 0; index < strArray.Length; ++index)
|
|
{
|
|
if (System.IO.File.Exists(Path.Combine(outputDir, strArray[index])))
|
|
System.IO.File.Delete(Path.Combine(outputDir, strArray[index]));
|
|
}
|
|
}
|
|
if (flag3 && !flag2)
|
|
{
|
|
this.fireDebug(" - Deleting Decrypted Contents...");
|
|
for (int index = 0; index < strArray.Length; ++index)
|
|
{
|
|
if (System.IO.File.Exists(Path.Combine(outputDir, strArray[index] + ".app")))
|
|
System.IO.File.Delete(Path.Combine(outputDir, strArray[index] + ".app"));
|
|
}
|
|
}
|
|
if (!flag2 && !flag1)
|
|
{
|
|
this.fireDebug(" - Deleting TMD and Ticket...");
|
|
System.IO.File.Delete(Path.Combine(outputDir, path2));
|
|
System.IO.File.Delete(Path.Combine(outputDir, "cetk"));
|
|
}
|
|
this.fireDebug("Downloading Title {0} v{1} Finished...", (object) titleId, (object) tmd.TitleVersion);
|
|
this.fireProgress(100);
|
|
}
|
|
|
|
private byte[] decryptContent(byte[] content, int contentIndex, Ticket tik, TMD tmd)
|
|
{
|
|
Array.Resize<byte>(ref content, Shared.AddPadding(content.Length, 16));
|
|
byte[] titleKey = tik.TitleKey;
|
|
byte[] numArray = new byte[16];
|
|
byte[] bytes = BitConverter.GetBytes(tmd.Contents[contentIndex].Index);
|
|
numArray[0] = bytes[1];
|
|
numArray[1] = bytes[0];
|
|
RijndaelManaged rijndaelManaged = new RijndaelManaged();
|
|
rijndaelManaged.Mode = CipherMode.CBC;
|
|
rijndaelManaged.Padding = PaddingMode.None;
|
|
rijndaelManaged.KeySize = 128;
|
|
rijndaelManaged.BlockSize = 128;
|
|
rijndaelManaged.Key = titleKey;
|
|
rijndaelManaged.IV = numArray;
|
|
ICryptoTransform decryptor = rijndaelManaged.CreateDecryptor();
|
|
MemoryStream memoryStream = new MemoryStream(content);
|
|
CryptoStream cryptoStream = new CryptoStream((Stream) memoryStream, decryptor, CryptoStreamMode.Read);
|
|
byte[] buffer = new byte[content.Length];
|
|
cryptoStream.Read(buffer, 0, buffer.Length);
|
|
cryptoStream.Dispose();
|
|
memoryStream.Dispose();
|
|
return buffer;
|
|
}
|
|
|
|
private bool CheckInet()
|
|
{
|
|
try
|
|
{
|
|
Dns.GetHostEntry("www.google.com");
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public event EventHandler<ProgressChangedEventArgs> Progress;
|
|
|
|
public event EventHandler<MessageEventArgs> Debug;
|
|
|
|
private void fireDebug(string debugMessage, params object[] args)
|
|
{
|
|
EventHandler<MessageEventArgs> debug = this.Debug;
|
|
if (debug == null)
|
|
return;
|
|
debug(new object(), new MessageEventArgs(string.Format(debugMessage, args)));
|
|
}
|
|
|
|
private void fireProgress(int progressPercentage)
|
|
{
|
|
EventHandler<ProgressChangedEventArgs> progress = this.Progress;
|
|
if (progress == null)
|
|
return;
|
|
progress(new object(), new ProgressChangedEventArgs(progressPercentage, (object) string.Empty));
|
|
}
|
|
}
|
|
}
|