public static async Task DownloadTitle(HttpClient client, string downloadDir, string titleId, params int[] metadataVersions) { string titleDir = Path.Combine(downloadDir, titleId); Directory.CreateDirectory(titleDir); // Download Common ETicKet (CETK) await DownloadTitleFile(client, titleDir, titleId, TicketFileName); // Download Title MetaData (TMD) string[] metadataNames = DownloadSuffixlessMetadata ? metadataVersions.Select(v => $"{MetadataFileName}.{v}").Union(new[] { MetadataFileName }).ToArray() : metadataVersions.Select(v => $"{MetadataFileName}.{v}").ToArray(); foreach (string metadataName in metadataNames) { string metadataDir = Path.Combine(titleDir, metadataName); Directory.CreateDirectory(metadataDir); string metadataPath = await DownloadTitleFile(client, metadataDir, titleId, metadataName); if (metadataPath == null) { continue; } // Parse out the content IDs from the metadata file, and download them TitleMetadata metadata = new TitleMetadata(metadataPath); for (int i = 0; i < metadata.NumContents; i++) { await DownloadTitleFile(client, metadataDir, titleId, metadata.ContentInfo[i].Id.ToString("X8")); } } }
public static bool VerifyMetadataContent(TitleMetadata metadata, string decPath, int contentIndex, string titleDir, string contentIdStr) { byte[] decHash = Hasher.CalcSha1Hash(decPath); if (!metadata.ContentInfo[contentIndex].DecSha1Hash.SequenceEqual(decHash)) { Log.Instance.Error( $"Hash for '{decPath}' does not match the recorded hash in '{Path.Combine(titleDir, metadata.FileName)}' (content index {metadata.ContentInfo[contentIndex].Index}). " + $"(`{metadata.ContentInfo[contentIndex].DecSha1Hash.ToHexString()}` != `{decHash.ToHexString()}`)"); return(false); } Log.Instance.Trace( $"Hash for '{decPath}' matches the recorded hash in '{Path.Combine(titleDir, metadata.FileName)}' (content index {metadata.ContentInfo[contentIndex].Index}). " + $"(`{metadata.ContentInfo[contentIndex].DecSha1Hash.ToHexString()}`)"); return(true); }
public static async Task <(TicketBooth.Ticket ticket, List <string> contentsList)> MakeTicketAndDecryptMetadataContents(byte[] titleIdBytes, TitleMetadata metadata, string titleDir, bool makeQolFiles = false) { if (metadata.NumContents <= 0) { return(null, new List <string>()); } List <string> contentsList = new List <string>(); TicketBooth.Ticket ticket = null; bool success = false; string contentPath = Path.Combine(titleDir, metadata.ContentInfo[0].Id.ToString("x8")); string appPath = ""; // TODO: Not needed for any known DSiWare titles, but could be a decent idea to test the gamecode of the game as a password foreach (string tryPass in KeyGenTryPasswords) { byte[] titleKey = TitleKeyGen.Derive(titleIdBytes, tryPass); ticket = new TicketBooth.Ticket(titleKey); appPath = await ticket.DecryptContent(metadata.ContentInfo[0].Index, contentPath); RomInfo cInfo = new RomInfo(appPath, makeQolFiles); success = cInfo.ValidContent; if (!success) { continue; } Log.Instance.Trace($"'{tryPass}' is the password for the title key of '{contentPath}'!"); // Somewhat redundant given the CRC validation of the decrypted ROM in RomInfo, but still good to check if (VerifyMetadataContent(metadata, appPath, 0, titleDir, metadata.ContentInfo[0].Id.ToString("x8"))) { await File.WriteAllTextAsync(Path.Combine(titleDir, TitlePasswordFileName), tryPass); await File.WriteAllBytesAsync(Path.Combine(titleDir, TitleKeyFileName), titleKey); } contentsList.Add(metadata.ContentInfo[0].Id.ToString("x8")); if (makeQolFiles) { MakeQolFiles(cInfo, titleDir); } break; } if (!success) { Log.Instance.Error($"Unable to find the password for the title key of '{contentPath}'."); File.Delete(appPath); return(null, new List <string>()); } for (int i = 1; i < metadata.NumContents; i++) { string contentName = metadata.ContentInfo[i].Id.ToString("x8"); contentsList.Add(contentName); contentPath = Path.Combine(titleDir, contentName); string decPath = await ticket.DecryptContent(metadata.ContentInfo[i].Index, contentPath); VerifyMetadataContent(metadata, decPath, i, titleDir, contentName); } return(ticket, contentsList); }
public static async Task <List <string> > DecryptMetadataContents(byte[] titleIdBytes, TicketBooth.Ticket ticket, TitleMetadata metadata, string titleDir, bool makeQolFiles = false) { if (metadata.NumContents <= 0) { return(new List <string>()); } List <string> contentsList = new List <string>(); for (int i = 0; i < metadata.NumContents; i++) { string contentName = metadata.ContentInfo[i].Id.ToString("x8"); contentsList.Add(contentName); string contentPath = Path.Combine(titleDir, contentName); string appPath = await ticket.DecryptContent(metadata.ContentInfo[i].Index, contentPath); // TODO: Could verify enc and dec file sizes match metadata recorded size, but at this point I don't think it's worth the additional processing time. if (VerifyMetadataContent(metadata, appPath, i, titleDir, contentName)) { string titlePass = DeriveTitlePassFromKey(titleIdBytes, ticket.TitleKey); if (titlePass != null) { await File.WriteAllTextAsync(Path.Combine(titleDir, TitlePasswordFileName), titlePass); } else { Log.Instance.Error($"Unable to find the title pass for title '{titleDir}'."); } await File.WriteAllBytesAsync(Path.Combine(titleDir, TitleKeyFileName), ticket.TitleKey); } if (i != 0 || !makeQolFiles) { continue; } RomInfo cInfo = new RomInfo(appPath, makeQolFiles); MakeQolFiles(cInfo, titleDir); } return(contentsList); }