├── .gitignore ├── README.md ├── iQueTool.sln └── iQueTool ├── App.config ├── Files ├── iQueArrayFile.cs ├── iQueCertCollection.cs ├── iQueKernel.cs └── iQueNand.cs ├── Natives.cs ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx ├── Shared.cs ├── Structs ├── BbContentMetadataHead.cs ├── BbCrlHead.cs ├── BbFat16.cs ├── BbLaunchMetadata.cs ├── BbRsaCert.cs ├── BbTicketHead.cs ├── OSBbSaGameMetaData.cs ├── iQueBlockSpare.cs ├── iQuePrivateData.cs ├── iQueSysAppSigArea.cs └── iQueUserData.cs ├── iQueTool.csproj └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | ServiceFabricBackup/ 241 | 242 | # SQL Server files 243 | *.mdf 244 | *.ldf 245 | *.ndf 246 | 247 | # Business Intelligence projects 248 | *.rdl.data 249 | *.bim.layout 250 | *.bim_*.settings 251 | 252 | # Microsoft Fakes 253 | FakesAssemblies/ 254 | 255 | # GhostDoc plugin setting file 256 | *.GhostDoc.xml 257 | 258 | # Node.js Tools for Visual Studio 259 | .ntvs_analysis.dat 260 | node_modules/ 261 | 262 | # Visual Studio 6 build log 263 | *.plg 264 | 265 | # Visual Studio 6 workspace options file 266 | *.opt 267 | 268 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 269 | *.vbw 270 | 271 | # Visual Studio LightSwitch build output 272 | **/*.HTMLClient/GeneratedArtifacts 273 | **/*.DesktopClient/GeneratedArtifacts 274 | **/*.DesktopClient/ModelManifest.xml 275 | **/*.Server/GeneratedArtifacts 276 | **/*.Server/ModelManifest.xml 277 | _Pvt_Extensions 278 | 279 | # Paket dependency manager 280 | .paket/paket.exe 281 | paket-files/ 282 | 283 | # FAKE - F# Make 284 | .fake/ 285 | 286 | # JetBrains Rider 287 | .idea/ 288 | *.sln.iml 289 | 290 | # CodeRush 291 | .cr/ 292 | 293 | # Python Tools for Visual Studio (PTVS) 294 | __pycache__/ 295 | *.pyc 296 | 297 | # Cake - Uncomment if you are using it 298 | # tools/** 299 | # !tools/packages.config 300 | 301 | # Tabs Studio 302 | *.tss 303 | 304 | # Telerik's JustMock configuration file 305 | *.jmconfig 306 | 307 | # BizTalk build output 308 | *.btp.cs 309 | *.btm.cs 310 | *.odx.cs 311 | *.xsd.cs 312 | 313 | # OpenCover UI analysis results 314 | OpenCover/ 315 | 316 | # Azure Stream Analytics local run output 317 | ASALocalRun/ 318 | 319 | # MSBuild Binary and Structured Log 320 | *.binlog 321 | 322 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | iQueTool 2 | ======== 3 | 4 | ``` 5 | iQueTool 0.4a: iQue Player file manipulator 6 | Usage : iquetool.exe [mode] [parameters] [filepath] 7 | 8 | Valid modes: nand / tickets / certs / crl / sparefix 9 | 10 | General parameters: 11 | -h (-help) - print iquetool usage 12 | -i (-info) - print basic info about file 13 | -wi (-writeinfo) - write detailed info about file to [filepath].txt 14 | -o (-output) - specify output filename/directory 15 | 16 | Mode "tickets" / "certs" / "crl": 17 | 18 | -x - extracts all entries from file 19 | -xi (-extractids) - extract entries with these indexes 20 | -xc (-extractcids) - extract entries with these content ids 21 | -xt (-extracttids) - extract entries with these ticket ids 22 | 23 | -n - writes extracted entries into a single array file 24 | 25 | Note that by default the extract commands above will extract tickets as seperate files 26 | with the format \ticket---.dat 27 | 28 | Mode "nand": 29 | 30 | -x - extracts all files from NAND 31 | -xk (-extractkernel) - extract secure-kernel from NAND 32 | -fs (-showallfs) - shows info about all found FS blocks 33 | 34 | -a - adds files to NAND 35 | -al (-addlist) - adds line-seperated list of files to NAND 36 | 37 | -d - delete files from NAND FS 38 | 39 | -uk (-updatekernel) - updates NAND with the given (cache) SKSA 40 | also takes bad-blocks into account and will work around them 41 | use -gs afterwards to generate a new spare with proper SAData fields 42 | 43 | -gs (-genspare) - generates block-spare/ECC data for this NAND 44 | -gp (-fullspare) - will generate page-spare/ECC data (0x20 pages per block) instead 45 | 46 | -sc (-skipchecksums) - skip verifying FS checksums 47 | -fc (-fixchecksums) - skips verifying & repairs all FS checksums 48 | -bd (-baddump) - will try reading inodes with a 0x10 byte offset 49 | 50 | Mode "sparefix": 51 | usage: sparefix [spare.bin path] 52 | fixes overdump / raw page-spare dumps to match BB block-spare dumps 53 | if nand.bin path is specified, will correct the spare data using that nand 54 | 55 | -o (-output) - specify output filename (default: [input]_fixed) 56 | -gp (-fullspare) - disables reducing page-spare data to block-spare 57 | 58 | iQue signature verification: 59 | To enable, drop a cert.sys file (taken from an iQue NAND) next to the iQueTool exe 60 | Alternatively you can put it at the root of your D: drive 61 | Also when opening a NAND image the cert.sys will automatically be loaded from it, if not already found locally 62 | ``` 63 | -------------------------------------------------------------------------------- /iQueTool.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.14 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "iQueTool", "iQueTool\iQueTool.csproj", "{BDE052C7-ADF7-4B11-B142-D33AF0CCFCF7}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {BDE052C7-ADF7-4B11-B142-D33AF0CCFCF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {BDE052C7-ADF7-4B11-B142-D33AF0CCFCF7}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {BDE052C7-ADF7-4B11-B142-D33AF0CCFCF7}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {BDE052C7-ADF7-4B11-B142-D33AF0CCFCF7}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {31D02C58-7F39-458B-B013-247BBC589470} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /iQueTool/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /iQueTool/Files/iQueArrayFile.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text; 3 | 4 | namespace iQueTool.Files 5 | { 6 | public class iQueArrayFile : List 7 | { 8 | private IO io; 9 | public string FilePath; 10 | 11 | public iQueArrayFile(string filePath) 12 | { 13 | FilePath = filePath; 14 | io = new IO(filePath); 15 | Read(); 16 | } 17 | 18 | public iQueArrayFile(byte[] data) 19 | { 20 | io = new IO(new System.IO.MemoryStream(data)); 21 | Read(); 22 | } 23 | 24 | bool Read() 25 | { 26 | var numEntries = io.Reader.ReadUInt32().EndianSwap(); 27 | for (uint i = 0; i < numEntries; i++) 28 | { 29 | var entry = io.Reader.ReadStruct(); 30 | Add(entry); 31 | } 32 | return true; 33 | } 34 | 35 | public override string ToString() 36 | { 37 | return ToString(false); 38 | } 39 | 40 | public string ToString(bool formatted) 41 | { 42 | var b = new StringBuilder(); 43 | b.AppendLine($"Num {typeof(T).Name} entries: {Count}"); 44 | b.AppendLine(); 45 | for (int i = 0; i < Count; i++) 46 | { 47 | b.AppendLine(this[i].ToString()); 48 | } 49 | 50 | return b.ToString(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /iQueTool/Files/iQueCertCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using iQueTool.Structs; 5 | 6 | namespace iQueTool.Files 7 | { 8 | public class iQueCertCollection : List 9 | { 10 | private IO io; 11 | public string FilePath; 12 | 13 | public static iQueCertCollection MainCollection; // fml... 14 | 15 | public iQueCertCollection(string filePath) 16 | { 17 | FilePath = filePath; 18 | io = new IO(filePath); 19 | Read(); 20 | } 21 | 22 | public iQueCertCollection(byte[] data) 23 | { 24 | io = new IO(new System.IO.MemoryStream(data)); 25 | Read(); 26 | } 27 | 28 | bool Read() 29 | { 30 | var numEntries = io.Reader.ReadUInt32().EndianSwap(); 31 | for (uint i = 0; i < numEntries; i++) 32 | { 33 | var entry = io.Reader.ReadStruct(); 34 | entry.EndianSwap(); 35 | Add(entry); 36 | } 37 | return true; 38 | } 39 | 40 | public override string ToString() 41 | { 42 | return ToString(false); 43 | } 44 | 45 | public string ToString(bool formatted) 46 | { 47 | var b = new StringBuilder(); 48 | b.AppendLine($"Num iQueCertificate entries: {Count}"); 49 | b.AppendLine(); 50 | for (int i = 0; i < Count; i++) 51 | { 52 | b.AppendLine(this[i].ToString(formatted, $"iQueCertificate-{i}"));//, i)); 53 | } 54 | 55 | return b.ToString(); 56 | } 57 | 58 | public bool GetCertificate(string certName, out BbRsaCert cert) 59 | { 60 | foreach(var c in this) 61 | { 62 | if (c.CertNameString.Equals(certName, StringComparison.InvariantCultureIgnoreCase) || $"{c.AuthorityString}-{c.CertNameString}".Equals(certName, StringComparison.InvariantCultureIgnoreCase)) 63 | { 64 | cert = c; 65 | return true; 66 | } 67 | } 68 | 69 | cert = new BbRsaCert(); 70 | return false; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /iQueTool/Files/iQueKernel.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | using iQueTool.Structs; 4 | 5 | namespace iQueTool.Files 6 | { 7 | public class iQueKernel 8 | { 9 | public const int SK_NUM_BLOCKS = 4; 10 | 11 | public const int SIGAREA_ADDR = 0x10000; 12 | public const int SIGAREA_END_ADDR = 0x14000; 13 | public const int SIGAREA_SZ = 0x4000; 14 | public const int SIGAREA_SZ_RAW = 0xBB8; 15 | 16 | private long startAddr; 17 | private IO io; 18 | public string FilePath; 19 | 20 | public byte[] SKData; 21 | 22 | public long SA1Addr = -1; 23 | public iQueSysAppSigArea SA1SigArea; 24 | public byte[] SA1Data; 25 | 26 | public long SA2Addr = -1; 27 | public iQueSysAppSigArea SA2SigArea; 28 | public byte[] SA2Data; 29 | 30 | public bool SA2IsValid 31 | { 32 | get 33 | { 34 | return SA2Addr > -1 && SA2SigArea.ContentMetadata.ContentSize > 0 && SA2SigArea.IsValid; 35 | } 36 | } 37 | 38 | public iQueKernel(string filePath) 39 | { 40 | FilePath = filePath; 41 | io = new IO(filePath); 42 | } 43 | 44 | public iQueKernel(IO io) 45 | { 46 | this.io = io; 47 | } 48 | 49 | public iQueKernel(byte[] data) 50 | { 51 | this.io = new IO(new MemoryStream(data)); 52 | } 53 | 54 | public bool Read() 55 | { 56 | startAddr = io.Stream.Position; 57 | SKData = io.Reader.ReadBytes(SK_NUM_BLOCKS * 0x4000); 58 | 59 | SA1Addr = startAddr + SIGAREA_ADDR; 60 | if (SA1Addr + SIGAREA_SZ_RAW >= io.Stream.Length) 61 | { 62 | SA1Addr = -1; 63 | return false; 64 | } 65 | 66 | // read SA1 ticket 67 | io.Stream.Position = SA1Addr; 68 | SA1SigArea = io.Reader.ReadStruct(); 69 | SA1SigArea.EndianSwap(); 70 | 71 | if (SA1SigArea.ContentMetadata.ContentSize == 0 || !SA1SigArea.IsValid) 72 | return true; // SA1 has no data 73 | 74 | // read SA1 data 75 | io.Stream.Position = SA1Addr + SIGAREA_SZ; 76 | SA1Data = io.Reader.ReadBytes((int)SA1SigArea.ContentMetadata.ContentSize); 77 | 78 | // check if there might be a valid SA2 area 79 | SA2Addr = SA1Addr + SIGAREA_SZ + SA1SigArea.ContentMetadata.ContentSize; 80 | if(SA2Addr + SIGAREA_SZ_RAW >= io.Stream.Length) 81 | { 82 | SA2Addr = -1; 83 | return true; // we read the SA1 fine so return true 84 | } 85 | 86 | // read SA2 ticket 87 | io.Stream.Position = SA2Addr; 88 | SA2SigArea = io.Reader.ReadStruct(); 89 | SA2SigArea.EndianSwap(); 90 | 91 | if (!SA2IsValid) 92 | return true; // don't try reading SA2 data if metadata isn't valid 93 | 94 | // read SA2 data 95 | io.Stream.Position = SA2Addr + SIGAREA_SZ; 96 | SA2Data = io.Reader.ReadBytes((int)SA2SigArea.ContentMetadata.ContentSize); 97 | 98 | return true; 99 | } 100 | 101 | public override string ToString() 102 | { 103 | return ToString(false); 104 | } 105 | 106 | public string ToString(bool formatted) 107 | { 108 | var b = new StringBuilder(); 109 | b.AppendLineSpace(SA1SigArea.ToString(formatted, "SKSA.SA1SigArea")); 110 | if(SA2IsValid) 111 | { 112 | b.AppendLine(); 113 | b.AppendLineSpace(SA2SigArea.ToString(formatted, "SKSA.SA2SigArea")); 114 | } 115 | 116 | return b.ToString(); 117 | } 118 | 119 | public void Extract(string extPath) 120 | { 121 | io.Stream.Position = startAddr + SIGAREA_ADDR; 122 | byte[] sa1sig = io.Reader.ReadBytes(SIGAREA_SZ); 123 | byte[] sa1 = io.Reader.ReadBytes((int)SA1SigArea.ContentMetadata.ContentSize); 124 | 125 | File.WriteAllBytes(extPath + $".{SA1SigArea.ContentMetadata.ContentId}-sa1sig", sa1sig); 126 | if(sa1.Length > 0) 127 | File.WriteAllBytes(extPath + $".{SA1SigArea.ContentMetadata.ContentId}-sa1", sa1); 128 | if (SA2IsValid) 129 | { 130 | byte[] sa2sig = io.Reader.ReadBytes(SIGAREA_SZ); 131 | byte[] sa2 = io.Reader.ReadBytes((int)SA2SigArea.ContentMetadata.ContentSize); 132 | File.WriteAllBytes(extPath + $".{SA2SigArea.ContentMetadata.ContentId}-sa2sig", sa2sig); 133 | if (sa2.Length > 0) 134 | File.WriteAllBytes(extPath + $".{SA2SigArea.ContentMetadata.ContentId}-sa2", sa2); 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /iQueTool/Files/iQueNand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using iQueTool.Structs; 5 | using System.IO; 6 | 7 | namespace iQueTool.Files 8 | { 9 | public class iQueNand 10 | { 11 | public const int BLOCK_SZ = 0x4000; 12 | 13 | public const int NUM_SK_BLOCKS = 4; 14 | public const int NUM_FAT_BLOCKS = 0x10; 15 | public const int NUM_SYS_AREA_BLOCKS = 0x40; // 0x100000 bytes, but theres some SKSAs that are larger?? 16 | public const int NUM_SYS_AREA_BLOCKS_DEV = 0x100; // largest SKSA is 0xE9 blocks, so I guess dev units probably had 0x100 for SKSA area 17 | 18 | public const int NUM_BLOCKS_IN_FAT = 0x1000; 19 | public const int NUM_FS_ENTRIES = 0x199; 20 | public const int FS_HEADER_ADDR = 0x3FF4; 21 | 22 | public const int MAGIC_BBFS = 0x42424653; 23 | public const int MAGIC_BBFL = 0x4242464C; 24 | 25 | public const short FAT_BLOCK_FREE = 0; 26 | public const short FAT_BLOCK_LAST = -1; // 0xffff for last block in chain (this block doesn't point to another) 27 | public const short FAT_BLOCK_BAD = -2; // 0xfffe for bad blocks 28 | public const short FAT_BLOCK_RESERVED = -3; // 0xfffd for SK/SA (reserved) blocks 29 | 30 | private IO io; 31 | public string FilePath; 32 | public int InodesOffset = 0; // only public ique dump is sorta mangled, inodes start 0x10 bytes away from where they should for some reason 33 | public bool SkipVerifyFsChecksums = false; 34 | 35 | public List FsBlocks; 36 | public List FsBadBlocks; 37 | 38 | // todo: put next 3 fields in an iQueNandFs class? 39 | public List FsHeaders; 40 | // read table as signed so we can treat end-of-chain and sys-allocated blocks as negative numbers 41 | // if there were more than 0x8000 blocks in a nand we really shouldn't do this, but luckily there aren't 42 | public List> FsAllocationTables; 43 | public List> FsInodes; 44 | 45 | public BbFat16 MainFs; 46 | public int MainFsBlock; 47 | public int MainFsIndex = -1; 48 | 49 | public List ModifiedInodes; 50 | public List ModifiedAllocTable; 51 | 52 | public List MainFsInodes 53 | { 54 | get 55 | { 56 | if (MainFsIndex < 0) 57 | return null; 58 | 59 | return FsInodes[MainFsIndex]; 60 | } 61 | } 62 | 63 | public List MainFsAllocTable 64 | { 65 | get 66 | { 67 | if (MainFsIndex < 0) 68 | return null; 69 | 70 | return FsAllocationTables[MainFsIndex]; 71 | } 72 | } 73 | 74 | // different sections/files of the NAND 75 | public iQueKernel SKSA; 76 | 77 | public bool HasPrivateData = false; 78 | public iQuePrivateData PrivateData; // depot.sys 79 | 80 | public iQueArrayFile Tickets; // ticket.sys 81 | public iQueArrayFile CRL; // crl.sys 82 | public iQueCertCollection Certs; // cert.sys 83 | 84 | public iQueNand(string filePath) 85 | { 86 | FilePath = filePath; 87 | io = new IO(filePath); 88 | } 89 | 90 | public void SeekToBlock(int blockIdx) 91 | { 92 | io.Stream.Position = blockIdx * BLOCK_SZ; 93 | } 94 | 95 | public int GetInodeIdx(string fileName) 96 | { 97 | if (MainFsIndex < 0) 98 | return -1; 99 | 100 | for(int i = 0; i < MainFsInodes.Count; i++) 101 | if(MainFsInodes[i].NameString.ToLower() == fileName.ToLower()) 102 | return i; 103 | 104 | return -1; 105 | } 106 | 107 | public short[] GetBlockChain(short blockIdx, int maxBlocks = int.MaxValue) 108 | { 109 | var chain = new List(); 110 | var curBlock = blockIdx; 111 | 112 | do 113 | { 114 | chain.Add(curBlock); 115 | curBlock = MainFsAllocTable[curBlock]; 116 | } 117 | while (curBlock >= 0 && (maxBlocks == int.MaxValue || chain.Count < maxBlocks)); // if curBlock is negative (eg 0xFFFD or 0xFFFF) stop following the chain 118 | 119 | return chain.ToArray(); 120 | } 121 | 122 | public int GetNextGoodBlock(int blockNum) 123 | { 124 | for (int i = blockNum + 1; i < NUM_BLOCKS_IN_FAT; i++) 125 | if (MainFsAllocTable[i] != FAT_BLOCK_BAD) 126 | return i; 127 | 128 | return -1; 129 | } 130 | 131 | public byte[] GetSKSAData() 132 | { 133 | using (var ms = new MemoryStream()) 134 | { 135 | var sksa = new IO(ms); 136 | 137 | // read SK data 138 | io.Stream.Position = 0; 139 | sksa.Writer.Write(io.Reader.ReadBytes(NUM_SK_BLOCKS * BLOCK_SZ)); 140 | 141 | int lastDataBlock = 0; 142 | int curDataBlock = NUM_SK_BLOCKS; 143 | 144 | // read SA1 metadata area 145 | int sa1MetadataBlock = curDataBlock; 146 | io.Stream.Position = curDataBlock * BLOCK_SZ; 147 | byte[] sa1MetaDataBytes = io.Reader.ReadBytes(BLOCK_SZ); 148 | 149 | var sa1Metadata = Shared.BytesToStruct(sa1MetaDataBytes); 150 | sa1Metadata.EndianSwap(); 151 | 152 | sksa.Writer.Write(sa1MetaDataBytes); 153 | 154 | // get next good block after SA1 metadata (which will be SA1 final block) 155 | curDataBlock = GetNextGoodBlock(curDataBlock); 156 | int sa1FinalDataBlock = curDataBlock; 157 | 158 | int numDataBlocks = (int)(sa1Metadata.ContentSize / BLOCK_SZ); 159 | byte[] sa1Data = new byte[sa1Metadata.ContentSize]; 160 | for (int i = numDataBlocks - 1; i >= 0; i--) 161 | { 162 | io.Stream.Position = curDataBlock * BLOCK_SZ; 163 | byte[] curData = io.Reader.ReadBytes(BLOCK_SZ); 164 | Array.Copy(curData, 0, sa1Data, i * BLOCK_SZ, BLOCK_SZ); 165 | 166 | lastDataBlock = curDataBlock; 167 | curDataBlock = GetNextGoodBlock(curDataBlock); 168 | } 169 | sksa.Writer.Write(sa1Data); 170 | 171 | // read SA2 metadata area 172 | int sa2MetadataBlock = curDataBlock; 173 | io.Stream.Position = curDataBlock * BLOCK_SZ; 174 | byte[] sa2MetadataBytes = io.Reader.ReadBytes(BLOCK_SZ); 175 | 176 | var sa2Metadata = Shared.BytesToStruct(sa2MetadataBytes); 177 | sa2Metadata.EndianSwap(); 178 | 179 | if(sa2Metadata.AuthorityString.StartsWith("Root")) // if SA2 is valid... 180 | { 181 | sksa.Writer.Write(sa2MetadataBytes); 182 | 183 | // get next good block after SA2 metadata (which will be SA2 final block) 184 | curDataBlock = GetNextGoodBlock(curDataBlock); 185 | int sa2FinalDataBlock = curDataBlock; 186 | 187 | numDataBlocks = (int)(sa2Metadata.ContentSize / BLOCK_SZ); 188 | byte[] sa2Data = new byte[sa2Metadata.ContentSize]; 189 | for (int i = numDataBlocks - 1; i >= 0; i--) 190 | { 191 | io.Stream.Position = curDataBlock * BLOCK_SZ; 192 | byte[] curData = io.Reader.ReadBytes(BLOCK_SZ); 193 | Array.Copy(curData, 0, sa2Data, i * BLOCK_SZ, BLOCK_SZ); 194 | 195 | lastDataBlock = curDataBlock; 196 | curDataBlock = GetNextGoodBlock(curDataBlock); 197 | } 198 | sksa.Writer.Write(sa2Data); 199 | } 200 | 201 | return ms.ToArray(); 202 | } 203 | } 204 | 205 | public bool SetSKSAData(string sksaPath) 206 | { 207 | return SetSKSAData(File.ReadAllBytes(sksaPath)); 208 | } 209 | 210 | public bool SetSKSAData(byte[] sksaData) 211 | { 212 | var sksa = new iQueKernel(sksaData); 213 | sksa.Read(); 214 | 215 | // write SK data 216 | io.Stream.Position = 0; 217 | io.Writer.Write(sksa.SKData); 218 | 219 | int lastDataBlock = -1; 220 | int curDataBlock = NUM_SK_BLOCKS; 221 | 222 | // write SA1 ticket area 223 | int sa1TicketBlock = curDataBlock; 224 | io.Stream.Position = curDataBlock * BLOCK_SZ; 225 | io.Writer.Write(new byte[BLOCK_SZ]); 226 | io.Stream.Position = curDataBlock * BLOCK_SZ; 227 | io.Writer.Write(sksa.SA1SigArea.GetBytes()); 228 | 229 | // get next good block after SA1 ticket (which will be SA1 final block) 230 | curDataBlock = GetNextGoodBlock(curDataBlock); 231 | int sa1FinalDataBlock = curDataBlock; 232 | 233 | int numDataBlocks = (int)(sksa.SA1SigArea.ContentMetadata.ContentSize / BLOCK_SZ); 234 | for (int i = numDataBlocks - 1; i >= 0; i--) 235 | { 236 | byte[] data = new byte[BLOCK_SZ]; 237 | Array.Copy(sksa.SA1Data, i * BLOCK_SZ, data, 0, BLOCK_SZ); 238 | 239 | io.Stream.Position = curDataBlock * BLOCK_SZ; 240 | io.Writer.Write(data); 241 | 242 | lastDataBlock = curDataBlock; 243 | curDataBlock = GetNextGoodBlock(curDataBlock); 244 | } 245 | 246 | if (sksa.SA2IsValid) 247 | { 248 | // write SA2 ticket area 249 | int sa2TicketBlock = curDataBlock; 250 | io.Stream.Position = curDataBlock * BLOCK_SZ; 251 | io.Writer.Write(new byte[BLOCK_SZ]); 252 | io.Stream.Position = curDataBlock * BLOCK_SZ; 253 | io.Writer.Write(sksa.SA2SigArea.GetBytes()); 254 | 255 | // get next good block after SA2 ticket (which will be SA2 final block) 256 | curDataBlock = GetNextGoodBlock(curDataBlock); 257 | int sa2FinalDataBlock = curDataBlock; 258 | 259 | numDataBlocks = (int)(sksa.SA2SigArea.ContentMetadata.ContentSize / BLOCK_SZ); 260 | for (int i = numDataBlocks - 1; i >= 0; i--) 261 | { 262 | byte[] data = new byte[BLOCK_SZ]; 263 | Array.Copy(sksa.SA2Data, i * BLOCK_SZ, data, 0, BLOCK_SZ); 264 | 265 | io.Stream.Position = curDataBlock * BLOCK_SZ; 266 | io.Writer.Write(data); 267 | 268 | lastDataBlock = curDataBlock; 269 | curDataBlock = GetNextGoodBlock(curDataBlock); 270 | } 271 | } 272 | 273 | io.Stream.Flush(); 274 | 275 | Console.WriteLine("Updated SKSA!"); 276 | Console.WriteLine("Wrote updated nand to " + FilePath); 277 | 278 | // reload this.SKSA 279 | SKSA = new iQueKernel(GetSKSAData()); 280 | SKSA.Read(); 281 | 282 | return true; 283 | } 284 | 285 | public bool Read() 286 | { 287 | var numBlocks = (int)(io.Stream.Length / BLOCK_SZ); 288 | 289 | if(numBlocks != NUM_BLOCKS_IN_FAT) 290 | return false; // invalid image 291 | 292 | var ret = ReadFilesystem(); 293 | if (!ret) 294 | return false; // failed to find valid FAT 295 | 296 | SKSA = new iQueKernel(GetSKSAData()); 297 | SKSA.Read(); 298 | 299 | int idx = GetInodeIdx("cert.sys"); 300 | if (idx >= 0) 301 | { 302 | var data = FileRead(MainFsInodes[idx]); 303 | Certs = new iQueCertCollection(data); 304 | if (iQueCertCollection.MainCollection == null) 305 | iQueCertCollection.MainCollection = new iQueCertCollection(data); // MainCollection wasn't loaded already, so lets try loading it from this nand (yolo) 306 | } 307 | 308 | idx = GetInodeIdx("depot.sys"); 309 | HasPrivateData = idx >= 0; 310 | if(HasPrivateData) 311 | { 312 | var data = FileRead(MainFsInodes[idx]); 313 | PrivateData = Shared.BytesToStruct(data); 314 | //PrivateData.EndianSwap(); 315 | } 316 | 317 | idx = GetInodeIdx("ticket.sys"); 318 | if(idx >= 0) 319 | { 320 | var data = FileRead(MainFsInodes[idx]); 321 | Tickets = new iQueArrayFile(data); 322 | for (int i = 0; i < Tickets.Count; i++) 323 | Tickets[i] = Tickets[i].EndianSwap(); 324 | } 325 | 326 | idx = GetInodeIdx("crl.sys"); 327 | if (idx >= 0) 328 | { 329 | var data = FileRead(MainFsInodes[idx]); 330 | CRL = new iQueArrayFile(data); 331 | foreach (var crl in CRL) 332 | crl.EndianSwap(); 333 | } 334 | 335 | // todo: 336 | // recrypt.sys 337 | // timer.sys 338 | // sig.db 339 | 340 | return true; 341 | } 342 | 343 | public void RepairFsChecksum(int fatBlockIdx) 344 | { 345 | io.Stream.Position = fatBlockIdx * BLOCK_SZ; 346 | 347 | ushort sum = 0; 348 | for (int i = 0; i < 0x1FFF; i++) 349 | sum += io.Reader.ReadUInt16().EndianSwap(); 350 | 351 | ushort res = (ushort)(0xCAD7 - sum); 352 | io.Stream.Position = (fatBlockIdx * BLOCK_SZ) + 0x3FFE; 353 | io.Writer.Write(res.EndianSwap()); // todo: reload FS / change FsHeaders[x].CheckSum 354 | } 355 | 356 | public void RepairFsChecksums() 357 | { 358 | foreach (var block in FsBlocks) 359 | RepairFsChecksum(block); 360 | } 361 | 362 | public bool VerifyFsChecksum(BbFat16 fatHeader, int fatBlockIdx) 363 | { 364 | io.Stream.Position = (fatBlockIdx * BLOCK_SZ); 365 | 366 | ushort sum = 0; 367 | for (int i = 0; i < 0x1FFF; i++) 368 | sum += io.Reader.ReadUInt16().EndianSwap(); 369 | 370 | sum += fatHeader.CheckSum; // should be EndianSwap'd already by ReadFilesystem 371 | return sum == 0xCAD7; 372 | } 373 | 374 | public bool VerifyFsChecksum(int fsIndex) 375 | { 376 | if (fsIndex < 0 || fsIndex >= FsHeaders.Count || fsIndex >= FsBlocks.Count) 377 | return false; 378 | 379 | var fatHeader = FsHeaders[fsIndex]; 380 | var fatBlockIdx = FsBlocks[fsIndex]; 381 | 382 | return VerifyFsChecksum(fatHeader, fatBlockIdx); 383 | } 384 | 385 | public byte[] GenerateSpareData(bool blockSpare, byte[] oldSpare = null) 386 | { 387 | int pageSkip = blockSpare ? 0x1F : 0; // block-spares only include the last page of each block 388 | 389 | using (var fixedStream = new MemoryStream()) 390 | { 391 | var numPages = io.Stream.Length / 0x200; 392 | var numSpareEntries = blockSpare ? io.Stream.Length / 0x4000 : numPages; 393 | var spareEntriesPerBlock = blockSpare ? 1 : 0x20; 394 | 395 | if (oldSpare != null) 396 | fixedStream.Write(oldSpare, 0, oldSpare.Length); 397 | else 398 | { 399 | // init empty spare 400 | for (int i = 0; i < (numSpareEntries * 0x10); i++) 401 | fixedStream.WriteByte(0xFF); 402 | } 403 | 404 | // fix SA-area spare bytes (first 3 bytes of spare, only set for SA1/SA2 blocks) 405 | { 406 | // write 0xFF as SAData for the SK 407 | fixedStream.Position = 0; 408 | for(int blockNum = 0; blockNum < NUM_SK_BLOCKS; blockNum++) 409 | { 410 | for(int page = 0; page < spareEntriesPerBlock; page++) 411 | { 412 | fixedStream.WriteByte(0xFF); 413 | fixedStream.WriteByte(0xFF); 414 | fixedStream.WriteByte(0xFF); 415 | fixedStream.Position += (0x10 - 3); 416 | } 417 | } 418 | 419 | int lastDataBlock = -1; 420 | int curDataBlock = NUM_SK_BLOCKS; 421 | 422 | int sa1TicketBlock = curDataBlock; 423 | 424 | // get next good block after SA1 ticket (which will be SA1 final block) 425 | curDataBlock = GetNextGoodBlock(curDataBlock); 426 | int sa1FinalDataBlock = curDataBlock; 427 | 428 | // loop through SA1 in reverse and write SA data block SAData fields 429 | int numDataBlocks = (int)(SKSA.SA1SigArea.ContentMetadata.ContentSize / BLOCK_SZ); 430 | for (int i = numDataBlocks - 1; i >= 0; i--) 431 | { 432 | lastDataBlock = curDataBlock; 433 | curDataBlock = GetNextGoodBlock(curDataBlock); 434 | if (i > 0) 435 | { 436 | fixedStream.Position = (curDataBlock * spareEntriesPerBlock) * 0x10; 437 | for (int page = 0; page < spareEntriesPerBlock; page++) 438 | { 439 | fixedStream.WriteByte((byte)lastDataBlock); 440 | fixedStream.WriteByte((byte)lastDataBlock); 441 | fixedStream.WriteByte((byte)lastDataBlock); 442 | fixedStream.Position += (0x10 - 3); 443 | } 444 | } 445 | } 446 | 447 | // write SA1 ticket block SAData 448 | fixedStream.Position = (sa1TicketBlock * spareEntriesPerBlock) * 0x10; 449 | for (int page = 0; page < spareEntriesPerBlock; page++) 450 | { 451 | fixedStream.WriteByte((byte)lastDataBlock); 452 | fixedStream.WriteByte((byte)lastDataBlock); 453 | fixedStream.WriteByte((byte)lastDataBlock); 454 | fixedStream.Position += (0x10 - 3); 455 | } 456 | 457 | // write SA1 final data block SAData (points to SA2 ticket block, or 0xFF) 458 | byte sa1FinalSAData = 0xFF; 459 | if (SKSA.SA2IsValid) 460 | sa1FinalSAData = (byte)curDataBlock; 461 | 462 | fixedStream.Position = (sa1FinalDataBlock * spareEntriesPerBlock) * 0x10; 463 | for (int page = 0; page < spareEntriesPerBlock; page++) 464 | { 465 | fixedStream.WriteByte((byte)sa1FinalSAData); 466 | fixedStream.WriteByte((byte)sa1FinalSAData); 467 | fixedStream.WriteByte((byte)sa1FinalSAData); 468 | fixedStream.Position += (0x10 - 3); 469 | } 470 | 471 | // if we have SA2, fix up the SAData for that too 472 | if(SKSA.SA2IsValid) 473 | { 474 | // write SA2 ticket area 475 | int sa2TicketBlock = curDataBlock; 476 | 477 | // get next good block after SA2 ticket (which will be SA2 final block) 478 | curDataBlock = GetNextGoodBlock(curDataBlock); 479 | int sa2FinalDataBlock = curDataBlock; 480 | 481 | // loop through SA2 in reverse and write SA2 data block SAData fields 482 | numDataBlocks = (int)(SKSA.SA2SigArea.ContentMetadata.ContentSize / BLOCK_SZ); 483 | for (int i = numDataBlocks - 1; i >= 0; i--) 484 | { 485 | lastDataBlock = curDataBlock; 486 | curDataBlock = GetNextGoodBlock(curDataBlock); 487 | if (i > 0) 488 | { 489 | fixedStream.Position = (curDataBlock * spareEntriesPerBlock) * 0x10; 490 | for (int page = 0; page < spareEntriesPerBlock; page++) 491 | { 492 | fixedStream.WriteByte((byte)lastDataBlock); 493 | fixedStream.WriteByte((byte)lastDataBlock); 494 | fixedStream.WriteByte((byte)lastDataBlock); 495 | fixedStream.Position += (0x10 - 3); 496 | } 497 | } 498 | } 499 | 500 | // write SA2 ticket block SAData (points to first SA2 data block) 501 | fixedStream.Position = (sa2TicketBlock * spareEntriesPerBlock) * 0x10; 502 | for (int page = 0; page < spareEntriesPerBlock; page++) 503 | { 504 | fixedStream.WriteByte((byte)lastDataBlock); 505 | fixedStream.WriteByte((byte)lastDataBlock); 506 | fixedStream.WriteByte((byte)lastDataBlock); 507 | fixedStream.Position += (0x10 - 3); 508 | } 509 | 510 | // write SA2 final data block SAData (0xFF) 511 | fixedStream.Position = (sa2FinalDataBlock * spareEntriesPerBlock) * 0x10; 512 | for (int page = 0; page < spareEntriesPerBlock; page++) 513 | { 514 | fixedStream.WriteByte((byte)0xFF); 515 | fixedStream.WriteByte((byte)0xFF); 516 | fixedStream.WriteByte((byte)0xFF); 517 | fixedStream.Position += (0x10 - 3); 518 | } 519 | } 520 | } 521 | 522 | // fix ECC bytes 523 | int spareNum = 0; 524 | for (int pageNum = pageSkip; pageNum < numPages; pageNum += (pageSkip + 1), spareNum++) 525 | { 526 | fixedStream.Position = spareNum * 0x10; 527 | fixedStream.Position += 6; 528 | fixedStream.WriteByte((byte)(blockSpare ? 0 : 0xFF)); // block-spare has 0x00 at 6th byte in spare 529 | 530 | fixedStream.Position += 1; // 0x8 into spare (ECC area) 531 | 532 | io.Stream.Position = (pageNum * 0x200); 533 | byte[] pageData = io.Reader.ReadBytes(0x200); 534 | byte[] ecc = iQueBlockSpare.Calculate512Ecc(pageData); 535 | fixedStream.Write(ecc, 0, 8); 536 | } 537 | 538 | // null out badblock spares based on badblock entries in the FAT 539 | if(MainFsAllocTable != null) 540 | { 541 | for(int blockNum = 0; blockNum < NUM_BLOCKS_IN_FAT; blockNum++) 542 | { 543 | if (MainFsAllocTable[blockNum] != FAT_BLOCK_BAD) 544 | continue; 545 | 546 | for (int blockPageNum = 0; blockPageNum < spareEntriesPerBlock; blockPageNum++) 547 | { 548 | fixedStream.Position = ((blockNum * spareEntriesPerBlock) + blockPageNum) * 0x10; 549 | for (int i = 0; i < 0x10; i++) 550 | fixedStream.WriteByte(0); 551 | } 552 | } 553 | } 554 | 555 | return fixedStream.ToArray(); 556 | } 557 | } 558 | 559 | public override string ToString() 560 | { 561 | return ToString(false); 562 | } 563 | 564 | public string ToString(bool formatted, bool fsInfoOnly = true, bool allFsInfo = false) 565 | { 566 | var b = new StringBuilder(); 567 | b.AppendLine("iQueNand:"); 568 | 569 | b.AppendLine($"MainFs (0x{(MainFsBlock * BLOCK_SZ):X} / block {MainFsBlock}):"); 570 | b.AppendLine($" SeqNo: {MainFs.SeqNo}"); 571 | b.AppendLine($" CheckSum: {MainFs.CheckSum}"); 572 | b.AppendLine($" NumFiles: {MainFsInodes.FindAll(s => s.Type == 1).Count}"); 573 | b.AppendLine(); 574 | for(int i = 0; i < MainFsInodes.Count; i++) 575 | { 576 | if (MainFsInodes[i].Type != 1) 577 | continue; 578 | b.AppendLine(" " + MainFsInodes[i].ToString(i)); 579 | } 580 | 581 | if(allFsInfo) 582 | { 583 | for(int i = 0; i < FsHeaders.Count; i++) 584 | { 585 | if (FsBlocks[i] == MainFsBlock) 586 | continue; 587 | 588 | b.AppendLine(); 589 | 590 | b.AppendLine($"Fs-{i} (0x{(FsBlocks[i] * BLOCK_SZ):X} / block {FsBlocks[i]})"); 591 | b.AppendLine($" SeqNo: {FsHeaders[i].SeqNo}"); 592 | b.AppendLine($" CheckSum: {FsHeaders[i].CheckSum}"); 593 | b.AppendLine($" NumFiles: {FsInodes[i].FindAll(s => s.Type == 1).Count}"); 594 | b.AppendLine(); 595 | for (int y = 0; y < FsInodes[i].Count; y++) 596 | { 597 | if (FsInodes[i][y].Type != 1) 598 | continue; 599 | b.AppendLine(" " + FsInodes[i][y].ToString(y)); 600 | } 601 | } 602 | } 603 | 604 | if (fsInfoOnly) 605 | return b.ToString(); 606 | 607 | b.AppendLine(); 608 | 609 | if (HasPrivateData) 610 | b.AppendLine(PrivateData.ToString(formatted, "iQuePrivateData (depot.sys)")); 611 | else 612 | b.AppendLine("Failed to read iQuePrivateData struct from depot.sys :("); 613 | 614 | if (SKSA != null) 615 | { 616 | b.AppendLine(SKSA.ToString(formatted)); 617 | 618 | b.AppendLine(); 619 | } 620 | 621 | if (Tickets != null) 622 | { 623 | b.AppendLine($"Num tickets: {Tickets.Count}"); 624 | for (int i = 0; i < Tickets.Count; i++) 625 | b.AppendLine(Tickets[i].ToString(formatted, $"iQueTitleData-{i}")); 626 | } 627 | else 628 | b.AppendLine("Failed to read iQueTitleData array from ticket.sys :("); 629 | 630 | if (CRL != null) 631 | b.AppendLine(CRL.ToString(formatted)); 632 | else 633 | b.AppendLine("Failed to read iQueCertificateRevocation array from crl.sys :("); 634 | 635 | if (Certs != null) 636 | b.AppendLine(Certs.ToString(formatted)); 637 | else 638 | b.AppendLine("Failed to read iQueCertificate array from cert.sys :("); 639 | 640 | return b.ToString(); 641 | } 642 | 643 | public void CreateModifiedInodes() 644 | { 645 | if (ModifiedInodes != null) 646 | return; 647 | 648 | ModifiedInodes = new List(); 649 | foreach(var node in MainFsInodes) 650 | { 651 | var newNode = new BbInode(); 652 | ModifiedInodes.Add(newNode.Copy(node)); 653 | } 654 | 655 | ModifiedAllocTable = new List(); 656 | foreach(var alloc in MainFsAllocTable) 657 | { 658 | ModifiedAllocTable.Add(alloc); 659 | } 660 | 661 | var newSeqNo = MainFs.SeqNo + 1; 662 | 663 | MainFs = new BbFat16(); 664 | MainFs.Magic = MAGIC_BBFS; 665 | MainFs.SeqNo = newSeqNo; 666 | MainFs.Link = 0; 667 | MainFs.CheckSum = 0; 668 | 669 | FsHeaders.Add(MainFs); 670 | FsInodes.Add(ModifiedInodes); 671 | FsAllocationTables.Add(ModifiedAllocTable); 672 | 673 | MainFsIndex = FsHeaders.Count - 1; 674 | } 675 | 676 | public byte[] GetChainData(short[] chain) 677 | { 678 | var data = new byte[chain.Length * 0x4000]; 679 | 680 | for (int i = 0; i < chain.Length; i++) 681 | { 682 | io.Stream.Position = chain[i] * BLOCK_SZ; 683 | 684 | int numRead = BLOCK_SZ; 685 | 686 | var blockData = io.Reader.ReadBytes(numRead); 687 | Array.Copy(blockData, 0, data, i * BLOCK_SZ, numRead); 688 | } 689 | 690 | return data; 691 | } 692 | 693 | public byte[] FileRead(BbInode inode) 694 | { 695 | var data = new byte[inode.Size]; 696 | var chain = GetBlockChain(inode.BlockIdx); 697 | 698 | for (int i = 0; i < chain.Length; i++) 699 | { 700 | io.Stream.Position = chain[i] * BLOCK_SZ; 701 | 702 | int numRead = BLOCK_SZ; 703 | if (i + 1 == chain.Length) 704 | numRead = (int)(inode.Size % BLOCK_SZ); 705 | if (numRead == 0) 706 | numRead = BLOCK_SZ; 707 | 708 | var blockData = io.Reader.ReadBytes(numRead); 709 | Array.Copy(blockData, 0, data, i * BLOCK_SZ, numRead); 710 | } 711 | 712 | return data; 713 | } 714 | 715 | public bool FileDelete(string fileName) 716 | { 717 | BbInode foundNode = new BbInode(); 718 | bool found = false; 719 | foreach(var node in MainFsInodes) 720 | { 721 | if(node.NameString == fileName) 722 | { 723 | foundNode = node; 724 | found = true; 725 | } 726 | } 727 | 728 | if (!found) 729 | return false; 730 | 731 | // make sure we have a modified inodes collection ready... 732 | CreateModifiedInodes(); 733 | 734 | // find node in modified inodes.. 735 | found = false; 736 | foreach (var node in ModifiedInodes) 737 | { 738 | if (node.NameString == fileName) 739 | { 740 | foundNode = node; 741 | found = true; 742 | } 743 | } 744 | if (!found) 745 | return false; 746 | 747 | // deallocate all blocks used by the file 748 | var chain = GetBlockChain(foundNode.BlockIdx); 749 | foreach(var block in chain) 750 | ModifiedAllocTable[block] = FAT_BLOCK_FREE; 751 | 752 | // remove file from inode collection 753 | ModifiedInodes.Remove(foundNode); 754 | 755 | return true; 756 | } 757 | 758 | public bool FileDelete(BbInode node) 759 | { 760 | var name = node.NameString; 761 | return FileDelete(name); 762 | } 763 | 764 | public short[] TryAllocateBlocks(int numBlocks) 765 | { 766 | var blockList = new List(); 767 | 768 | for(int i = 0; i < numBlocks; i++) 769 | { 770 | bool foundUnused = false; 771 | for(short block = NUM_SYS_AREA_BLOCKS; block < NUM_BLOCKS_IN_FAT; block++) 772 | { 773 | if(MainFsAllocTable[block] == FAT_BLOCK_FREE && !blockList.Contains(block)) 774 | { 775 | // found one! 776 | blockList.Add(block); 777 | foundUnused = true; 778 | break; 779 | } 780 | } 781 | 782 | if(!foundUnused) 783 | return null; // couldn't find enough unused blocks :( 784 | } 785 | 786 | // make sure we have a modified alloc table ready... 787 | CreateModifiedInodes(); 788 | 789 | for (int i = 0; i < blockList.Count; i++) 790 | { 791 | var blockNum = blockList[i]; 792 | var nextBlock = FAT_BLOCK_LAST; 793 | if (i < blockList.Count - 1) 794 | nextBlock = blockList[i + 1]; 795 | 796 | ModifiedAllocTable[blockNum] = nextBlock; 797 | } 798 | 799 | return blockList.ToArray(); 800 | } 801 | 802 | public bool FileWrite(string fileName, byte[] fileData, ref BbInode newNode) 803 | { 804 | newNode = new BbInode(); 805 | newNode.NameString = fileName; 806 | newNode.Type = 1; 807 | newNode.Size = (uint)fileData.Length; 808 | 809 | // remove any existing file with this name 810 | FileDelete(newNode.NameString); 811 | 812 | // make sure we have a modified inodes collection ready... 813 | CreateModifiedInodes(); 814 | 815 | int numBlocks = (fileData.Length + (BLOCK_SZ - 1)) / BLOCK_SZ; 816 | var blockList = TryAllocateBlocks(numBlocks); 817 | if (blockList == null) 818 | return false; // couldn't find enough unused blocks :( 819 | 820 | // start writing the file data! 821 | for(int i = 0; i < numBlocks; i++) 822 | { 823 | int dataOffset = i * BLOCK_SZ; 824 | int numWrite = BLOCK_SZ; 825 | if (dataOffset + numWrite > fileData.Length) 826 | numWrite = fileData.Length % BLOCK_SZ; 827 | 828 | SeekToBlock(blockList[i]); 829 | io.Stream.Write(fileData, dataOffset, numWrite); 830 | } 831 | newNode.BlockIdx = blockList[0]; 832 | 833 | // add new inode to inode list... 834 | // make seperate inode to provided one, to make sure nothing can change after adding it to inode list 835 | 836 | var realNewNode = new BbInode(); 837 | realNewNode.Copy(newNode); 838 | ModifiedInodes.Add(realNewNode); 839 | 840 | var chain = GetBlockChain(newNode.BlockIdx); 841 | 842 | // success! 843 | return true; 844 | } 845 | 846 | private bool ReadFilesystem() 847 | { 848 | FsHeaders = new List(); 849 | FsBlocks = new List(); 850 | FsBadBlocks = new List(); 851 | FsInodes = new List>(); 852 | FsAllocationTables = new List>(); 853 | 854 | int latestSeqNo = -1; 855 | for (int i = 0; i < NUM_FAT_BLOCKS; i++) 856 | { 857 | var blockNum = (NUM_BLOCKS_IN_FAT - 1) - i; // read FAT area from end to beginning 858 | SeekToBlock(blockNum); 859 | 860 | io.Stream.Position += FS_HEADER_ADDR; 861 | var header = io.Reader.ReadStruct(); 862 | header.EndianSwap(); 863 | if (header.Magic != MAGIC_BBFS) // todo: && header.Magic != MAGIC_BBFL, once we know what BBFL/"linked fats" actually are 864 | continue; 865 | 866 | if (!SkipVerifyFsChecksums && InodesOffset == 0) // only care about fs checksum if this is a proper dump and we aren't using hacky InodesOffset hack 867 | if (!VerifyFsChecksum(header, blockNum)) 868 | { 869 | FsBadBlocks.Add(blockNum); 870 | continue; // bad FS checksum :( 871 | } 872 | 873 | FsHeaders.Add(header); 874 | FsBlocks.Add(blockNum); 875 | 876 | if (header.SeqNo > latestSeqNo) 877 | { 878 | MainFs = header; 879 | MainFsBlock = blockNum; 880 | MainFsIndex = FsHeaders.Count - 1; 881 | latestSeqNo = header.SeqNo; 882 | } 883 | 884 | io.Stream.Position = blockNum * BLOCK_SZ; 885 | var allocTable = new List(); 886 | for (int y = 0; y < NUM_BLOCKS_IN_FAT; y++) 887 | allocTable.Add((short)(io.Reader.ReadUInt16().EndianSwap())); 888 | 889 | int numEntries = NUM_FS_ENTRIES; 890 | if (InodesOffset > 0) 891 | { 892 | io.Stream.Position += InodesOffset; // skip weird truncated inode if needed 893 | numEntries--; // and now we'll have to read one less entry 894 | } 895 | 896 | // now begin reading inodes 897 | var inodes = new List(); 898 | for (int y = 0; y < numEntries; y++) 899 | { 900 | var inode = io.Reader.ReadStruct(); 901 | inode.EndianSwap(); 902 | inodes.Add(inode); 903 | } 904 | var invalidInodes = new List(); 905 | for (int y = 0; y < inodes.Count; y++) 906 | if (!inodes[y].IsValid) 907 | invalidInodes.Add(y); 908 | 909 | for(int y = invalidInodes.Count - 1; y >= 0; y--) 910 | inodes.RemoveAt(invalidInodes[y]); 911 | 912 | FsAllocationTables.Add(allocTable); 913 | FsInodes.Add(inodes); 914 | } 915 | 916 | return latestSeqNo >= 0; 917 | } 918 | 919 | public bool WriteFilesystem() 920 | { 921 | if (ModifiedInodes == null) 922 | return true; // nothing to write... 923 | 924 | int destBlock = -1; 925 | int lowestSeqNo = -1; 926 | int lowestSeqNoBlock = -1; 927 | for (int i = 0; i < NUM_FAT_BLOCKS; i++) 928 | { 929 | var blockNum = (NUM_BLOCKS_IN_FAT - 1) - i; // read FAT area from end to beginning 930 | 931 | // check bad block... 932 | if (MainFsAllocTable[blockNum] == FAT_BLOCK_BAD) 933 | continue; 934 | 935 | SeekToBlock(blockNum); 936 | 937 | io.Stream.Position += FS_HEADER_ADDR; 938 | var header = io.Reader.ReadStruct(); 939 | header.EndianSwap(); 940 | if (header.Magic != MAGIC_BBFS) // todo: && header.Magic != MAGIC_BBFL, once we know what BBFL/"linked fats" actually are 941 | { 942 | // no BBFS header here - perfect! 943 | destBlock = blockNum; 944 | break; 945 | } 946 | if(header.SeqNo == MainFs.SeqNo) // if this blocks seqNo is the same as our modded seqNo it's probably from this instance, so lets just overwrite that 947 | { 948 | destBlock = blockNum; 949 | break; 950 | } 951 | if(lowestSeqNo == -1 || lowestSeqNo > header.SeqNo) 952 | { 953 | lowestSeqNo = header.SeqNo; 954 | lowestSeqNoBlock = blockNum; 955 | } 956 | } 957 | 958 | if (destBlock == -1) // all FS blocks are used, lets overwrite the oldest one 959 | destBlock = lowestSeqNoBlock; 960 | 961 | if (destBlock == -1) 962 | return false; // wtf? 963 | 964 | // null out block 965 | SeekToBlock(destBlock); 966 | io.Writer.Write(new byte[BLOCK_SZ]); 967 | 968 | SeekToBlock(destBlock); 969 | 970 | // write alloc table... 971 | for (int i = 0; i < NUM_BLOCKS_IN_FAT; i++) 972 | io.Writer.Write(ModifiedAllocTable[i].EndianSwap()); 973 | 974 | // write inodes... 975 | foreach(var inode in ModifiedInodes) 976 | { 977 | inode.EndianSwap(); 978 | io.Writer.WriteStruct(inode); 979 | inode.EndianSwap(); 980 | } 981 | SeekToBlock(destBlock); 982 | io.Stream.Position += FS_HEADER_ADDR; 983 | 984 | // write FS header... 985 | MainFs.EndianSwap(); 986 | io.Writer.WriteStruct(MainFs); 987 | MainFs.EndianSwap(); 988 | 989 | // add FS as FS block... 990 | if (!FsBlocks.Contains(destBlock)) 991 | FsBlocks.Add(destBlock); 992 | 993 | // fix FS checksum... 994 | RepairFsChecksum(destBlock); 995 | 996 | // complete! 997 | return true; 998 | } 999 | } 1000 | } 1001 | -------------------------------------------------------------------------------- /iQueTool/Natives.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Security.Cryptography; 4 | 5 | namespace iQueTool 6 | { 7 | class Natives 8 | { 9 | [StructLayout(LayoutKind.Sequential)] 10 | // ReSharper disable once InconsistentNaming 11 | public struct BCRYPT_PSS_PADDING_INFO 12 | { 13 | public BCRYPT_PSS_PADDING_INFO(string pszAlgId, int cbSalt) 14 | { 15 | this.pszAlgId = pszAlgId; 16 | this.cbSalt = cbSalt; 17 | } 18 | 19 | [MarshalAs(UnmanagedType.LPWStr)] 20 | public string pszAlgId; 21 | public int cbSalt; 22 | } 23 | 24 | [DllImport("ncrypt.dll", SetLastError = false)] 25 | public static extern uint NCryptOpenStorageProvider(out IntPtr phProvider, 26 | [MarshalAs(UnmanagedType.LPWStr)] string pszProviderName, 27 | uint dwFlags); 28 | 29 | [DllImport("ncrypt.dll", SetLastError = false)] 30 | public static extern uint NCryptImportKey(IntPtr hProvider, 31 | IntPtr hImportKey, 32 | [MarshalAs(UnmanagedType.LPWStr)] string pszBlobType, 33 | IntPtr pParameterList, 34 | out IntPtr phKey, 35 | [MarshalAs(UnmanagedType.LPArray)] 36 | byte[] pbData, 37 | uint cbData, 38 | uint dwFlags); 39 | 40 | [DllImport("ncrypt.dll", SetLastError = false)] 41 | public static extern uint NCryptVerifySignature(IntPtr hKey, 42 | [In] ref BCRYPT_PSS_PADDING_INFO pPaddingInfo, 43 | [In, MarshalAs(UnmanagedType.LPArray)] byte[] pbHashValue, 44 | int cbHashValue, 45 | [In, MarshalAs(UnmanagedType.LPArray)] byte[] pbSignature, 46 | int cbSignature, 47 | uint dwFlags); 48 | 49 | [DllImport("ncrypt.dll", SetLastError = false)] 50 | public static extern uint NCryptSignHash(IntPtr hKey, 51 | [In] ref BCRYPT_PSS_PADDING_INFO pPaddingInfo, 52 | [MarshalAs(UnmanagedType.LPArray)] 53 | byte[] pbHashValue, 54 | int cbHashValue, 55 | [MarshalAs(UnmanagedType.LPArray)] 56 | byte[] pbSignature, 57 | int cbSignature, 58 | [Out] out uint pcbResult, 59 | int dwFlags); 60 | 61 | [DllImport("ncrypt.dll", SetLastError = false)] 62 | public static extern uint NCryptSignHash(IntPtr hKey, 63 | [In] ref BCRYPT_PSS_PADDING_INFO pPaddingInfo, 64 | [In, MarshalAs(UnmanagedType.LPArray)] byte[] pbHashValue, 65 | int cbHashValue, 66 | IntPtr pbSignature, 67 | int cbSignature, 68 | [Out] out uint pcbResult, 69 | uint dwFlags); 70 | 71 | [DllImport("ncrypt.dll", SetLastError = false)] 72 | public static extern uint NCryptFreeObject(IntPtr hObject); 73 | 74 | internal static RSAParameters GenerateRSAParametersFromPublicKey(byte[] exponent, byte[] modulus) 75 | { 76 | return new RSAParameters 77 | { 78 | Exponent = exponent, 79 | Modulus = Swap8(modulus) 80 | }; 81 | } 82 | internal static RSAParameters GenerateRSAParametersFromPublicKey(byte[] publicKey) 83 | { 84 | //return GenerateRSAParametersFromPublicKey(publicKey, false); 85 | return new RSAParameters 86 | { 87 | Exponent = Swap(GetData(publicKey, 0x00, 0x04)), 88 | Modulus = Swap8(GetData(publicKey, 0x4, 0x200)) 89 | }; 90 | } 91 | 92 | internal static byte[] GetData(byte[] data, int pos, int length) 93 | { 94 | // Create a new output buffer 95 | byte[] outBuffer = new byte[length]; 96 | 97 | // Loop and copy the data 98 | for (int x = pos, y = 0; x < pos + length; x++, y++) 99 | outBuffer[y] = data[x]; 100 | 101 | // Return our data 102 | return outBuffer; 103 | } 104 | internal static byte[] Swap8(byte[] input) 105 | { 106 | byte[] buffer = new byte[input.Length]; 107 | Array.Copy(input, buffer, input.Length); 108 | for (int x = 0; x < input.Length; x += 8) 109 | Array.Reverse(buffer, x, 8); 110 | 111 | return Swap(buffer); 112 | } 113 | internal static byte[] Swap(byte[] input) 114 | { 115 | byte[] buffer = new byte[input.Length]; 116 | Array.Copy(input, buffer, input.Length); 117 | Array.Reverse(buffer); 118 | return buffer; 119 | } 120 | 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /iQueTool/Program.cs: -------------------------------------------------------------------------------- 1 | using NDesk.Options; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using iQueTool.Structs; 6 | using iQueTool.Files; 7 | 8 | namespace iQueTool 9 | { 10 | class Program 11 | { 12 | // parameters 13 | static bool printHelp = false; 14 | static bool printInfo = false; 15 | static bool writeInfo = false; 16 | 17 | static string outputFile = String.Empty; 18 | 19 | static bool extractAll = false; 20 | static string extractContentIds = String.Empty; 21 | static string extractTIDs = String.Empty; 22 | static string extractIDs = String.Empty; 23 | 24 | static string addFiles = String.Empty; 25 | static string addFilesList = String.Empty; 26 | 27 | static string deleteFiles = String.Empty; 28 | 29 | static bool extractKernel = false; 30 | 31 | static bool writeNewTicketFile = false; 32 | 33 | static bool skipVerifyChecksums = false; 34 | static bool fixFsChecksums = false; 35 | static bool isBadDump = false; 36 | 37 | static bool showAllFsInfo = false; 38 | 39 | static string updateKernelPath = String.Empty; 40 | 41 | static string generateSparePath = String.Empty; 42 | static bool generateFullSpare = false; 43 | 44 | static string filePath = String.Empty; 45 | static string extraPath = String.Empty; 46 | 47 | static void Main(string[] args) 48 | { 49 | if (File.Exists("cert.sys")) 50 | iQueCertCollection.MainCollection = new iQueCertCollection(File.ReadAllBytes("cert.sys")); 51 | else if (File.Exists(@"D:\cert.sys")) // try reading from root of a drive, so we don't have to copy cert.sys with us everywhere 52 | iQueCertCollection.MainCollection = new iQueCertCollection(File.ReadAllBytes(@"D:\cert.sys")); 53 | 54 | if (iQueCertCollection.MainCollection != null) 55 | { 56 | if (File.Exists("ique_root.bin")) 57 | iQueCertCollection.MainCollection.Add(new BbRsaCert("", "Root", File.ReadAllBytes("ique_root.bin"))); 58 | else if (File.Exists(@"D:\ique_root.bin")) 59 | iQueCertCollection.MainCollection.Add(new BbRsaCert("", "Root", File.ReadAllBytes(@"D:\ique_root.bin"))); 60 | } 61 | 62 | const string fmt = " "; 63 | 64 | var p = new OptionSet { 65 | { "h|?|help", v => printHelp = v != null }, 66 | { "i|info", v => printInfo = v != null }, 67 | { "wi|writeinfo", v => writeInfo = v != null }, 68 | { "o|output=", v => outputFile = v }, 69 | 70 | { "x|extract", v => extractAll = v != null }, 71 | { "xc|extractcids=", v => extractContentIds = v }, 72 | { "xt|extracttids=", v => extractTIDs = v }, 73 | { "xi|extractids=", v => extractIDs = v }, 74 | 75 | { "a|addfile|addfiles=", v => addFiles = v }, 76 | { "al|addlist=", v => addFilesList = v }, 77 | 78 | { "d|deletefile|deletefiles=", v => deleteFiles = v }, 79 | 80 | { "xk|extractkernel", v => extractKernel = v != null }, 81 | { "fs|showallfs", v => showAllFsInfo = v != null }, 82 | 83 | { "uk|updatekernel=", v => updateKernelPath = v }, 84 | 85 | { "gs|genspare=", v => generateSparePath = v }, 86 | { "gp|fullspare", v => generateFullSpare = v != null }, 87 | 88 | { "sc|skipchecksums", v => skipVerifyChecksums = v != null }, 89 | { "fc|fixchecksums", v => fixFsChecksums = v != null }, 90 | { "bd|baddump", v => isBadDump = v != null }, 91 | 92 | { "n|newfile", v => writeNewTicketFile = v != null } 93 | }; 94 | 95 | var extraArgs = p.Parse(args); 96 | 97 | Console.WriteLine("iQueTool 0.4a: iQue Player file manipulator"); 98 | 99 | if (printHelp || extraArgs.Count <= 1) 100 | { 101 | Console.WriteLine("Usage : iquetool.exe [mode] [parameters] [filepath]"); 102 | Console.WriteLine(); 103 | Console.WriteLine("Valid modes: nand / tickets / certs / crl / sparefix"); // / privdata / kernel"); 104 | Console.WriteLine(); 105 | Console.WriteLine("General parameters:"); 106 | Console.WriteLine(fmt + "-h (-help) - print iquetool usage"); 107 | Console.WriteLine(fmt + "-i (-info) - print basic info about file"); 108 | Console.WriteLine(fmt + "-wi (-writeinfo) - write detailed info about file to [filepath].txt"); 109 | Console.WriteLine(fmt + "-o (-output) - specify output filename/directory"); 110 | 111 | Console.WriteLine(); 112 | Console.WriteLine("Mode \"tickets\" / \"certs\" / \"crl\":"); 113 | Console.WriteLine(); 114 | Console.WriteLine(fmt + "-x - extracts all entries from file"); 115 | Console.WriteLine(fmt + "-xi (-extractids) - extract entries with these indexes"); 116 | Console.WriteLine(fmt + "-xc (-extractcids) - extract entries with these content ids"); 117 | Console.WriteLine(fmt + "-xt (-extracttids) - extract entries with these ticket ids"); 118 | Console.WriteLine(); 119 | Console.WriteLine(fmt + "-n - writes extracted entries into a single array file"); 120 | 121 | Console.WriteLine(); 122 | Console.WriteLine("Note that by default the extract commands above will extract tickets as seperate files"); 123 | Console.WriteLine("with the format \\ticket---.dat"); 124 | 125 | Console.WriteLine(); 126 | Console.WriteLine("Mode \"nand\":"); 127 | Console.WriteLine(); 128 | Console.WriteLine(fmt + "-x - extracts all files from NAND"); 129 | //Console.WriteLine(fmt + "-xi (-extractids) - extract inodes with these indexes"); 130 | Console.WriteLine(fmt + "-xk (-extractkernel) - extract secure-kernel from NAND"); 131 | Console.WriteLine(fmt + "-fs (-showallfs) - shows info about all found FS blocks"); 132 | Console.WriteLine(); 133 | Console.WriteLine(fmt + "-a - adds files to NAND"); 134 | Console.WriteLine(fmt + "-al (-addlist) - adds line-seperated list of files to NAND"); 135 | Console.WriteLine(); 136 | Console.WriteLine(fmt + "-d - delete files from NAND FS"); 137 | Console.WriteLine(); 138 | Console.WriteLine(fmt + "-uk (-updatekernel) - updates NAND with the given (cache) SKSA"); 139 | Console.WriteLine(fmt + fmt + "also takes bad-blocks into account and will work around them"); 140 | Console.WriteLine(fmt + fmt + "use -gs afterwards to generate a new spare with proper SAData fields"); 141 | Console.WriteLine(); 142 | Console.WriteLine(fmt + "-gs (-genspare) - generates block-spare/ECC data for this NAND"); 143 | Console.WriteLine(fmt + "-gp (-fullspare) - will generate page-spare/ECC data (0x20 pages per block) instead"); 144 | Console.WriteLine(); 145 | Console.WriteLine(fmt + "-sc (-skipchecksums) - skip verifying FS checksums"); 146 | Console.WriteLine(fmt + "-fc (-fixchecksums) - skips verifying & repairs all FS checksums"); 147 | Console.WriteLine(fmt + "-bd (-baddump) - will try reading inodes with a 0x10 byte offset"); 148 | Console.WriteLine(); 149 | Console.WriteLine("Mode \"sparefix\":"); 150 | Console.WriteLine(fmt + "usage: sparefix [spare.bin path] "); 151 | Console.WriteLine(fmt + "fixes overdump / raw page-spare dumps to match BB block-spare dumps"); 152 | Console.WriteLine(fmt + "if nand.bin path is specified, will correct the spare data using that nand"); 153 | Console.WriteLine(); 154 | Console.WriteLine(fmt + "-o (-output) - specify output filename (default: [input]_fixed)"); 155 | Console.WriteLine(fmt + "-gp (-fullspare) - disables reducing page-spare data to block-spare"); 156 | Console.WriteLine(); 157 | 158 | Console.WriteLine("iQue signature verification: " + (iQueCertCollection.MainCollection != null ? "enabled" : "disabled")); 159 | if (iQueCertCollection.MainCollection == null) 160 | { 161 | Console.WriteLine(fmt + "To enable, drop a cert.sys file (taken from an iQue NAND) next to the iQueTool exe"); 162 | Console.WriteLine(fmt + "Alternatively you can put it at the root of your D: drive"); 163 | Console.WriteLine(fmt + "Also when opening a NAND image the cert.sys will automatically be loaded from it, if not already found locally"); 164 | } 165 | Console.WriteLine(); 166 | return; 167 | } 168 | Console.WriteLine(); 169 | 170 | string mode = extraArgs[0].ToLower(); 171 | filePath = extraArgs[1]; 172 | if (extraArgs.Count > 2) 173 | extraPath = extraArgs[2]; 174 | 175 | if (mode == "tickets") 176 | ModeArrayFile(); 177 | else if (mode == "certs") 178 | ModeArrayFile(); 179 | else if (mode == "crl") 180 | ModeArrayFile(); 181 | else if (mode == "nand") 182 | ModeNAND(); 183 | else if (mode == "sparefix") 184 | ModeSpareFix(); 185 | else 186 | { 187 | Console.WriteLine($"Invalid mode \"{mode}\"."); 188 | Console.WriteLine("Valid modes are: nand / tickets / certs / crl / sparefix"); // / privdata / kernel"); 189 | return; 190 | } 191 | } 192 | 193 | static void ModeSpareFix() 194 | { 195 | if (string.IsNullOrEmpty(outputFile)) 196 | outputFile = filePath + "_fixed"; 197 | Console.WriteLine($"Reading spare from {filePath}..."); 198 | 199 | using (var fixedStream = new MemoryStream()) 200 | { 201 | using (var reader = new BinaryReader(File.OpenRead(filePath))) 202 | { 203 | bool isBlockSpare = true; 204 | if (reader.BaseStream.Length == 64 * 1024) 205 | { 206 | // must be a block-spare already, just copy it all to the fixed stream so we can fix up the ECC 207 | byte[] spare = reader.ReadBytes(64 * 1024); 208 | fixedStream.Write(spare, 0, 64 * 1024); 209 | } 210 | else 211 | { 212 | int numSkip = 0; 213 | if (reader.BaseStream.Length == 1024 * 1024) // overdump, just skip 0xF0 after each read 214 | numSkip = 0xF0; 215 | else if (reader.BaseStream.Length == 1024 * 1024 * 2) // page-spare dump, we only want the spare of the last page in each block 216 | { 217 | numSkip = 0x1F0; 218 | reader.BaseStream.Position = 0x1F0; 219 | } 220 | else 221 | { 222 | Console.WriteLine("Spare isn't overdump/page-spare/block-spare dump?"); 223 | return; 224 | } 225 | 226 | if (generateFullSpare) 227 | { 228 | if (numSkip != 0x1F0) 229 | { 230 | Console.WriteLine("Invalid spare used with -gp (-fullspare)"); 231 | Console.WriteLine("This switch only works with page/block-spares, not overdumps!"); 232 | return; 233 | } 234 | 235 | // user gave -gp switch, so just copy the whole page-spare instead of reducing to block-spare 236 | reader.BaseStream.Position = 0; 237 | byte[] spare = reader.ReadBytes(1024 * 1024 * 2); 238 | fixedStream.Write(spare, 0, 1024 * 1024 * 2); 239 | 240 | isBlockSpare = false; 241 | } 242 | else 243 | while (reader.BaseStream.Position < reader.BaseStream.Length) 244 | { 245 | byte[] spare = reader.ReadBytes(0x10); 246 | spare[6] = 0; // fix page-spare dump to match format of block-spare dump 247 | fixedStream.Write(spare, 0, 0x10); 248 | if (reader.BaseStream.Position < reader.BaseStream.Length) // with raw page-spare dumps we're at the end here, so check for that 249 | reader.BaseStream.Position += numSkip; 250 | } 251 | } 252 | 253 | // if nand.bin is specified, read from it and correct the spare data 254 | if(!string.IsNullOrEmpty(extraPath)) 255 | { 256 | var nand = new iQueNand(extraPath); 257 | nand.Read(); 258 | var fixedSpare = nand.GenerateSpareData(isBlockSpare, fixedStream.ToArray()); // todo: make a stream-based GenerateSpareData method? 259 | fixedStream.Position = 0; 260 | fixedStream.Write(fixedSpare, 0, fixedSpare.Length); 261 | } 262 | } 263 | File.WriteAllBytes(outputFile, fixedStream.ToArray()); 264 | } 265 | Console.WriteLine($"Saved fixed spare to {outputFile}!"); 266 | } 267 | 268 | static void ModeNAND() 269 | { 270 | Console.WriteLine($"Opening NAND image from {filePath}..."); 271 | Console.WriteLine(); 272 | 273 | if(!string.IsNullOrEmpty(outputFile) && !string.IsNullOrEmpty(updateKernelPath)) 274 | { 275 | // we're updating kernel with output file specified, so copy the input file to specified output file and work with that instead 276 | if (File.Exists(outputFile)) 277 | File.Delete(outputFile); 278 | 279 | File.Copy(filePath, outputFile); 280 | filePath = outputFile; 281 | } 282 | 283 | var nandFile = new iQueNand(filePath) { 284 | SkipVerifyFsChecksums = skipVerifyChecksums || fixFsChecksums, 285 | InodesOffset = isBadDump ? 0x10 : 0 286 | }; 287 | 288 | if (!nandFile.Read()) 289 | { 290 | Console.WriteLine($"[!] Failed to read NAND image!"); 291 | return; 292 | } 293 | 294 | if(!string.IsNullOrEmpty(addFiles) || !string.IsNullOrEmpty(addFilesList)) 295 | { 296 | var filesToAdd = new List(); 297 | if(!string.IsNullOrEmpty(addFiles)) 298 | { 299 | var files = addFiles.Split(','); 300 | filesToAdd.AddRange(files); 301 | } 302 | 303 | if(!string.IsNullOrEmpty(addFilesList)) 304 | { 305 | if(File.Exists(addFilesList)) 306 | { 307 | var files = File.ReadAllLines(addFilesList); 308 | filesToAdd.AddRange(files); 309 | } 310 | else 311 | Console.WriteLine($"[!] -al (addlist) failed, list {addFilesList} doesn't exist!"); 312 | } 313 | 314 | if(filesToAdd.Count > 0) 315 | { 316 | int successful = 0; 317 | foreach(var file in filesToAdd) 318 | { 319 | if(File.Exists(file)) 320 | { 321 | Console.WriteLine($"Adding file to NAND: {file}"); 322 | BbInode node = new BbInode(); 323 | if (nandFile.FileWrite(Path.GetFileName(file), File.ReadAllBytes(file), ref node)) 324 | { 325 | Console.WriteLine($"File added successfully! name: {node.NameString}, block: {node.BlockIdx}"); 326 | successful++; 327 | } 328 | else 329 | Console.WriteLine($"[!] Failed to add file, likely not enough free space in nand!"); 330 | } 331 | else 332 | Console.WriteLine($"[!] Failed to add non-existing file:{Environment.NewLine}\t{file}"); 333 | } 334 | if(successful > 0) 335 | { 336 | Console.WriteLine("Writing updated FS to NAND..."); 337 | if (nandFile.WriteFilesystem()) 338 | Console.WriteLine($"Added {successful} files successfully!"); 339 | else 340 | Console.WriteLine("[!] Failed to write updated FS, couldn't find FS block to write?"); 341 | } 342 | } 343 | else 344 | { 345 | Console.WriteLine("[!] -al (addlist) failed, no files to add!"); 346 | } 347 | } 348 | 349 | if(!string.IsNullOrEmpty(deleteFiles)) 350 | { 351 | var filesToDelete = deleteFiles.Split(','); 352 | int successful = 0; 353 | foreach(var file in filesToDelete) 354 | { 355 | // lazy way to get proper 8.3 name for this file.. 356 | BbInode inode = new BbInode(); 357 | inode.NameString = Path.GetFileName(file); 358 | 359 | if (nandFile.FileDelete(inode.NameString)) 360 | { 361 | Console.WriteLine($"File {inode.NameString} removed from NAND!"); 362 | successful++; 363 | } 364 | else 365 | Console.WriteLine($"Failed to remove file {inode.NameString} from NAND..."); 366 | } 367 | 368 | if(successful > 0) 369 | { 370 | Console.WriteLine("Writing updated FS to NAND..."); 371 | if (nandFile.WriteFilesystem()) 372 | Console.WriteLine($"Removed {successful} files successfully!"); 373 | else 374 | Console.WriteLine("[!] Failed to write updated FS, couldn't find FS block to write?"); 375 | } 376 | } 377 | 378 | if (fixFsChecksums) 379 | nandFile.RepairFsChecksums(); 380 | 381 | if(printInfo) 382 | Console.WriteLine(nandFile.ToString(true, true, showAllFsInfo)); 383 | 384 | if(writeInfo) 385 | { 386 | File.WriteAllText(filePath + ".txt", nandFile.ToString(true, false, showAllFsInfo)); 387 | Console.WriteLine($"Wrote detailed NAND info to {filePath}.txt"); 388 | } 389 | 390 | if(!string.IsNullOrEmpty(updateKernelPath)) 391 | { 392 | Console.WriteLine($"Updating NAND SKSA to {updateKernelPath}"); 393 | nandFile.SetSKSAData(updateKernelPath); 394 | } 395 | else 396 | { 397 | // extract / extractkernel can only be used if we aren't updating kernel in the same session 398 | if (extractAll) 399 | { 400 | if (string.IsNullOrEmpty(outputFile)) 401 | { 402 | outputFile = filePath + "_ext"; 403 | Console.WriteLine("[!] No output path (-o) given, set path to:"); 404 | Console.WriteLine($"- {outputFile}"); 405 | Console.WriteLine(); 406 | } 407 | 408 | if (!Directory.Exists(outputFile)) 409 | Directory.CreateDirectory(outputFile); 410 | 411 | int count = 0; 412 | foreach (var file in nandFile.MainFsInodes) 413 | { 414 | if (!file.IsValid) 415 | continue; 416 | 417 | var extPath = Path.Combine(outputFile, file.NameString); 418 | Console.WriteLine($"Writing file to {extPath}"); 419 | File.WriteAllBytes(extPath, nandFile.FileRead(file)); 420 | count++; 421 | } 422 | 423 | Console.WriteLine($"Extracted {count} files to {outputFile}"); 424 | } 425 | 426 | if (extractKernel) 427 | { 428 | if (string.IsNullOrEmpty(outputFile)) 429 | { 430 | outputFile = filePath + ".sksa.bin"; 431 | Console.WriteLine("[!] No output path (-o) given, set path to:"); 432 | Console.WriteLine($"- {outputFile}"); 433 | Console.WriteLine(); 434 | } 435 | if (extractAll) // if we just extracted the fs files we'll extract SKSA into the same folder 436 | outputFile = Path.Combine(outputFile, "sksa.bin"); 437 | 438 | File.WriteAllBytes(outputFile, nandFile.GetSKSAData()); 439 | 440 | Console.WriteLine($"Extracted SKSA to {outputFile}"); 441 | } 442 | } 443 | 444 | if(!string.IsNullOrEmpty(generateSparePath)) 445 | { 446 | File.WriteAllBytes(generateSparePath, nandFile.GenerateSpareData(!generateFullSpare)); 447 | Console.WriteLine($"Wrote generated {(generateFullSpare ? "page" : "block")}-spare data to {generateSparePath}"); 448 | } 449 | } 450 | 451 | // warning: stinky code 452 | static void ModeArrayFile() 453 | { 454 | var type = typeof(T); 455 | 456 | Console.WriteLine($"Opening {type.Name} file {filePath}..."); 457 | Console.WriteLine(); 458 | 459 | var arrayFile = new iQueArrayFile(filePath); 460 | 461 | for(int i = 0; i < arrayFile.Count; i++) 462 | { 463 | if (type == typeof(OSBbSaGameMetaData)) 464 | arrayFile[i] = (T)(object)((OSBbSaGameMetaData)(object)arrayFile[i]).EndianSwap(); 465 | else if (type == typeof(BbRsaCert)) 466 | arrayFile[i] = (T)(object)((BbRsaCert)(object)arrayFile[i]).EndianSwap(); 467 | else if (type == typeof(BbCrlHead)) 468 | arrayFile[i] = (T)(object)((BbCrlHead)(object)arrayFile[i]).EndianSwap(); 469 | } 470 | 471 | var info = arrayFile.ToString(true); 472 | 473 | if (printInfo) 474 | Console.WriteLine(info); 475 | 476 | if (writeInfo) 477 | { 478 | File.WriteAllText(filePath + ".txt", info); 479 | Console.WriteLine($"Wrote {type.Name} info to {filePath}.txt"); 480 | } 481 | 482 | if((!String.IsNullOrEmpty(extractContentIds) || !String.IsNullOrEmpty(extractTIDs)) && type != typeof(OSBbSaGameMetaData)) 483 | { 484 | Console.WriteLine("Warning: using -xc or -xt in wrong mode"); 485 | Console.WriteLine("(those params are only valid for the \"ticket\" mode)"); 486 | extractContentIds = String.Empty; 487 | extractTIDs = String.Empty; 488 | } 489 | 490 | if (extractAll || !String.IsNullOrEmpty(extractIDs) || !String.IsNullOrEmpty(extractContentIds) || !String.IsNullOrEmpty(extractTIDs)) 491 | { 492 | if (String.IsNullOrEmpty(outputFile)) 493 | { 494 | outputFile = filePath + "_ext"; 495 | if (writeNewTicketFile) 496 | outputFile += ".dat"; 497 | Console.WriteLine("[!] No output path (-o) given, set path to:"); 498 | Console.WriteLine($"- {outputFile}"); 499 | Console.WriteLine(); 500 | } 501 | 502 | var extractEntries = new List(); 503 | if (extractAll) 504 | extractEntries = arrayFile; 505 | else 506 | { 507 | if (!String.IsNullOrEmpty(extractContentIds) && type == typeof(OSBbSaGameMetaData)) 508 | { 509 | var ids = extractContentIds.Split(','); 510 | foreach (var id in ids) 511 | { 512 | uint intID = 0xFFFFFFFF; 513 | if (!uint.TryParse(id, out intID)) 514 | { 515 | Console.WriteLine($"extractContentIds: couldn't parse content ID {id}!"); 516 | continue; 517 | } 518 | 519 | var tickets = arrayFile.FindAll(t => ((OSBbSaGameMetaData)(object)t).ContentMetadata.ContentId == intID); 520 | if (tickets.Count <= 0) 521 | { 522 | Console.WriteLine($"extractContentIds: failed to find ticket with content ID {id}!"); 523 | continue; 524 | } 525 | 526 | if (!extractEntries.Contains(tickets[0])) 527 | extractEntries.Add(tickets[0]); 528 | } 529 | } 530 | 531 | if (!String.IsNullOrEmpty(extractTIDs) && type == typeof(OSBbSaGameMetaData)) 532 | { 533 | var ids = extractTIDs.Split(','); 534 | foreach (var id in ids) 535 | { 536 | ushort intID = 0xFFFF; 537 | if (!ushort.TryParse(id, out intID)) 538 | { 539 | Console.WriteLine($"extractTIDs: couldn't parse TID {id}!"); 540 | continue; 541 | } 542 | 543 | var tickets = arrayFile.FindAll(t => ((OSBbSaGameMetaData)(object)t).Ticket.TicketId == intID); 544 | if (tickets.Count <= 0) 545 | { 546 | Console.WriteLine($"extractTIDs: failed to find ticket with TID {id}!"); 547 | continue; 548 | } 549 | 550 | if (!extractEntries.Contains(tickets[0])) 551 | extractEntries.Add(tickets[0]); 552 | } 553 | } 554 | 555 | if (!String.IsNullOrEmpty(extractIDs)) 556 | { 557 | var ids = extractIDs.Split(','); 558 | foreach (var id in ids) 559 | { 560 | int intID = -1; 561 | if (!int.TryParse(id, out intID)) 562 | { 563 | Console.WriteLine($"extractIDs: couldn't parse ID {id}!"); 564 | continue; 565 | } 566 | 567 | if (intID >= arrayFile.Count || intID < 0) 568 | { 569 | Console.WriteLine($"extractIDs: couldn't find ID {id} as its out of bounds!"); 570 | continue; 571 | } 572 | 573 | if (!extractEntries.Contains(arrayFile[intID])) 574 | extractEntries.Add(arrayFile[intID]); 575 | } 576 | } 577 | } 578 | 579 | if (extractEntries.Count <= 0) 580 | { 581 | Console.WriteLine("Extract failed: none of the specified entries were found?"); 582 | return; 583 | } 584 | 585 | Console.WriteLine($"Extracting entries to {outputFile}"); 586 | if (!writeNewTicketFile) 587 | { 588 | Console.WriteLine($"^ as seperate raw files!"); 589 | if (!Directory.Exists(outputFile)) 590 | Directory.CreateDirectory(outputFile); 591 | 592 | for(int i = 0; i < extractEntries.Count; i++) 593 | { 594 | var entry = extractEntries[i]; 595 | var entryType = "unk"; 596 | var name = i.ToString(); 597 | byte[] data = null; 598 | if (type == typeof(OSBbSaGameMetaData)) 599 | { 600 | entryType = "ticket"; 601 | name = ((OSBbSaGameMetaData)(object)entry).TicketUID; 602 | data = ((OSBbSaGameMetaData)(object)entry).GetBytes(); 603 | } 604 | else if (type == typeof(BbRsaCert)) 605 | { 606 | entryType = "cert"; 607 | name = ((BbRsaCert)(object)entry).CertNameString; 608 | data = ((BbRsaCert)(object)entry).GetBytes(); 609 | } 610 | else if (type == typeof(BbCrlHead)) 611 | { 612 | entryType = "crl"; 613 | name = ((BbCrlHead)(object)entry).CertNameString; 614 | data = ((BbCrlHead)(object)entry).GetBytes(); 615 | } 616 | 617 | var outputPath = $"{entryType}-{name}.dat"; 618 | outputPath = Path.Combine(outputFile, outputPath); 619 | if (File.Exists(outputPath)) 620 | File.Delete(outputPath); 621 | 622 | Console.WriteLine($"Writing entry to {outputPath}"); 623 | File.WriteAllBytes(outputPath, data); 624 | } 625 | } 626 | else 627 | { 628 | Console.WriteLine($"^ into a single new array file!"); 629 | using (var outputIO = new IO(outputFile, FileMode.Create)) 630 | { 631 | outputIO.Writer.Write(((uint)extractEntries.Count).EndianSwap()); 632 | for (int i = 0; i < extractEntries.Count; i++) 633 | { 634 | var entry = extractEntries[i]; 635 | var name = i.ToString(); 636 | byte[] data = null; 637 | if (type == typeof(OSBbSaGameMetaData)) 638 | { 639 | name = ((OSBbSaGameMetaData)(object)entry).TicketUID; 640 | data = ((OSBbSaGameMetaData)(object)entry).GetBytes(); 641 | } 642 | else if (type == typeof(BbRsaCert)) 643 | { 644 | name = ((BbRsaCert)(object)entry).CertNameString; 645 | data = ((BbRsaCert)(object)entry).GetBytes(); 646 | } 647 | else if (type == typeof(BbCrlHead)) 648 | { 649 | name = ((BbCrlHead)(object)entry).CertNameString; 650 | data = ((BbCrlHead)(object)entry).GetBytes(); 651 | } 652 | Console.WriteLine($"Writing entry {name}"); 653 | outputIO.Writer.Write(data); 654 | } 655 | } 656 | } 657 | Console.WriteLine($"Extraction complete, wrote {extractEntries.Count} entries!"); 658 | } 659 | 660 | Console.WriteLine(); 661 | } 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /iQueTool/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("iQueTool")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("iQueTool")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("bde052c7-adf7-4b11-b142-d33af0ccfcf7")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("0.4.1.0")] 36 | [assembly: AssemblyFileVersion("0.4.1.0")] 37 | -------------------------------------------------------------------------------- /iQueTool/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace iQueTool.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("iQueTool.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /iQueTool/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | -------------------------------------------------------------------------------- /iQueTool/Shared.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Numerics; 8 | 9 | namespace iQueTool 10 | { 11 | // ReSharper disable once InconsistentNaming 12 | public class IO : IDisposable 13 | { 14 | public BinaryReader Reader; 15 | public BinaryWriter Writer; 16 | public Stream Stream; 17 | 18 | public IO(string filePath) 19 | { 20 | Stream = new FileStream(filePath, FileMode.Open); 21 | InitIo(); 22 | } 23 | 24 | public IO(string filePath, FileMode mode) 25 | { 26 | Stream = new FileStream(filePath, mode); 27 | InitIo(); 28 | } 29 | 30 | public IO(Stream baseStream) 31 | { 32 | Stream = baseStream; 33 | InitIo(); 34 | } 35 | 36 | public void Dispose() 37 | { 38 | Stream.Dispose(); 39 | } 40 | 41 | public bool AddBytes(long numBytes) 42 | { 43 | const int blockSize = 0x1000; 44 | 45 | long startPos = Stream.Position; 46 | long startSize = Stream.Length; 47 | long endPos = startPos + numBytes; 48 | long endSize = Stream.Length + numBytes; 49 | 50 | Stream.SetLength(endSize); 51 | 52 | long totalWrite = startSize - startPos; 53 | 54 | while (totalWrite > 0) 55 | { 56 | int toRead = totalWrite < blockSize ? (int)totalWrite : blockSize; 57 | 58 | Stream.Position = startPos + (totalWrite - toRead); 59 | var data = Reader.ReadBytes(toRead); 60 | 61 | Stream.Position = startPos + (totalWrite - toRead); 62 | var blankData = new byte[toRead]; 63 | Writer.Write(blankData); 64 | 65 | Stream.Position = endPos + (totalWrite - toRead); 66 | Writer.Write(data); 67 | 68 | totalWrite -= toRead; 69 | } 70 | 71 | Stream.Position = startPos; 72 | 73 | return true; 74 | } 75 | 76 | public bool DeleteBytes(long numBytes) 77 | { 78 | if (Stream.Position + numBytes > Stream.Length) 79 | return false; 80 | 81 | const int blockSize = 0x1000; 82 | 83 | long startPos = Stream.Position; 84 | long endPos = startPos + numBytes; 85 | long endSize = Stream.Length - numBytes; 86 | long i = 0; 87 | 88 | while (i < endSize) 89 | { 90 | long totalRemaining = endSize - i; 91 | int toRead = totalRemaining < blockSize ? (int)totalRemaining : blockSize; 92 | 93 | Stream.Position = endPos + i; 94 | byte[] data = Reader.ReadBytes(toRead); 95 | 96 | Stream.Position = startPos + i; 97 | Writer.Write(data); 98 | 99 | i += toRead; 100 | } 101 | 102 | Stream.SetLength(endSize); 103 | return true; 104 | } 105 | 106 | private void InitIo() 107 | { 108 | Reader = new BinaryReader(Stream); 109 | Writer = new BinaryWriter(Stream); 110 | } 111 | } 112 | public static class Shared 113 | { 114 | public static string FindFile(string fileName) 115 | { 116 | string test = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); 117 | if (File.Exists(test)) 118 | return test; 119 | string[] drives = Directory.GetLogicalDrives(); 120 | foreach (string drive in drives) 121 | { 122 | test = Path.Combine(drive, fileName); 123 | if (File.Exists(test)) 124 | return test; 125 | } 126 | return String.Empty; 127 | } 128 | 129 | /// 130 | /// Reads in a block from a file and converts it to the struct 131 | /// type specified by the template parameter 132 | /// 133 | /// 134 | /// 135 | /// 136 | public static T ReadStruct(this BinaryReader reader) 137 | { 138 | var size = Marshal.SizeOf(typeof(T)); 139 | // Read in a byte array 140 | var bytes = reader.ReadBytes(size); 141 | 142 | return BytesToStruct(bytes); 143 | } 144 | 145 | public static bool WriteStruct(this BinaryWriter writer, T structure) 146 | { 147 | byte[] bytes = StructToBytes(structure); 148 | 149 | writer.Write(bytes); 150 | 151 | return true; 152 | } 153 | 154 | public static T BytesToStruct(byte[] bytes) 155 | { 156 | // Pin the managed memory while, copy it out the data, then unpin it 157 | var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); 158 | var theStructure = (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T)); 159 | handle.Free(); 160 | 161 | return theStructure; 162 | } 163 | 164 | public static byte[] StructToBytes(T structure) 165 | { 166 | var bytes = new byte[Marshal.SizeOf(typeof(T))]; 167 | 168 | // Pin the managed memory while, copy in the data, then unpin it 169 | var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned); 170 | Marshal.StructureToPtr(structure, handle.AddrOfPinnedObject(), true); 171 | handle.Free(); 172 | 173 | return bytes; 174 | } 175 | 176 | public static string ToHexString(this byte[] bytes) 177 | { 178 | return bytes.Aggregate("", (current, b) => current + (b.ToString("X2") + " ")); 179 | } 180 | 181 | public static bool IsArrayEmpty(this byte[] bytes) 182 | { 183 | return bytes.All(b => b == 0); 184 | } 185 | public static bool IsEqualTo(this byte[] byte1, byte[] byte2) 186 | { 187 | if (byte1.Length != byte2.Length) 188 | return false; 189 | 190 | for (int i = 0; i < byte1.Length; i++) 191 | if (byte1[i] != byte2[i]) 192 | return false; 193 | 194 | return true; 195 | } 196 | 197 | public static bool IsFlagSet(this uint flags, uint flag) 198 | { 199 | return (flags & flag) == flag; 200 | } 201 | 202 | public static uint RemoveFlag(this uint flags, uint flag) 203 | { 204 | return IsFlagSet(flags, flag) ? ToggleFlag(flags, flag) : flags; 205 | } 206 | 207 | public static uint ToggleFlag(this uint flags, uint flag) 208 | { 209 | return (flags ^ flag); 210 | } 211 | 212 | public static void AppendLineSpace(this StringBuilder b, string str) 213 | { 214 | b.AppendLine(str + " "); 215 | } 216 | 217 | public static byte[] MorphIv(byte[] iv) 218 | { 219 | byte dl = 0; 220 | var newIv = new byte[0x10]; 221 | 222 | for (int i = 0; i < 0x10; i++) 223 | { 224 | byte cl = iv[i]; 225 | byte al = cl; 226 | al = (byte)(al + al); 227 | al = (byte)(al | dl); 228 | dl = cl; 229 | newIv[i] = al; 230 | dl = (byte)(dl >> 7); 231 | } 232 | if (dl != 0) 233 | newIv[0] = (byte)(newIv[0] ^ 0x87); 234 | return newIv; 235 | } 236 | 237 | public static short EndianSwap(this short num) 238 | { 239 | byte[] data = BitConverter.GetBytes(num); 240 | Array.Reverse(data); 241 | return BitConverter.ToInt16(data, 0); 242 | } 243 | 244 | public static int EndianSwap(this int num) 245 | { 246 | byte[] data = BitConverter.GetBytes(num); 247 | Array.Reverse(data); 248 | return BitConverter.ToInt32(data, 0); 249 | } 250 | 251 | public static long EndianSwap(this long num) 252 | { 253 | byte[] data = BitConverter.GetBytes(num); 254 | Array.Reverse(data); 255 | return BitConverter.ToInt64(data, 0); 256 | } 257 | 258 | public static ushort EndianSwap(this ushort num) 259 | { 260 | byte[] data = BitConverter.GetBytes(num); 261 | Array.Reverse(data); 262 | return BitConverter.ToUInt16(data, 0); 263 | } 264 | 265 | public static uint EndianSwap(this uint num) 266 | { 267 | byte[] data = BitConverter.GetBytes(num); 268 | Array.Reverse(data); 269 | return BitConverter.ToUInt32(data, 0); 270 | } 271 | 272 | public static ulong EndianSwap(this ulong num) 273 | { 274 | byte[] data = BitConverter.GetBytes(num); 275 | Array.Reverse(data); 276 | return BitConverter.ToUInt64(data, 0); 277 | } 278 | 279 | public static bool iQueSignatureVerify(byte[] data, byte[] signature, byte[] pubKeyModulus, byte[] pubKeyExponent) 280 | { 281 | var hash = new SHA1Managed().ComputeHash(data); 282 | 283 | // resize sig in case its too large 284 | var sig = new byte[pubKeyModulus.Length]; 285 | Array.Copy(signature, sig, pubKeyModulus.Length); 286 | 287 | using (var rsa = new RSACryptoServiceProvider()) 288 | { 289 | var RSAKeyInfo = new RSAParameters() { Modulus = pubKeyModulus, Exponent = pubKeyExponent }; 290 | rsa.ImportParameters(RSAKeyInfo); 291 | 292 | var RSADeformatter = new RSAPKCS1SignatureDeformatter(rsa); 293 | RSADeformatter.SetHashAlgorithm("SHA1"); 294 | 295 | return RSADeformatter.VerifySignature(hash, sig); 296 | } 297 | } 298 | private static BigInteger PrepareBigInteger(byte[] unsignedBigEndian) 299 | { 300 | // Leave an extra 0x00 byte so that the sign bit is clear 301 | byte[] tmp = new byte[unsignedBigEndian.Length + 1]; 302 | Buffer.BlockCopy(unsignedBigEndian, 0, tmp, 1, unsignedBigEndian.Length); 303 | Array.Reverse(tmp); 304 | return new BigInteger(tmp); 305 | } 306 | 307 | public static byte[] iQueSignatureDecrypt(byte[] signature, byte[] pubKeyModulus, byte[] pubKeyExponent) 308 | { 309 | // resize sig in case its too large 310 | var sig = new byte[pubKeyModulus.Length]; 311 | Array.Copy(signature, sig, pubKeyModulus.Length); 312 | 313 | BigInteger n = PrepareBigInteger(pubKeyModulus); 314 | BigInteger e = PrepareBigInteger(pubKeyExponent); 315 | BigInteger sig2 = PrepareBigInteger(sig); 316 | BigInteger paddedMsgVal = BigInteger.ModPow(sig2, e, n); 317 | byte[] paddedMsg = paddedMsgVal.ToByteArray(); 318 | Array.Reverse(paddedMsg); 319 | 320 | // seems to use some kind of padding, PKCS1? 321 | // might be why fakesigning failed to work - our fakesign sig would have bad padding 322 | // need to find a signature that has retVal[0] below set to 0! 323 | byte[] retVal = new byte[0x14]; 324 | Array.Copy(paddedMsg, 235, retVal, 0, 0x14); 325 | return retVal; 326 | } 327 | 328 | // from https://stackoverflow.com/questions/249760/how-can-i-convert-a-unix-timestamp-to-datetime-and-vice-versa 329 | public static DateTime UnixTimeStampToDateTime(double unixTimeStamp) 330 | { 331 | // Unix timestamp is seconds past epoch 332 | System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, System.DateTimeKind.Utc); 333 | dtDateTime = dtDateTime.AddSeconds(unixTimeStamp).ToLocalTime(); 334 | return dtDateTime; 335 | } 336 | 337 | public static string NullTermCharsToString(char[] chars) 338 | { 339 | if (chars == null) 340 | return String.Empty; 341 | 342 | var str = new string(chars); 343 | var end = str.IndexOf('\0'); 344 | 345 | return end < 0 ? str : str.Remove(end); 346 | } 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /iQueTool/Structs/BbContentMetadataHead.cs: -------------------------------------------------------------------------------- 1 | using iQueTool.Files; 2 | using System; 3 | using System.Runtime.InteropServices; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | 7 | namespace iQueTool.Structs 8 | { 9 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 10 | public struct BbContentMetadataHead 11 | { 12 | /* 0x2800 */ public uint UnusedPadding; // always 0? 13 | 14 | /* 0x2804 */ public uint CACRLVersion; 15 | /* 0x2808 */ public uint CPCRLVersion; 16 | 17 | /* 0x280C */ public uint ContentSize; 18 | 19 | /* 0x2810 */ public uint ContentFlags; // 0 for tickets, 1 for SA? 20 | 21 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 22 | /* 0x2814 */ public byte[] TitleKeyIV; 23 | 24 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] 25 | /* 0x2824 */ public byte[] ContentHash; // SHA1 hash of the decrypted content 26 | 27 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 28 | /* 0x2838 */ public byte[] EncryptionIV; 29 | 30 | /* 0x2848 */ public uint ExecutionFlags; // 2 for game tickets, 0 for SA/iQue Club tickets 31 | /* 0x284C */ public uint AccessRights; // 0 for game tickets, 0x1F7 for normal SAs, 0x1B3 for weird (0 byte/3MB) SAs (but 0x13 for iQue Club ticket..) 32 | /* 0x2850 */ public uint KernelRights; // 0x4000 for games, 0xFFFFFFFF for normal SAs, 0xE01 for weird SAs, 0x6001 for iQue Club 33 | /* 0x2854 */ public uint BoundBBID; // if non-zero, app can only be ran by this BBID 34 | 35 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 36 | /* 0x2858 */ public char[] Authority; // always a CP cert, CP = content publisher? 37 | 38 | /* 0x2898 */ public uint ContentId; // can't be higher than 99999999 ?? if (cid / 100) % 10 == 9, this is a game manual 39 | 40 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 41 | /* 0x289C */ public byte[] TitleKey; // encrypted with common key if SA, or common key + console ECDH key if app 42 | 43 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] 44 | /* 0x28AC */ public byte[] Signature; // signature of 0x0 - 0xAC, before title key is encrypted with console ECDH key 45 | 46 | public string AuthorityString 47 | { 48 | get 49 | { 50 | return Shared.NullTermCharsToString(Authority).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 51 | } 52 | } 53 | 54 | public bool IsGameManual 55 | { 56 | get 57 | { 58 | return TitleId % 10 == 9; // last digit of titleid must be 9 59 | } 60 | } 61 | 62 | public uint TitleId 63 | { 64 | get 65 | { 66 | if (ContentId < 10000) 67 | return ContentId / 1000; // 4-digit content id must be SKSA, discard last 3 digits 68 | return ContentId / 100; // discard last 2 digits (version) 69 | } 70 | } 71 | 72 | public uint TitleVersion 73 | { 74 | get 75 | { 76 | if (ContentId < 10000) 77 | return ContentId % 1000; // 4-digit content id must be SKSA, return last 3 digits of content id 78 | return ContentId % 100; // last 2 digits of content id 79 | } 80 | } 81 | 82 | 83 | public byte[] DecryptTitleKey(byte[] commonKey, bool useIv = true) 84 | { 85 | // unsure if this is decrypting the right field 86 | // or if titlekey is even encrypted with AES128CBC 87 | // (seems Wii keys are though so it makes sense for iQue too) 88 | 89 | byte[] iv = new byte[0x10]; 90 | if(useIv) 91 | { 92 | // Wii uses 8-byte titleID as IV, so we'll test something similar 93 | byte[] cid = BitConverter.GetBytes(ContentId); 94 | Array.Copy(cid, iv, 4); 95 | } 96 | 97 | using (var aes = new AesManaged()) 98 | { 99 | aes.Key = commonKey; 100 | aes.IV = iv; 101 | aes.Mode = CipherMode.CBC; 102 | aes.Padding = PaddingMode.None; 103 | 104 | var cipher = aes.CreateDecryptor(); 105 | return cipher.TransformFinalBlock(TitleKey, 0, 0x10); 106 | } 107 | } 108 | 109 | public byte[] TicketHash 110 | { 111 | get 112 | { 113 | byte[] data = GetBytes(); 114 | Array.Resize(ref data, 0xAC); 115 | 116 | var sha1 = new SHA1Managed(); 117 | byte[] hash = sha1.ComputeHash(data); 118 | return hash; 119 | } 120 | } 121 | 122 | public byte[] DecryptedSignature 123 | { 124 | get 125 | { 126 | if (iQueCertCollection.MainCollection == null) 127 | return null; 128 | 129 | BbRsaCert authority; 130 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 131 | return null; 132 | 133 | return Shared.iQueSignatureDecrypt(Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 134 | } 135 | } 136 | 137 | public bool IsSignatureValid 138 | { 139 | get 140 | { 141 | if (iQueCertCollection.MainCollection == null) 142 | return false; 143 | 144 | BbRsaCert authority; 145 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 146 | return false; 147 | 148 | byte[] data = Shared.StructToBytes(this); 149 | Array.Resize(ref data, 0xAC); 150 | 151 | var res = Shared.iQueSignatureVerify(data, Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 152 | if (res) 153 | return true; 154 | 155 | // sig verify failed, try endian swapping 156 | EndianSwap(); 157 | data = Shared.StructToBytes(this); 158 | Array.Resize(ref data, 0xAC); 159 | EndianSwap(); 160 | 161 | return Shared.iQueSignatureVerify(data, Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 162 | } 163 | } 164 | 165 | public void EndianSwap() 166 | { 167 | CACRLVersion = CACRLVersion.EndianSwap(); 168 | CPCRLVersion = CPCRLVersion.EndianSwap(); 169 | 170 | ContentSize = ContentSize.EndianSwap(); 171 | ContentFlags = ContentFlags.EndianSwap(); 172 | 173 | ExecutionFlags = ExecutionFlags.EndianSwap(); 174 | AccessRights = AccessRights.EndianSwap(); 175 | KernelRights = KernelRights.EndianSwap(); 176 | BoundBBID = BoundBBID.EndianSwap(); 177 | 178 | ContentId = ContentId.EndianSwap(); 179 | } 180 | 181 | public byte[] GetBytes() 182 | { 183 | EndianSwap(); // back to device endian (BE) 184 | byte[] bytes = Shared.StructToBytes(this); 185 | EndianSwap(); // back to native endian (LE) 186 | return bytes; 187 | } 188 | 189 | public override string ToString() 190 | { 191 | return ToString(false); 192 | } 193 | 194 | public string ToString(bool formatted, string header = "BbContentMetaDataHead") 195 | { 196 | var b = new StringBuilder(); 197 | if (!string.IsNullOrEmpty(header)) 198 | b.AppendLine($"{header}:"); 199 | 200 | string fmt = formatted ? " " : ""; 201 | 202 | if (UnusedPadding != 0) 203 | b.AppendLineSpace(fmt + $"UnusedPadding != 0! (0x{UnusedPadding:X8})"); 204 | 205 | if (iQueCertCollection.MainCollection == null) 206 | b.AppendLineSpace(fmt + $"(Unable to verify RSA signature: cert.sys not found)"); 207 | else 208 | b.AppendLineSpace(fmt + $"(RSA signature {(IsSignatureValid ? "validated" : "appears invalid")})"); 209 | 210 | b.AppendLine(); 211 | 212 | b.AppendLineSpace(fmt + $"ContentId: {ContentId} (title: {TitleId}v{TitleVersion})"); 213 | b.AppendLineSpace(fmt + $"ContentSize: {ContentSize}"); 214 | b.AppendLineSpace(fmt + "ContentHash:" + Environment.NewLine + fmt + ContentHash.ToHexString()); 215 | 216 | b.AppendLine(); 217 | b.AppendLineSpace(fmt + $"ContentFlags: 0x{ContentFlags:X8}"); 218 | b.AppendLineSpace(fmt + $"ExecutionFlags: 0x{ExecutionFlags:X8}"); 219 | b.AppendLineSpace(fmt + $"AccessRights: 0x{AccessRights:X8}"); 220 | b.AppendLineSpace(fmt + $"KernelRights: 0x{KernelRights:X8}"); 221 | 222 | if(BoundBBID != 0) // only output BoundBBID if it's actually set 223 | b.AppendLineSpace(fmt + $"BoundBBID: {BoundBBID}"); 224 | 225 | b.AppendLine(); 226 | b.AppendLineSpace(fmt + "TitleKey:" + Environment.NewLine + fmt + TitleKey.ToHexString()); 227 | 228 | b.AppendLine(); 229 | b.AppendLineSpace(fmt + "TitleKeyIV:" + Environment.NewLine + fmt + TitleKeyIV.ToHexString()); 230 | 231 | b.AppendLine(); 232 | b.AppendLineSpace(fmt + "EncryptionIV:" + Environment.NewLine + fmt + EncryptionIV.ToHexString()); 233 | 234 | b.AppendLineSpace(fmt + $"Authority: {AuthorityString}"); 235 | 236 | b.AppendLine(); 237 | b.AppendLineSpace(fmt + "Signature:" + Environment.NewLine + fmt + Signature.ToHexString()); 238 | 239 | b.AppendLine(); 240 | b.AppendLineSpace(fmt + "Ticket Hash:" + Environment.NewLine + fmt + TicketHash.ToHexString()); 241 | 242 | var decSig = DecryptedSignature; 243 | if (decSig != null) 244 | { 245 | b.AppendLine(); 246 | b.AppendLineSpace(fmt + "Expected Hash (decrypted from signature):" + Environment.NewLine + fmt + decSig.ToHexString()); 247 | } 248 | 249 | return b.ToString(); 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /iQueTool/Structs/BbCrlHead.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using iQueTool.Files; 5 | 6 | namespace iQueTool.Structs 7 | { 8 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 9 | public struct BbCrlHead 10 | { 11 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x200)] 12 | /* 0x000 */ public byte[] Signature; // doesn't seem to validate atm... probably checking wrong region 13 | 14 | /* 0x200 */ public uint Type; 15 | /* 0x204 */ public uint SigType; 16 | /* 0x208 */ public uint UnusedPadding; 17 | /* 0x20C */ public uint VersionNumber; 18 | 19 | /* 0x210 */ public uint Timestamp; 20 | 21 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 22 | /* 0x214 */ public char[] Authority; 23 | 24 | /* 0x254 */ public uint NumRevoked; 25 | 26 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 27 | /* 0x258 */ public char[] CertName; 28 | 29 | public string AuthorityString 30 | { 31 | get 32 | { 33 | return Shared.NullTermCharsToString(Authority).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 34 | } 35 | } 36 | 37 | public string CertNameString 38 | { 39 | get 40 | { 41 | return Shared.NullTermCharsToString(CertName).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 42 | } 43 | } 44 | 45 | public DateTime TimestampDateTime 46 | { 47 | get 48 | { 49 | return Shared.UnixTimeStampToDateTime(Timestamp); 50 | } 51 | } 52 | 53 | public byte[] DecryptedSignature 54 | { 55 | get 56 | { 57 | if (iQueCertCollection.MainCollection == null) 58 | return null; 59 | 60 | BbRsaCert authority; 61 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 62 | return null; 63 | 64 | return Shared.iQueSignatureDecrypt(Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 65 | } 66 | } 67 | 68 | public bool IsSignatureValid 69 | { 70 | get 71 | { 72 | if (iQueCertCollection.MainCollection == null) 73 | return false; 74 | 75 | BbRsaCert authority; 76 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 77 | return false; 78 | 79 | byte[] data = Shared.StructToBytes(this); 80 | byte[] dataNoSig = new byte[0x98]; 81 | 82 | Array.Copy(data, 0x200, dataNoSig, 0, 0x98); // remove first 0x200 bytes 83 | 84 | var res = Shared.iQueSignatureVerify(data, Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 85 | if (res) 86 | return true; 87 | 88 | // sig verify failed, try endian swapping 89 | EndianSwap(); 90 | data = Shared.StructToBytes(this); 91 | Array.Copy(data, 0x200, dataNoSig, 0, 0x98); 92 | EndianSwap(); 93 | 94 | return Shared.iQueSignatureVerify(data, Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 95 | } 96 | } 97 | 98 | public BbCrlHead EndianSwap() 99 | { 100 | Type = Type.EndianSwap(); 101 | SigType = SigType.EndianSwap(); 102 | UnusedPadding = UnusedPadding.EndianSwap(); 103 | VersionNumber = VersionNumber.EndianSwap(); 104 | 105 | Timestamp = Timestamp.EndianSwap(); 106 | 107 | NumRevoked = NumRevoked.EndianSwap(); 108 | 109 | return this; 110 | } 111 | 112 | public byte[] GetBytes() 113 | { 114 | EndianSwap(); // back to device endian (BE) 115 | byte[] bytes = Shared.StructToBytes(this); 116 | EndianSwap(); // back to native endian (LE) 117 | return bytes; 118 | } 119 | 120 | public override string ToString() 121 | { 122 | return ToString(true); 123 | } 124 | 125 | public string ToString(bool formatted, string header = "BbCrlHead") 126 | { 127 | var b = new StringBuilder(); 128 | if (!string.IsNullOrEmpty(header)) 129 | b.AppendLine($"{header}:"); 130 | 131 | string fmt = formatted ? " " : ""; 132 | 133 | if (UnusedPadding != 0) 134 | b.AppendLineSpace(fmt + $"UnusedPadding != 0! (0x{UnusedPadding:X8})"); 135 | 136 | b.AppendLineSpace(fmt + $"CertName: {CertNameString}"); 137 | b.AppendLineSpace(fmt + $"Authority: {AuthorityString}"); 138 | 139 | b.AppendLineSpace(fmt + $"Type: {Type}"); 140 | b.AppendLineSpace(fmt + $"SigType: {SigType}"); 141 | b.AppendLineSpace(fmt + $"VersionNumber: {VersionNumber}"); 142 | b.AppendLineSpace(fmt + $"Timestamp: {TimestampDateTime} ({Timestamp})"); 143 | b.AppendLineSpace(fmt + $"NumRevoked: {NumRevoked}"); 144 | 145 | b.AppendLine(); 146 | b.AppendLineSpace(fmt + "Signature:" + Environment.NewLine + fmt + Signature.ToHexString()); 147 | 148 | var decSig = DecryptedSignature; 149 | if (decSig != null) 150 | { 151 | b.AppendLine(); 152 | b.AppendLineSpace(fmt + "Expected Hash (decrypted from signature):" + Environment.NewLine + fmt + decSig.ToHexString()); 153 | } 154 | 155 | return b.ToString(); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /iQueTool/Structs/BbFat16.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace iQueTool.Structs 6 | { 7 | // FAT header is at (fs_addr + 0x3FF4) in NAND, highest seqno is the best FAT 8 | // ique_diag seems to go through each block (highest to lowest) and check Magic = *(DWORD*)(blockAddr + 0x3ff4), if Magic is "BBFS" then it checks if the seqno is the highest 9 | // there's also BBFL for "linked fats", the dump I have doesn't have any of those though, i assume they're for when the FAT overflows 10 | // also afaik the top section of the nand is reserved for these FATs, so that it doesn't need to scan the entire nand i guess 11 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 12 | public struct BbFat16 13 | { 14 | /* 0x0 */ public uint Magic; 15 | /* 0x4 */ public int SeqNo; 16 | /* 0x8 */ public ushort Link; 17 | /* 0xA */ public ushort CheckSum; // all dwords in (fs+0 : fs+0x2000) added together + this checksum must equal 0xCAD7? 18 | 19 | public void EndianSwap() 20 | { 21 | Magic = Magic.EndianSwap(); 22 | SeqNo = SeqNo.EndianSwap(); 23 | Link = Link.EndianSwap(); 24 | CheckSum = CheckSum.EndianSwap(); 25 | } 26 | } 27 | 28 | // inode table starts at (fs_addr + 0x2000), with 0x199 max entries 29 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 30 | public struct BbInode 31 | { 32 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x8)] 33 | /* 0x00 */ public char[] Name; 34 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3)] 35 | /* 0x08 */ public char[] Extension; 36 | /* 0x0B */ public byte Type; 37 | /* 0x0C */ public short BlockIdx; // offset = BlockIdx * 0x4000, BlockIdx can sometimes be -1 for some reason? 38 | 39 | /* 0x0E */ public ushort PaddingE; // ique_diag only uses a WORD for the blockidx, so this must just be padding 40 | 41 | /* 0x10 */ public uint Size; 42 | 43 | public string NameString 44 | { 45 | get 46 | { 47 | if (Name == null && Extension == null) 48 | return String.Empty; 49 | var name = Shared.NullTermCharsToString(Name).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 50 | var ext = Shared.NullTermCharsToString(Extension).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 51 | return $"{name}.{ext}"; 52 | } 53 | set 54 | { 55 | var filename = value; 56 | var extension = ""; 57 | var extIdx = value.IndexOf("."); 58 | 59 | if(extIdx > -1 && filename.Length > (extIdx+1)) 60 | { 61 | extension = filename.Substring(extIdx + 1); 62 | filename = filename.Substring(0, extIdx); 63 | } 64 | 65 | if (filename.Length > 8) 66 | filename = filename.Substring(0, 8); 67 | if (extension.Length > 3) 68 | extension = extension.Substring(0, 3); 69 | 70 | // make sure name = 8 chars and extension = 3 chars 71 | var nameList = new List(filename.ToCharArray()); 72 | var extList = new List(extension.ToCharArray()); 73 | if (nameList.Count < 8) 74 | nameList.AddRange(new char[8 - nameList.Count]); 75 | if (extList.Count < 8) 76 | extList.AddRange(new char[3 - extList.Count]); 77 | 78 | Name = nameList.ToArray(); 79 | Extension = extList.ToArray(); 80 | } 81 | } 82 | 83 | public bool IsValid 84 | { 85 | get 86 | { 87 | return Type == 1 && BlockIdx != -1; 88 | } 89 | } 90 | 91 | public BbInode Copy(BbInode source) 92 | { 93 | Name = new char[8]; 94 | 95 | for (int i = 0; i < 8; i++) 96 | Name[i] = source.Name[i]; 97 | 98 | Extension = new char[3]; 99 | for (int i = 0; i < 3; i++) 100 | Extension[i] = source.Extension[i]; 101 | 102 | Type = source.Type; 103 | BlockIdx = source.BlockIdx; 104 | PaddingE = source.PaddingE; 105 | Size = source.Size; 106 | 107 | return this; 108 | } 109 | 110 | public void EndianSwap() 111 | { 112 | BlockIdx = (short)((ushort)BlockIdx).EndianSwap(); 113 | PaddingE = PaddingE.EndianSwap(); 114 | Size = Size.EndianSwap(); 115 | } 116 | 117 | public override string ToString() 118 | { 119 | return ToString(-1); 120 | } 121 | 122 | public string ToString(int idx) 123 | { 124 | var ret = $"name {NameString} block {BlockIdx} size 0x{Size:X} type {Type}"; 125 | if (Type == 1 && !IsValid) 126 | ret += " (has 'valid' field set, but uses invalid block idx?)"; 127 | 128 | if (idx == -1) 129 | return ret; 130 | return $"{idx}: {ret}"; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /iQueTool/Structs/BbLaunchMetadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace iQueTool.Structs 6 | { 7 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 8 | public struct BbLaunchMetadata 9 | { 10 | /* 0x0 */ public uint EepromAddress; 11 | /* 0x4 */ public uint EepromSize; 12 | /* 0x8 */ public uint FlashAddress; 13 | /* 0xC */ public uint FlashSize; 14 | /* 0x10 */ public uint SramAddress; 15 | /* 0x14 */ public uint SramSize; 16 | 17 | /* 0x18 */ public uint ControllerPak0Address; // controller 1 addon flags? (rumble / controller pak / ???) 18 | /* 0x1C */ public uint ControllerPak1Address; // controller 2 addon flags? (rumble / controller pak / ???) 19 | /* 0x20 */ public uint ControllerPak2Address; // controller 3 addon flags? (rumble / controller pak / ???) 20 | /* 0x24 */ public uint ControllerPak3Address; // controller 4 addon flags? (rumble / controller pak / ???) 21 | /* 0x28 */ public uint ControllerPakSize; // saves to a .u0* file if NumU0XFiles > 0? 22 | 23 | /* 0x2C */ public uint RomBase; // always 0xB0000000? 24 | /* 0x30 */ public uint TvType; // always 1? 25 | /* 0x34 */ public uint MemSize; // always 0x400000? 26 | /* 0x38 */ public uint ErrataSize; // always 0? 27 | /* 0x3C */ public uint ErrataAddress; // always 0? 28 | 29 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3)] 30 | /* 0x40 */ public byte[] Magic; // "CAM" for every ticket I've seen 31 | /* 0x43 */ public byte NumU0XFiles; // .u01 / .u02? seems to be used in Animal Crossing, maybe RTC related? 32 | 33 | public BbLaunchMetadata EndianSwap() 34 | { 35 | EepromAddress = EepromAddress.EndianSwap(); 36 | EepromSize = EepromSize.EndianSwap(); 37 | FlashAddress = FlashAddress.EndianSwap(); 38 | FlashSize = FlashSize.EndianSwap(); 39 | SramAddress = SramAddress.EndianSwap(); 40 | SramSize = SramSize.EndianSwap(); 41 | 42 | ControllerPak0Address = ControllerPak0Address.EndianSwap(); 43 | ControllerPak1Address = ControllerPak1Address.EndianSwap(); 44 | ControllerPak2Address = ControllerPak2Address.EndianSwap(); 45 | ControllerPak3Address = ControllerPak3Address.EndianSwap(); 46 | ControllerPakSize = ControllerPakSize.EndianSwap(); 47 | 48 | RomBase = RomBase.EndianSwap(); 49 | TvType = TvType.EndianSwap(); 50 | MemSize = MemSize.EndianSwap(); 51 | ErrataSize = ErrataSize.EndianSwap(); 52 | ErrataAddress = ErrataAddress.EndianSwap(); 53 | 54 | return this; 55 | } 56 | 57 | public string ToString(bool formatted, string header = "BbLaunchMetadata") 58 | { 59 | var b = new StringBuilder(); 60 | if (!string.IsNullOrEmpty(header)) 61 | b.AppendLine($"{header}:"); 62 | 63 | string fmt = formatted ? " " : ""; 64 | 65 | if (ErrataSize != 0) 66 | b.AppendLineSpace(fmt + $"ErrataSize != 0! (0x{ErrataSize:X8})"); 67 | if (ErrataAddress != 0) 68 | b.AppendLineSpace(fmt + $"ErrataAddress != 0! (0x{ErrataAddress:X8})"); 69 | 70 | b.AppendLine(); 71 | b.AppendLineSpace(fmt + $"EepromAddress: 0x{EepromAddress:X8}, size: 0x{EepromSize:X}"); 72 | b.AppendLineSpace(fmt + $"FlashAddress: 0x{FlashAddress:X8}, size: 0x{FlashSize:X}"); 73 | b.AppendLineSpace(fmt + $"SramAddress: 0x{SramAddress:X8}, size: 0x{SramSize:X}"); 74 | 75 | b.AppendLine(); 76 | b.AppendLineSpace(fmt + $"ControllerPak0Address: 0x{ControllerPak0Address:X}"); 77 | b.AppendLineSpace(fmt + $"ControllerPak1Address: 0x{ControllerPak1Address:X}"); 78 | b.AppendLineSpace(fmt + $"ControllerPak2Address: 0x{ControllerPak2Address:X}"); 79 | b.AppendLineSpace(fmt + $"ControllerPak3Address: 0x{ControllerPak3Address:X}"); 80 | b.AppendLineSpace(fmt + $"ControllerPakSize: 0x{ControllerPakSize:X}"); 81 | 82 | b.AppendLine(); 83 | b.AppendLineSpace(fmt + $"RomBase: 0x{RomBase:X8}"); 84 | b.AppendLineSpace(fmt + $"TvType: 0x{TvType:X8}"); 85 | b.AppendLineSpace(fmt + $"MemSize: 0x{MemSize:X8}"); 86 | 87 | if (ErrataAddress != 0 || ErrataSize != 0) 88 | { 89 | b.AppendLineSpace(fmt + $"ErrataSize: 0x{ErrataSize:X8}"); 90 | b.AppendLineSpace(fmt + $"ErrataAddress: 0x{ErrataAddress:X8}"); 91 | } 92 | 93 | b.AppendLine(); 94 | b.AppendLineSpace(fmt + $"NumU0XFiles: 0x{NumU0XFiles:X}"); 95 | 96 | b.AppendLine(); 97 | b.AppendLineSpace(fmt + "Magic:" + Environment.NewLine + fmt + Magic.ToHexString()); 98 | 99 | return b.ToString(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /iQueTool/Structs/BbRsaCert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using iQueTool.Files; 5 | 6 | namespace iQueTool.Structs 7 | { 8 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 9 | public struct BbRsaCert 10 | { 11 | /* 0x000 */ public uint CertType; 12 | /* 0x004 */ public uint SigType; // 0 if signature is 2048-bit, 1 if sig is 4096-bit? 13 | /* 0x008 */ public uint Date; // timestamp? 14 | 15 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 16 | /* 0x00C */ public char[] Authority; 17 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 18 | /* 0x04C */ public char[] CertName; 19 | 20 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] 21 | /* 0x08C */ public byte[] PublicKeyModulus; 22 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] 23 | /* 0x18C */ public byte[] PublicKeyExponent; 24 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x200)] 25 | /* 0x190 */ public byte[] Signature; // signature of 0x0 - 0x190 made using Authority key 26 | 27 | 28 | public BbRsaCert(string authority, string certName, byte[] modulus) 29 | { 30 | CertType = 0; 31 | SigType = 0; 32 | Date = 0; 33 | Authority = authority.ToCharArray(); 34 | CertName = certName.ToCharArray(); 35 | 36 | PublicKeyModulus = modulus; 37 | PublicKeyExponent = new byte[] { 0x00, 0x01, 0x00, 0x01 }; 38 | Signature = new byte[0x200]; 39 | } 40 | 41 | public string AuthorityString 42 | { 43 | get 44 | { 45 | return Shared.NullTermCharsToString(Authority).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 46 | } 47 | } 48 | 49 | public string CertNameString 50 | { 51 | get 52 | { 53 | return Shared.NullTermCharsToString(CertName).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 54 | } 55 | } 56 | 57 | public byte[] DecryptedSignature 58 | { 59 | get 60 | { 61 | if (iQueCertCollection.MainCollection == null) 62 | return null; 63 | 64 | BbRsaCert authority; 65 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 66 | return null; 67 | 68 | return Shared.iQueSignatureDecrypt(Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 69 | } 70 | } 71 | 72 | public bool IsSignatureValid 73 | { 74 | get 75 | { 76 | if (iQueCertCollection.MainCollection == null) 77 | return false; 78 | 79 | BbRsaCert authority; 80 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 81 | return false; 82 | 83 | byte[] data = Shared.StructToBytes(this); 84 | Array.Resize(ref data, 0x190); 85 | 86 | var res = Shared.iQueSignatureVerify(data, Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 87 | if (res) 88 | return true; 89 | 90 | // sig verify failed, try endian swapping 91 | EndianSwap(); 92 | data = Shared.StructToBytes(this); 93 | Array.Resize(ref data, 0x190); 94 | EndianSwap(); 95 | 96 | return Shared.iQueSignatureVerify(data, Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 97 | } 98 | } 99 | 100 | public BbRsaCert EndianSwap() 101 | { 102 | CertType = CertType.EndianSwap(); 103 | SigType = SigType.EndianSwap(); 104 | Date = Date.EndianSwap(); 105 | 106 | return this; 107 | } 108 | 109 | public byte[] GetBytes() 110 | { 111 | EndianSwap(); // back to device endian (BE) 112 | byte[] bytes = Shared.StructToBytes(this); 113 | EndianSwap(); // back to native endian (LE) 114 | return bytes; 115 | } 116 | 117 | public override string ToString() 118 | { 119 | return ToString(true); 120 | } 121 | 122 | public string ToString(bool formatted, string header = "BbRsaCert") 123 | { 124 | var b = new StringBuilder(); 125 | if (!string.IsNullOrEmpty(header)) 126 | b.AppendLine($"{header}:"); 127 | 128 | string fmt = formatted ? " " : ""; 129 | 130 | if (iQueCertCollection.MainCollection == null) 131 | b.AppendLineSpace(fmt + $"(Unable to verify RSA signature: cert.sys not found)"); 132 | else 133 | b.AppendLineSpace(fmt + $"(RSA signature {(IsSignatureValid ? "validated" : "appears invalid")})"); 134 | 135 | b.AppendLineSpace(fmt + $"CertName: {CertNameString} ({(string.IsNullOrEmpty(AuthorityString) ? CertNameString : $"{AuthorityString}-{CertNameString}")})"); 136 | b.AppendLineSpace(fmt + $"CertType: {CertType}"); 137 | b.AppendLineSpace(fmt + $"SigType: {SigType}"); 138 | b.AppendLineSpace(fmt + $"Date: {Date}"); 139 | 140 | b.AppendLine(); 141 | b.AppendLineSpace(fmt + $"Authority: {AuthorityString}"); 142 | 143 | b.AppendLine(); 144 | b.AppendLineSpace(fmt + "PublicKeyModulus:" + Environment.NewLine + fmt + PublicKeyModulus.ToHexString()); 145 | b.AppendLineSpace(fmt + "PublicKeyExponent:" + Environment.NewLine + fmt + PublicKeyExponent.ToHexString()); 146 | 147 | b.AppendLine(); 148 | b.AppendLineSpace(fmt + "Signature:" + Environment.NewLine + fmt + Signature.ToHexString()); 149 | 150 | var decSig = DecryptedSignature; 151 | if (decSig != null) 152 | { 153 | b.AppendLine(); 154 | b.AppendLineSpace(fmt + "Expected Hash (decrypted from signature):" + Environment.NewLine + fmt + decSig.ToHexString()); 155 | } 156 | 157 | return b.ToString(); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /iQueTool/Structs/BbTicketHead.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using iQueTool.Files; 5 | 6 | namespace iQueTool.Structs 7 | { 8 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 9 | public struct BbTicketHead 10 | { 11 | /* 0x29AC */ public uint BBID; // matches value inside id.sys, seems to be stored in the iQue unit somewhere, iQue unit ID has to match id.sys ID which has to match BBID field 12 | 13 | // next field seems to determine the type of ticket 14 | // tid >= 0x8000 is LP (limited play?) 15 | // tid >= 0x7000 && tid < 0x8000 is a "global ticket" 16 | // tid < 0x7000 is a permanent ticket 17 | /* 0x29B0 */ public ushort TicketId; // ticketid? titleid? both terms seem to be used 18 | 19 | /* 0x29B2 */ public ushort TrialType; 20 | /* 0x29B4 */ public ushort TrialLimit; // number of minutes/launches for this trial 21 | 22 | /* 0x29B6 */ public ushort UnusedReserved; 23 | 24 | /* 0x29B8 */ public uint TSCRLVersion; // ticket_crl_version? 25 | 26 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 27 | /* 0x29BC */ public byte[] CMD_IV; // titlekey_iv? isn't this already set in CMD? 28 | 29 | // from iquebrew: 30 | // ECC public key used with console's ECC private key to derive unique title key encryption key via ECDH 31 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 32 | /* 0x29CC */ public byte[] ServerECCKey; 33 | 34 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x40)] 35 | /* 0x2A0C */ public char[] Authority; // always an XS (exchange server) cert? 36 | 37 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x100)] 38 | /* 0x2A4C */ public byte[] Signature; // signature? doesn't seem to verify no matter what I try... 39 | 40 | public string AuthorityString 41 | { 42 | get 43 | { 44 | return Shared.NullTermCharsToString(Authority).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 45 | } 46 | } 47 | 48 | public bool IsTicketLimitedPlay 49 | { 50 | get 51 | { 52 | return TicketId >= 0x8000; 53 | } 54 | } 55 | 56 | public bool IsTicketGlobal 57 | { 58 | get 59 | { 60 | return TicketId >= 0x7000 && TicketId < 0x8000; 61 | } 62 | } 63 | 64 | public bool IsTicketPermanent 65 | { 66 | get 67 | { 68 | return TicketId < 0x7000; 69 | } 70 | } 71 | 72 | public byte[] DecryptedSignature 73 | { 74 | get 75 | { 76 | if (iQueCertCollection.MainCollection == null) 77 | return null; 78 | 79 | BbRsaCert authority; 80 | if (!iQueCertCollection.MainCollection.GetCertificate(AuthorityString, out authority)) 81 | return null; 82 | 83 | return Shared.iQueSignatureDecrypt(Signature, authority.PublicKeyModulus, authority.PublicKeyExponent); 84 | } 85 | } 86 | 87 | public BbTicketHead EndianSwap() 88 | { 89 | BBID = BBID.EndianSwap(); 90 | TicketId = TicketId.EndianSwap(); 91 | 92 | TrialType = TrialType.EndianSwap(); 93 | TrialLimit = TrialLimit.EndianSwap(); 94 | 95 | UnusedReserved = UnusedReserved.EndianSwap(); 96 | TSCRLVersion = TSCRLVersion.EndianSwap(); 97 | 98 | return this; 99 | } 100 | 101 | public string ToString(bool formatted, string header = "BbTicketHead") 102 | { 103 | var b = new StringBuilder(); 104 | if (!string.IsNullOrEmpty(header)) 105 | b.AppendLine($"{header}:"); 106 | 107 | string fmt = formatted ? " " : ""; 108 | 109 | if (UnusedReserved != 0) 110 | b.AppendLineSpace(fmt + $"UnusedReserved != 0! (0x{UnusedReserved:X4})"); 111 | 112 | b.AppendLine(); 113 | b.AppendLineSpace(fmt + $"BBID: 0x{BBID:X}"); 114 | 115 | string ticketType = ""; 116 | if (IsTicketGlobal) 117 | ticketType = "global"; 118 | else if (IsTicketLimitedPlay) 119 | ticketType = "limited play"; 120 | else if (IsTicketPermanent) 121 | ticketType = "permanent"; 122 | 123 | b.AppendLineSpace(fmt + $"TicketId: 0x{TicketId:X8} ({ticketType} ticket)"); 124 | 125 | string trialType = TrialType == 1 ? "launch count" : (TrialType == 2 ? "time-limited" : "time-limited or not trial"); 126 | 127 | b.AppendLineSpace(fmt + $"TrialType: {TrialType} ({trialType})"); 128 | 129 | b.AppendLineSpace(fmt + $"TSCRLVersion / ticket_crl_version: {TSCRLVersion}"); 130 | 131 | b.AppendLine(); 132 | b.AppendLineSpace(fmt + "CMD_IV / titlekey_iv:" + Environment.NewLine + fmt + CMD_IV.ToHexString()); 133 | 134 | b.AppendLine(); 135 | b.AppendLineSpace(fmt + "ServerECCKey:" + Environment.NewLine + fmt + ServerECCKey.ToHexString()); 136 | 137 | b.AppendLine(); 138 | b.AppendLineSpace(fmt + $"Authority: {AuthorityString}"); 139 | 140 | b.AppendLine(); 141 | b.AppendLineSpace(fmt + "Signature:" + Environment.NewLine + fmt + Signature.ToHexString()); 142 | 143 | var decSig = DecryptedSignature; 144 | if (decSig != null) 145 | { 146 | b.AppendLine(); 147 | b.AppendLineSpace(fmt + "Expected Hash (decrypted from signature):" + Environment.NewLine + fmt + decSig.ToHexString()); 148 | } 149 | 150 | return b.ToString(); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /iQueTool/Structs/OSBbSaGameMetaData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | using iQueTool.Files; 5 | 6 | namespace iQueTool.Structs 7 | { 8 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 9 | public struct OSBbSaGameMetaData 10 | { 11 | /* 0x00 */ public BbLaunchMetadata LaunchMetadata; 12 | /* 0x44 */ public ushort ThumbImgLength; // can't be more than 0x4000, decompressed length must be exactly 0x1880 bytes! 13 | /* 0x46 */ public ushort TitleImgLength; // can't be more than 0x10000 (how exactly would that even fit?) 14 | 15 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x27B8)] 16 | /* 0x48 */ public byte[] ImagesAndTitle; // contains gzipped thumb img, gzipped title img, title name + sometimes an ISBN 17 | 18 | /* 0x2800 */ public BbContentMetadataHead ContentMetadata; 19 | 20 | /* 0x29AC */ public BbTicketHead Ticket; 21 | 22 | public byte[] ThumbImage 23 | { 24 | get 25 | { 26 | if (ImagesAndTitle == null) 27 | return null; 28 | 29 | var img = new byte[ThumbImgLength]; 30 | Array.Copy(ImagesAndTitle, 0, img, 0, ThumbImgLength); 31 | return img; 32 | } 33 | } 34 | public byte[] TitleImage 35 | { 36 | get 37 | { 38 | if (ImagesAndTitle == null) 39 | return null; 40 | 41 | var img = new byte[TitleImgLength]; 42 | Array.Copy(ImagesAndTitle, ThumbImgLength, img, 0, TitleImgLength); 43 | return img; 44 | } 45 | } 46 | 47 | public byte[] TitleInfoBytes 48 | { 49 | get 50 | { 51 | if (ImagesAndTitle == null) 52 | return null; 53 | 54 | var nameSize = 0x27B8 - ThumbImgLength - TitleImgLength; 55 | var name = new byte[nameSize]; 56 | Array.Copy(ImagesAndTitle, ThumbImgLength + TitleImgLength, name, 0, nameSize); 57 | return name; 58 | } 59 | } 60 | 61 | public int TitleNameLength 62 | { 63 | get 64 | { 65 | var bytes = TitleInfoBytes; 66 | for (int i = 0; i < bytes.Length; i++) 67 | if (bytes[i] == 0) 68 | return i; 69 | 70 | return -1; 71 | } 72 | } 73 | 74 | public int ISBNLength 75 | { 76 | get 77 | { 78 | var nameLength = TitleNameLength; 79 | if (nameLength < 0) 80 | return -1; 81 | 82 | var bytes = TitleInfoBytes; 83 | for (int i = nameLength + 1; i < bytes.Length; i++) 84 | if (bytes[i] == 0) 85 | return i - (nameLength + 1); 86 | 87 | return -1; 88 | } 89 | } 90 | 91 | public string TitleName 92 | { 93 | get 94 | { 95 | var bytes = TitleInfoBytes; 96 | var size = TitleNameLength; 97 | if (size <= 0) 98 | return String.Empty; 99 | var nameBytes = new byte[size]; 100 | Array.Copy(bytes, nameBytes, size); 101 | 102 | return Encoding.GetEncoding(936).GetString(nameBytes).Replace("\0", "").Replace("\r", "").Replace("\n", ""); // gb2312 (codepage 936) 103 | } 104 | } 105 | 106 | public string ISBN 107 | { 108 | get 109 | { 110 | var bytes = TitleInfoBytes; 111 | var nameSize = TitleNameLength; 112 | if (nameSize < 0) 113 | return string.Empty; 114 | 115 | var isbnSize = ISBNLength; 116 | if (isbnSize <= 0) 117 | return String.Empty; 118 | 119 | var isbnBytes = new byte[isbnSize]; 120 | Array.Copy(bytes, nameSize + 1, isbnBytes, 0, isbnSize); 121 | 122 | return Encoding.UTF8.GetString(isbnBytes).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 123 | } 124 | } 125 | 126 | // makes a unique string for each bbid/contentid/titleid combination 127 | public string TicketUID 128 | { 129 | get 130 | { 131 | return $"{Ticket.BBID:X}-{ContentMetadata.ContentId}-{Ticket.TicketId:X}"; 132 | } 133 | } 134 | 135 | public OSBbSaGameMetaData EndianSwap() 136 | { 137 | LaunchMetadata.EndianSwap(); 138 | 139 | ThumbImgLength = ThumbImgLength.EndianSwap(); 140 | TitleImgLength = TitleImgLength.EndianSwap(); 141 | 142 | ContentMetadata.EndianSwap(); 143 | Ticket.EndianSwap(); 144 | 145 | return this; 146 | } 147 | 148 | public byte[] GetBytes() 149 | { 150 | EndianSwap(); // back to device endian (BE) 151 | byte[] bytes = Shared.StructToBytes(this); 152 | EndianSwap(); // back to native endian (LE) 153 | return bytes; 154 | } 155 | 156 | public override string ToString() 157 | { 158 | return ToString(true); 159 | } 160 | 161 | public string ToString(bool formatted, string header = "OSBbSaGameMetaData") 162 | { 163 | var b = new StringBuilder(); 164 | if(!string.IsNullOrEmpty(header)) 165 | b.AppendLine($"{header}:"); 166 | 167 | string fmt = formatted ? " " : ""; 168 | 169 | // some stuff to alert me of unks that are different 170 | 171 | if (ThumbImgLength > 0x4000) 172 | b.AppendLineSpace(fmt + "ThumbImgLength > 0x4000! (invalid?)"); 173 | if (TitleImgLength > 0x10000) // unsure how this can even be possible, but it seems to get checked anyway 174 | b.AppendLineSpace(fmt + "TitleImgLength > 0x10000! (invalid?)"); 175 | 176 | if (ContentMetadata.ContentId > 99999999) 177 | b.AppendLineSpace(fmt + "Ticket.ContentId > 99999999! (invalid?)"); 178 | 179 | b.AppendLine(); 180 | b.AppendLineSpace(LaunchMetadata.ToString(formatted, header + ".LaunchMetadata")); 181 | 182 | b.AppendLineSpace(fmt + $"ThumbImgLength: {ThumbImgLength}"); 183 | b.AppendLineSpace(fmt + $"TitleImgLength: {TitleImgLength}"); 184 | 185 | b.AppendLine(); 186 | b.AppendLineSpace(fmt + "TitleName: " + TitleName + (!String.IsNullOrEmpty(ISBN) ? $" ({ISBN})" : "")); 187 | 188 | b.AppendLine(); 189 | b.AppendLineSpace(ContentMetadata.ToString(formatted, header + ".ContentMetadata")); 190 | 191 | b.AppendLine(); 192 | b.AppendLineSpace(Ticket.ToString(formatted, header + ".Ticket")); 193 | 194 | return b.ToString(); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /iQueTool/Structs/iQueBlockSpare.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace iQueTool.Structs 8 | { 9 | // format of the BB-returned block-spare data, which seems slightly different to the actual on-chip page-spare data? 10 | // seems BB returns only the last page's spare data for each block read (0x20 pages per block), while setting 0x6 to 0x00 for some reason 11 | 12 | // when writing BB gets sent all FF as spare data (except for SA area, where first 3 bytes of spare are set by _bbc_writeSystemApp/wsa & __bbc_write_system_app) 13 | // unit must be recalcing the ECC itself before writing? 14 | 15 | // spare is returned as all 00 for bad blocks 16 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 17 | public struct iQueBlockSpare 18 | { 19 | // next three are only set for SA license/ticket & SA data blocks 20 | // seems to be set like 21 | // SAx license block: block num of last SAx data block (set by __bbc_write_system_app) 22 | // SAx block 1: block num of next SA license block, or 0xFF if theres no SA following this one (set by _bbc_writeSystemApp / wsa) 23 | // SAx block n: block num of block n-1 (set by __bbc_write_system_app) 24 | // block nums are treated as a single byte, copied to all three fields 25 | 26 | // i guess because of this it might be reading the SAs back to front? 27 | // SA1 ticket block points to SA1 last block, which points to SA1 last block(-1) etc, until it gets around to the first SA1 data block, which then points to the SA2 ticket 28 | // must be how it handles badblocks in the SA area? 29 | 30 | public byte SAData_0; 31 | public byte SAData_1; 32 | public byte SAData_2; 33 | 34 | public byte Unk3; // always FF? 35 | public byte Unk4; // always FF? 36 | public byte BadBlockIndicator; // any bit unset means bad block (each bit is checked seperately.. each bit counts as 4 pages? (4 pages = each 2048-byte part?)) 37 | public byte Unk6; // always 00? (0xFF in page spares) 38 | public byte Unk7; // always FF? 39 | 40 | // ECC is all FF in pages that are either all 00 or all FF (or blocks where the last page is all 00/all FF) 41 | 42 | // ECC for 0x100 - 0x200 of the page 43 | public byte Ecc2_0; 44 | public byte Ecc2_1; 45 | public byte Ecc2_2; 46 | 47 | public byte UnkB; // always FF? 48 | public byte UnkC; // always FF? 49 | 50 | // ECC for 0x0 - 0x100 of the page 51 | public byte Ecc1_0; 52 | public byte Ecc1_1; 53 | public byte Ecc1_2; 54 | 55 | // calcs ECC for a 512-byte block, formatted like the iQue ([3-byte ECC(0x100:0x200)] 0xFF 0xFF [3-byte ECC(0:0x100)]) 56 | public static byte[] Calculate512Ecc(byte[] pageData) 57 | { 58 | byte[] res = new byte[8]; 59 | res[3] = res[4] = 0xFF; 60 | 61 | byte[] ecc1 = Calculate256Ecc(pageData, 0); 62 | byte[] ecc2 = Calculate256Ecc(pageData, 0x100); 63 | Array.Copy(ecc2, 0, res, 0, 3); 64 | Array.Copy(ecc1, 0, res, 5, 3); 65 | 66 | return res; 67 | } 68 | 69 | // ECC algo ported from https://github.com/TheBlueMatt/u-boot/blob/master/drivers/mtd/nand/nand_ecc.c 70 | static byte[] EccPrecalcTable = 71 | { 72 | 0x00, 0x55, 0x56, 0x03, 0x59, 0x0c, 0x0f, 0x5a, 0x5a, 0x0f, 0x0c, 0x59, 0x03, 0x56, 0x55, 0x00, 73 | 0x65, 0x30, 0x33, 0x66, 0x3c, 0x69, 0x6a, 0x3f, 0x3f, 0x6a, 0x69, 0x3c, 0x66, 0x33, 0x30, 0x65, 74 | 0x66, 0x33, 0x30, 0x65, 0x3f, 0x6a, 0x69, 0x3c, 0x3c, 0x69, 0x6a, 0x3f, 0x65, 0x30, 0x33, 0x66, 75 | 0x03, 0x56, 0x55, 0x00, 0x5a, 0x0f, 0x0c, 0x59, 0x59, 0x0c, 0x0f, 0x5a, 0x00, 0x55, 0x56, 0x03, 76 | 0x69, 0x3c, 0x3f, 0x6a, 0x30, 0x65, 0x66, 0x33, 0x33, 0x66, 0x65, 0x30, 0x6a, 0x3f, 0x3c, 0x69, 77 | 0x0c, 0x59, 0x5a, 0x0f, 0x55, 0x00, 0x03, 0x56, 0x56, 0x03, 0x00, 0x55, 0x0f, 0x5a, 0x59, 0x0c, 78 | 0x0f, 0x5a, 0x59, 0x0c, 0x56, 0x03, 0x00, 0x55, 0x55, 0x00, 0x03, 0x56, 0x0c, 0x59, 0x5a, 0x0f, 79 | 0x6a, 0x3f, 0x3c, 0x69, 0x33, 0x66, 0x65, 0x30, 0x30, 0x65, 0x66, 0x33, 0x69, 0x3c, 0x3f, 0x6a, 80 | 0x6a, 0x3f, 0x3c, 0x69, 0x33, 0x66, 0x65, 0x30, 0x30, 0x65, 0x66, 0x33, 0x69, 0x3c, 0x3f, 0x6a, 81 | 0x0f, 0x5a, 0x59, 0x0c, 0x56, 0x03, 0x00, 0x55, 0x55, 0x00, 0x03, 0x56, 0x0c, 0x59, 0x5a, 0x0f, 82 | 0x0c, 0x59, 0x5a, 0x0f, 0x55, 0x00, 0x03, 0x56, 0x56, 0x03, 0x00, 0x55, 0x0f, 0x5a, 0x59, 0x0c, 83 | 0x69, 0x3c, 0x3f, 0x6a, 0x30, 0x65, 0x66, 0x33, 0x33, 0x66, 0x65, 0x30, 0x6a, 0x3f, 0x3c, 0x69, 84 | 0x03, 0x56, 0x55, 0x00, 0x5a, 0x0f, 0x0c, 0x59, 0x59, 0x0c, 0x0f, 0x5a, 0x00, 0x55, 0x56, 0x03, 85 | 0x66, 0x33, 0x30, 0x65, 0x3f, 0x6a, 0x69, 0x3c, 0x3c, 0x69, 0x6a, 0x3f, 0x65, 0x30, 0x33, 0x66, 86 | 0x65, 0x30, 0x33, 0x66, 0x3c, 0x69, 0x6a, 0x3f, 0x3f, 0x6a, 0x69, 0x3c, 0x66, 0x33, 0x30, 0x65, 87 | 0x00, 0x55, 0x56, 0x03, 0x59, 0x0c, 0x0f, 0x5a, 0x5a, 0x0f, 0x0c, 0x59, 0x03, 0x56, 0x55, 0x00 88 | }; 89 | 90 | // calcs ECC for a 256-byte block 91 | public static byte[] Calculate256Ecc(byte[] pageData, int offset) 92 | { 93 | byte idx, reg1, reg2, reg3, tmp1, tmp2; 94 | int i; 95 | 96 | /* Initialize variables */ 97 | reg1 = reg2 = reg3 = 0; 98 | 99 | /* Build up column parity */ 100 | for (i = 0; i < 256; i++) 101 | { 102 | /* Get CP0 - CP5 from table */ 103 | idx = EccPrecalcTable[pageData[i + offset]]; 104 | reg1 ^= (byte)(idx & 0x3f); 105 | 106 | /* All bit XOR = 1 ? */ 107 | if ((idx & 0x40) == 0x40) 108 | { 109 | reg3 ^= (byte)i; 110 | reg2 ^= (byte)~((byte)i); 111 | } 112 | } 113 | 114 | /* Create non-inverted ECC code from line parity */ 115 | tmp1 = (byte)((reg3 & 0x80) >> 0); /* B7 -> B7 */ 116 | tmp1 |= (byte)((reg2 & 0x80) >> 1); /* B7 -> B6 */ 117 | tmp1 |= (byte)((reg3 & 0x40) >> 1); /* B6 -> B5 */ 118 | tmp1 |= (byte)((reg2 & 0x40) >> 2); /* B6 -> B4 */ 119 | tmp1 |= (byte)((reg3 & 0x20) >> 2); /* B5 -> B3 */ 120 | tmp1 |= (byte)((reg2 & 0x20) >> 3); /* B5 -> B2 */ 121 | tmp1 |= (byte)((reg3 & 0x10) >> 3); /* B4 -> B1 */ 122 | tmp1 |= (byte)((reg2 & 0x10) >> 4); /* B4 -> B0 */ 123 | 124 | tmp2 = (byte)((reg3 & 0x08) << 4); /* B3 -> B7 */ 125 | tmp2 |= (byte)((reg2 & 0x08) << 3); /* B3 -> B6 */ 126 | tmp2 |= (byte)((reg3 & 0x04) << 3); /* B2 -> B5 */ 127 | tmp2 |= (byte)((reg2 & 0x04) << 2); /* B2 -> B4 */ 128 | tmp2 |= (byte)((reg3 & 0x02) << 2); /* B1 -> B3 */ 129 | tmp2 |= (byte)((reg2 & 0x02) << 1); /* B1 -> B2 */ 130 | tmp2 |= (byte)((reg3 & 0x01) << 1); /* B0 -> B1 */ 131 | tmp2 |= (byte)((reg2 & 0x01) << 0); /* B7 -> B0 */ 132 | 133 | /* Calculate final ECC code */ 134 | byte[] ecc = new byte[3]; 135 | ecc[0] = (byte)~tmp2; 136 | ecc[1] = (byte)~tmp1; 137 | ecc[2] = (byte)(((~reg1) << 2) | 0x03); 138 | 139 | return ecc; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /iQueTool/Structs/iQuePrivateData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace iQueTool.Structs 6 | { 7 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 8 | public struct iQuePrivateData 9 | { 10 | /* 0x0 */ public uint BBID; 11 | /* 0x4 */ public uint Timestamp; 12 | 13 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x24)] 14 | /* 0x8 */ public char[] BBModel; 15 | 16 | /* 0x2C */ public uint SecureContentId; // SKSA content id 17 | /* 0x30 */ public uint CrlVersion; 18 | 19 | public string BBModelString 20 | { 21 | get 22 | { 23 | return Shared.NullTermCharsToString(BBModel).Replace("\0", "").Replace("\r", "").Replace("\n", ""); 24 | } 25 | } 26 | 27 | public DateTime TimestampDateTime 28 | { 29 | get 30 | { 31 | return Shared.UnixTimeStampToDateTime(Timestamp); 32 | } 33 | } 34 | 35 | public void EndianSwap() 36 | { 37 | BBID = BBID.EndianSwap(); 38 | Timestamp = Timestamp.EndianSwap(); 39 | 40 | SecureContentId = SecureContentId.EndianSwap(); 41 | CrlVersion = CrlVersion.EndianSwap(); 42 | } 43 | 44 | public byte[] GetBytes() 45 | { 46 | EndianSwap(); // back to device endian (BE) 47 | byte[] bytes = Shared.StructToBytes(this); 48 | EndianSwap(); // back to native endian (LE) 49 | return bytes; 50 | } 51 | 52 | public override string ToString() 53 | { 54 | return ToString(false); 55 | } 56 | 57 | public string ToString(bool formatted, string header = "iQuePrivateData") 58 | { 59 | var b = new StringBuilder(); 60 | if (!string.IsNullOrEmpty(header)) 61 | b.AppendLine($"{header}:"); 62 | 63 | string fmt = formatted ? " " : ""; 64 | 65 | b.AppendLineSpace(fmt + $"BB Model: {BBModelString}"); 66 | b.AppendLineSpace(fmt + $"BBID: 0x{BBID:X}"); 67 | b.AppendLineSpace(fmt + $"Timestamp: {TimestampDateTime} ({Timestamp})"); 68 | b.AppendLineSpace(fmt + $"SecureContentId: {SecureContentId}"); 69 | b.AppendLineSpace(fmt + $"CrlVersion: {CrlVersion}"); 70 | 71 | return b.ToString(); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /iQueTool/Structs/iQueSysAppSigArea.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Text; 4 | 5 | namespace iQueTool.Structs 6 | { 7 | // SK = secure kernel? 8 | // SA = system app? 9 | // SK format: 10 | // 0x0 - 0x10000: unk (kernel?) 11 | // 0x10000 - 0x14000: SA1 sigarea 12 | // 0x14000 - (0x14000 + Signature.ContentSize): SA1 13 | // (0x14000 + Signature.ContentSize) - (0x14000 + Signature.ContentSize + 0x4000): SA2 sigarea 14 | // (0x14000 + Signature.ContentSize + 0x4000) - EOF: SA2 15 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 16 | public struct iQueSysAppSigArea 17 | { 18 | public BbContentMetadataHead ContentMetadata; 19 | public BbRsaCert Certificate; 20 | public BbRsaCert Authority; 21 | 22 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x38)] 23 | public byte[] Unk8CC; 24 | 25 | public uint RevocationAddr; 26 | public uint RevocationNameAddr; 27 | public uint AuthorityAddr; 28 | 29 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x10)] 30 | public byte[] Unk910; 31 | 32 | public BbCrlHead Revocation; 33 | 34 | public bool IsValid 35 | { 36 | get 37 | { 38 | return AuthorityAddr == 0x53C; // AuthorityAddr seems to always be 0x53C 39 | } 40 | } 41 | 42 | public void EndianSwap() 43 | { 44 | ContentMetadata.EndianSwap(); 45 | Certificate.EndianSwap(); 46 | Authority.EndianSwap(); 47 | 48 | RevocationAddr = RevocationAddr.EndianSwap(); 49 | RevocationNameAddr = RevocationNameAddr.EndianSwap(); 50 | AuthorityAddr = AuthorityAddr.EndianSwap(); 51 | 52 | Revocation.EndianSwap(); 53 | } 54 | 55 | public byte[] GetBytes() 56 | { 57 | EndianSwap(); // back to device endian (BE) 58 | byte[] bytes = Shared.StructToBytes(this); 59 | EndianSwap(); // back to native endian (LE) 60 | return bytes; 61 | } 62 | 63 | public override string ToString() 64 | { 65 | return ToString(false); 66 | } 67 | 68 | public string ToString(bool formatted, string header = "iQueSysAppSigArea") 69 | { 70 | var b = new StringBuilder(); 71 | if (!string.IsNullOrEmpty(header)) 72 | b.AppendLine($"{header}:"); 73 | 74 | string fmt = formatted ? " " : ""; 75 | 76 | b.AppendLineSpace(fmt + $"RevocationAddr: 0x{RevocationAddr:X}"); 77 | b.AppendLineSpace(fmt + $"RevocationNameAddr: 0x{RevocationNameAddr:X}"); 78 | b.AppendLineSpace(fmt + $"AuthorityAddr: 0x{AuthorityAddr:X}"); 79 | 80 | b.AppendLine(); 81 | b.AppendLineSpace(fmt + "Unk8CC:" + Environment.NewLine + fmt + Unk8CC.ToHexString()); 82 | 83 | b.AppendLine(); 84 | b.AppendLineSpace(fmt + "Unk910:" + Environment.NewLine + fmt + Unk910.ToHexString()); 85 | 86 | b.AppendLine(); 87 | b.AppendLine(ContentMetadata.ToString(formatted, header + ".BbContentMetadataHead")); 88 | b.AppendLine(); 89 | b.AppendLine(Certificate.ToString(formatted, header + ".iQueCertificate")); 90 | b.AppendLine(); 91 | b.AppendLine(Authority.ToString(formatted, header + ".iQueCertificate (Authority)")); 92 | b.AppendLine(); 93 | b.AppendLine(Revocation.ToString(formatted, header + ".iQueCertificateRevocation")); 94 | b.AppendLine(); 95 | 96 | return b.ToString(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /iQueTool/Structs/iQueUserData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace iQueTool.Structs 8 | { 9 | // set via iQue Club app? (was removed in later sysupdates and replaced with iQue Club inside iQue@Home afaik) 10 | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)] 11 | public struct iQueUserData 12 | { 13 | public int Unk0; // always 1? 14 | 15 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x82)] 16 | public byte[] Unk4; 17 | 18 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x2C)] 19 | public char[] Data86; // password maybe? 20 | 21 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x9F)] 22 | public char[] DataB2; 23 | 24 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x11)] 25 | public char[] Data151; // phone #? 26 | 27 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0xF)] 28 | public char[] Data162; // name? 29 | 30 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x7)] 31 | public char[] Data171; 32 | 33 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x5A)] 34 | public char[] Data178; 35 | 36 | public char Data1D2; // seen 2 and 0 37 | 38 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x3E2D)] 39 | public byte[] Unk1D3; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /iQueTool/iQueTool.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {BDE052C7-ADF7-4B11-B142-D33AF0CCFCF7} 8 | Exe 9 | iQueTool 10 | iQueTool 11 | v4.0 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\packages\NDesk.Options.0.2.1\lib\NDesk.Options.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | True 55 | True 56 | Resources.resx 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ResXFileCodeGenerator 80 | Resources.Designer.cs 81 | Designer 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /iQueTool/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------