From 25ca59bf86ecfadbde6659bd77bbffd44f79dbae Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Tue, 17 Mar 2026 19:35:42 +0000 Subject: [PATCH] [Convert] Add title key extraction from SI/GI partitions for Wii U --- Aaru.Core/Image/Convert/WiiU/WiiuTitleKeys.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 Aaru.Core/Image/Convert/WiiU/WiiuTitleKeys.cs diff --git a/Aaru.Core/Image/Convert/WiiU/WiiuTitleKeys.cs b/Aaru.Core/Image/Convert/WiiU/WiiuTitleKeys.cs new file mode 100644 index 000000000..fcb0423ae --- /dev/null +++ b/Aaru.Core/Image/Convert/WiiU/WiiuTitleKeys.cs @@ -0,0 +1,155 @@ +using System; +using System.Text; +using Aaru.CommonTypes.Interfaces; +using Aaru.Helpers; + +namespace Aaru.Core.Image.WiiU; + +/// Wii U title key extraction from SI/GI partitions. +static class WiiuTitleKeys +{ + /// + /// Extract title keys from TITLE.TIK files within SI and GI partitions, + /// and assign them to matching GM partitions. Partitions without a title key + /// are assigned the disc key as fallback. + /// + /// Source image. + /// 16-byte disc key. + /// Partition array to update with extracted keys. + /// Number of title keys found. + public static int ExtractTitleKeys(IMediaImage inputImage, byte[] discKey, WiiuPartition[] partitions) + { + int keysFound = 0; + + for(int p = 0; p < partitions.Length; p++) + { + // Only scan SI and GI partitions + if(!partitions[p].Identifier.StartsWith("SI", StringComparison.Ordinal) && + !partitions[p].Identifier.StartsWith("GI", StringComparison.Ordinal)) + continue; + + ulong partDiscOff = WiiuCrypto.ENCRYPTED_OFFSET + + (ulong)partitions[p].StartSector * (uint)WiiuCrypto.SECTOR_SIZE - + 0x10000; + + // Read FST header (first physical sector of partition) + byte[] fstHdr = WiiuToc.ReadVolumeDecrypted(inputImage, discKey, partDiscOff, 0, + (uint)WiiuCrypto.SECTOR_SIZE); + + if(fstHdr == null) continue; + + // Verify "FST\0" magic + if(fstHdr[0] != 'F' || fstHdr[1] != 'S' || fstHdr[2] != 'T' || fstHdr[3] != 0) continue; + + uint offsetFactor = BigEndianBitConverter.ToUInt32(fstHdr, 4); + uint clusterCount = BigEndianBitConverter.ToUInt32(fstHdr, 8); + + var clusterOffsets = new ulong[clusterCount]; + + for(uint c = 0; c < clusterCount; c++) + { + uint raw = BigEndianBitConverter.ToUInt32(fstHdr, (int)(0x20 + c * 0x20)); + ulong start = (ulong)raw * (uint)WiiuCrypto.SECTOR_SIZE; + + clusterOffsets[c] = start > (uint)WiiuCrypto.SECTOR_SIZE + ? start - (uint)WiiuCrypto.SECTOR_SIZE + : 0; + } + + ulong entriesOffset = (ulong)offsetFactor * clusterCount + 0x20; + + // Read root entry to get total entries + byte[] rootEntryData = WiiuToc.ReadVolumeDecrypted(inputImage, discKey, partDiscOff, entriesOffset, 0x10); + + if(rootEntryData == null) continue; + + uint totalEntries = BigEndianBitConverter.ToUInt32(rootEntryData, 8); + + if(totalEntries > 100000) continue; + + ulong nameTableOffset = entriesOffset + (ulong)totalEntries * 0x10; + + // Read all FST entries + name table + uint fstDataSize = (uint)(nameTableOffset - entriesOffset + 0x10000); + + if(fstDataSize > 16 * 1024 * 1024) continue; + + byte[] fstData = WiiuToc.ReadVolumeDecrypted(inputImage, discKey, partDiscOff, entriesOffset, fstDataSize); + + if(fstData == null) continue; + + uint nameTableBase = (uint)(nameTableOffset - entriesOffset); + + // Scan for TITLE.TIK files + for(uint e = 0; e < totalEntries; e++) + { + int entOff = (int)(e * 0x10); + byte type = fstData[entOff]; + uint nameOff = BigEndianBitConverter.ToUInt32(fstData, entOff) & 0x00FFFFFF; + ulong fileOff = (ulong)BigEndianBitConverter.ToUInt32(fstData, entOff + 4) << 5; + uint fileSize = BigEndianBitConverter.ToUInt32(fstData, entOff + 8); + ushort clusterId = (ushort)((fstData[entOff + 0x0E] << 8) | fstData[entOff + 0x0F]); + + if(type == 1) continue; // directory + + if(fileSize < 0x200) continue; + + if(nameOff >= fstDataSize - nameTableBase) continue; + + // Get filename from name table + int nameStart = (int)(nameTableBase + nameOff); + int nameEnd = nameStart; + + while(nameEnd < fstData.Length && fstData[nameEnd] != 0) nameEnd++; + + string fname = Encoding.ASCII.GetString(fstData, nameStart, nameEnd - nameStart); + + if(!fname.Equals("title.tik", StringComparison.OrdinalIgnoreCase)) continue; + + if(clusterId >= clusterCount) continue; + + // Read ticket: encrypted title key at TIK+0x1BF, title ID at TIK+0x1DC + ulong tikVolumeOff = clusterOffsets[clusterId] + fileOff; + + byte[] tikBuf = WiiuToc.ReadVolumeDecrypted(inputImage, discKey, partDiscOff, + tikVolumeOff + 0x1BF, 0x10 + 0x1D + 8); + + if(tikBuf == null) continue; + + var encTitleKey = new byte[16]; + var titleId = new byte[8]; + Array.Copy(tikBuf, 0, encTitleKey, 0, 16); + Array.Copy(tikBuf, 0x1D, titleId, 0, 8); + + // Decrypt title key + byte[] decTitleKey = WiiuCrypto.DecryptTitleKey(encTitleKey, titleId); + + // Build expected GM partition name from title ID + string gmName = $"GM{titleId[0]:X2}{titleId[1]:X2}{titleId[2]:X2}{titleId[3]:X2}" + + $"{titleId[4]:X2}{titleId[5]:X2}{titleId[6]:X2}{titleId[7]:X2}"; + + // Match to a GM partition + for(int g = 0; g < partitions.Length; g++) + { + if(!partitions[g].Identifier.StartsWith("GM", StringComparison.Ordinal)) continue; + + if(!partitions[g].Identifier.StartsWith(gmName, StringComparison.Ordinal)) continue; + + Array.Copy(decTitleKey, partitions[g].Key, 16); + partitions[g].HasTitleKey = true; + keysFound++; + + break; + } + } + } + + // Set disc key as fallback for partitions without title key + for(int i = 0; i < partitions.Length; i++) + { + if(!partitions[i].HasTitleKey) Array.Copy(discKey, partitions[i].Key, 16); + } + + return keysFound; + } +} \ No newline at end of file