diff --git a/DotnetCliVersion.txt b/DotnetCliVersion.txt index 33ba87ed..c4d6f4ec 100644 --- a/DotnetCliVersion.txt +++ b/DotnetCliVersion.txt @@ -1 +1 @@ -2.1.500 \ No newline at end of file +2.2.103 \ No newline at end of file diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 00000000..2ad71d6b --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,5 @@ +mode: ContinuousDeployment +increment: Patch +branches: + master: + tag: alpha \ No newline at end of file diff --git a/README.md b/README.md index c8ccab69..700ae7d5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # LibHac +[![NuGet](https://img.shields.io/nuget/v/LibHac.svg?style=flat-square)](https://www.nuget.org/packages/LibHac) +[![MyGet](https://img.shields.io/myget/libhac/vpre/libhac.svg?label=myget&style=flat-square)](https://www.myget.org/feed/libhac/package/nuget/LibHac) +[![AppVeyor Build Status](https://img.shields.io/appveyor/ci/thealexbarney/LibHac/master.svg?style=flat-square)](https://ci.appveyor.com/project/Thealexbarney/libhac/history) + LibHac is a .NET and .NET Core library for opening, decrypting and extracting common content file formats used by the Nintendo Switch. Most content is imported and exported using a standard `IStorage` interface. This means that reading nested file types and encryptions can easily be done by linking different file readers together. @@ -112,4 +116,4 @@ Console-unique keys can be loaded from a text file by specifying a filename with Title keys can be loaded from a text file by specifying a filename with the `--titlekeys` argument. The file should contain one key per line in the form `rights_id,HEXADECIMALKEY`. -If a keyfile is not set at the command line, hactoolnet will search for and load keyfiles in `$HOME/.switch/prod.keys`, `$HOME/.switch/console.keys` and `$HOME/.switch/title.keys`. \ No newline at end of file +If a keyfile is not set at the command line, hactoolnet will search for and load keyfiles in `$HOME/.switch/prod.keys`, `$HOME/.switch/console.keys` and `$HOME/.switch/title.keys`. diff --git a/appveyor.yml b/appveyor.yml index 7d189dee..6a211e2b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,8 @@ version: 0.1.3-{build} image: Visual Studio 2017 +environment: + myget_api_key: + secure: 0xJoYAtR6psXCRvk1qm5czDObkeRjHKPjfe5gIExXVFPwA0VVODYv/hBZYUtz2F3 build_script: - ps: .\build.ps1 test: off \ No newline at end of file diff --git a/build.ps1 b/build.ps1 index 304207eb..5cc0b61e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,7 @@ [CmdletBinding()] Param( #[switch]$CustomParam, + [switch]$BuildDotnetCoreOnly, [Parameter(Position = 0, Mandatory = $false, ValueFromRemainingArguments = $true)] [string[]]$BuildArguments ) @@ -43,14 +44,17 @@ try { if (Test-Path $DotNetGlobalFile) { $DotNetVersion = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json).sdk.version } + + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + # If dotnet is installed locally, and expected version is not set or installation matches the expected version if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` (!(Test-Path variable:DotNetVersion) -or $(& cmd.exe /c 'dotnet --version 2>&1') -eq $DotNetVersion)) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } - else { - $DotNetDirectory = "$TempDirectory\dotnet-win" - $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + elseif ($null -eq (Get-Command $env:DOTNET_EXE -ErrorAction SilentlyContinue) -and ` + (!(Test-Path variable:DotNetVersion) -or $(& cmd.exe /c '$env:DOTNET_EXE --version 2>&1') -ne $DotNetVersion)) { # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" @@ -68,8 +72,16 @@ try { Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" + if($BuildDotnetCoreOnly) { + $BuildArguments += "-DoCoreBuildOnly" + $BuildArguments += "true" + } + ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } } +catch { + Write-Output $_.Exception.Message +} finally { if (Test-Path $DotNetGlobalFile) { Remove-Item $DotNetGlobalFile diff --git a/build.sh b/build.sh index 63edfcd9..ed21297c 100644 --- a/build.sh +++ b/build.sh @@ -3,7 +3,7 @@ echo $(bash --version 2>&1 | head -n 1) #CUSTOMPARAM=0 -BUILD_ARGUMENTS=() +BUILD_ARGUMENTS=("-DoCoreBuildOnly") for i in "$@"; do case $(echo $1 | awk '{print tolower($0)}') in # -custom-param) CUSTOMPARAM=1;; @@ -37,30 +37,34 @@ export NUGET_XMLDOC_MODE="skip" function FirstJsonValue { perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2} } -trap "rm -f $DOTNET_GLOBAL_FILE" INT TERM EXIT +trap "rm -f \"$DOTNET_GLOBAL_FILE\"" INT TERM EXIT dotnetCliVersion=$(cat DotnetCliVersion.txt) json="{\"sdk\":{\"version\":\"$dotnetCliVersion\"}}" - echo "$json" > $DOTNET_GLOBAL_FILE + echo "$json" > "$DOTNET_GLOBAL_FILE" # If global.json exists, load expected version if [ -f "$DOTNET_GLOBAL_FILE" ]; then DOTNET_VERSION=$dotnetCliVersion fi +DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" +export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + # If dotnet is installed locally, and expected version is not set or installation matches the expected version if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") ]]; then export DOTNET_EXE="$(command -v dotnet)" -else - DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" - export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" - +elif [[ ! (-x "$DOTNET_EXE" && (-z ${DOTNET_VERSION+x} || $($DOTNET_EXE --version) == "$DOTNET_VERSION")) ]]; then + # Download install script DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" mkdir -p "$TEMP_DIRECTORY" - curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" - chmod +x "$DOTNET_INSTALL_FILE" + + if [ ! -x "$DOTNET_INSTALL_FILE" ]; then + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + fi # Install by channel or version if [ -z ${DOTNET_VERSION+x} ]; then diff --git a/build/Build.cs b/build/Build.cs index 1cacbe25..ec8a469c 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -4,265 +4,554 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; using ICSharpCode.SharpZipLib.Zip; using ILRepacking; using Nuke.Common; +using Nuke.Common.BuildServers; using Nuke.Common.Git; using Nuke.Common.ProjectModel; using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Tools.SignTool; using static Nuke.Common.IO.FileSystemTasks; using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.Tools.DotNet.DotNetTasks; -class Build : NukeBuild +namespace LibHacBuild { - public static int Main() => Execute(x => x.Results); + partial class Build : NukeBuild + { + public static int Main() => Execute(x => x.Results); - [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] - readonly string Configuration = IsLocalBuild ? "Debug" : "Release"; + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly string Configuration = IsLocalBuild ? "Debug" : "Release"; - [Solution("LibHac.sln")] readonly Solution Solution; - [GitRepository] readonly GitRepository GitRepository; + [Parameter("Build only .NET Core targets if true. Default is false on Windows")] + readonly bool DoCoreBuildOnly; - AbsolutePath SourceDirectory => RootDirectory / "src"; - AbsolutePath TestsDirectory => RootDirectory / "tests"; - AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; - AbsolutePath TempDirectory => RootDirectory / ".tmp"; - AbsolutePath CliCoreDir => TempDirectory / "hactoolnet_netcoreapp2.1"; - AbsolutePath CliFrameworkDir => TempDirectory / "hactoolnet_net46"; - AbsolutePath CliFrameworkZip => ArtifactsDirectory / "hactoolnet.zip"; - AbsolutePath CliCoreZip => ArtifactsDirectory / "hactoolnet_netcore.zip"; + [Solution("LibHac.sln")] readonly Solution Solution; + [GitRepository] readonly GitRepository GitRepository; + [GitVersion] readonly GitVersion GitVersion; - AbsolutePath CliMergedExe => ArtifactsDirectory / "hactoolnet.exe"; + AbsolutePath SourceDirectory => RootDirectory / "src"; + AbsolutePath TestsDirectory => RootDirectory / "tests"; + AbsolutePath ArtifactsDirectory => RootDirectory / "artifacts"; + AbsolutePath SignedArtifactsDirectory => ArtifactsDirectory / "signed"; + AbsolutePath TempDirectory => RootDirectory / ".tmp"; + AbsolutePath CliCoreDir => TempDirectory / "hactoolnet_netcoreapp2.1"; + AbsolutePath CliFrameworkDir => TempDirectory / "hactoolnet_net46"; + AbsolutePath CliFrameworkZip => ArtifactsDirectory / "hactoolnet.zip"; + AbsolutePath CliCoreZip => ArtifactsDirectory / "hactoolnet_netcore.zip"; - Project LibHacProject => Solution.GetProject("LibHac").NotNull(); - Project LibHacTestProject => Solution.GetProject("LibHac.Tests").NotNull(); - Project HactoolnetProject => Solution.GetProject("hactoolnet").NotNull(); + AbsolutePath CliMergedExe => ArtifactsDirectory / "hactoolnet.exe"; - Target Clean => _ => _ - .Executes(() => - { - DeleteDirectories(GlobDirectories(SourceDirectory, "**/bin", "**/obj")); - DeleteDirectories(GlobDirectories(TestsDirectory, "**/bin", "**/obj")); - EnsureCleanDirectory(ArtifactsDirectory); - EnsureCleanDirectory(CliCoreDir); - EnsureCleanDirectory(CliFrameworkDir); - }); + Project LibHacProject => Solution.GetProject("LibHac").NotNull(); + Project LibHacTestProject => Solution.GetProject("LibHac.Tests").NotNull(); + Project HactoolnetProject => Solution.GetProject("hactoolnet").NotNull(); - Target Restore => _ => _ - .DependsOn(Clean) - .Executes(() => - { - DotNetRestoreSettings settings = new DotNetRestoreSettings() - .SetProjectFile(Solution); + string AppVeyorVersion { get; set; } + Dictionary VersionProps { get; set; } = new Dictionary(); - if (EnvironmentInfo.IsUnix) settings = settings.RemoveRuntimes("net46"); + const string CertFileName = "cert.pfx"; - DotNetRestore(s => settings); - }); - - Target Compile => _ => _ - .DependsOn(Restore) - .Executes(() => - { - DotNetBuildSettings buildSettings = new DotNetBuildSettings() - .SetProjectFile(Solution) - .EnableNoRestore() - .SetConfiguration(Configuration); - - if (EnvironmentInfo.IsUnix) buildSettings = buildSettings.SetFramework("netcoreapp2.1"); - - DotNetBuild(s => buildSettings); - - DotNetPublishSettings publishSettings = new DotNetPublishSettings() - .EnableNoRestore() - .SetConfiguration(Configuration); - - DotNetPublish(s => publishSettings - .SetProject(HactoolnetProject) - .SetFramework("netcoreapp2.1") - .SetOutput(CliCoreDir)); - - if (EnvironmentInfo.IsWin) + Target SetVersion => _ => _ + .OnlyWhenStatic(() => GitRepository != null) + .Executes(() => { + AppVeyorVersion = $"{GitVersion.AssemblySemVer}"; + if (!string.IsNullOrWhiteSpace(GitVersion.PreReleaseTag)) + { + AppVeyorVersion += $"-{GitVersion.PreReleaseTag}+{GitVersion.Sha.Substring(0, 8)}"; + } + + string suffix = GitVersion.PreReleaseTag; + + if (!string.IsNullOrWhiteSpace(suffix)) + { + if (!GitRepository.IsOnMasterBranch()) + { + suffix = $"-{suffix}"; + } + + suffix += $"+{GitVersion.Sha.Substring(0, 8)}"; + } + + VersionProps = new Dictionary + { + ["VersionPrefix"] = GitVersion.AssemblySemVer, + ["VersionSuffix"] = suffix + }; + + Console.WriteLine($"Building version {AppVeyorVersion}"); + + if (Host == HostType.AppVeyor) + { + SetAppVeyorVersion(AppVeyorVersion); + } + }); + + Target Clean => _ => _ + .Executes(() => + { + DeleteDirectories(GlobDirectories(SourceDirectory, "**/bin", "**/obj")); + DeleteDirectories(GlobDirectories(TestsDirectory, "**/bin", "**/obj")); + EnsureCleanDirectory(ArtifactsDirectory); + EnsureCleanDirectory(CliCoreDir); + EnsureCleanDirectory(CliFrameworkDir); + }); + + Target Restore => _ => _ + .DependsOn(Clean) + .Executes(() => + { + DotNetRestoreSettings settings = new DotNetRestoreSettings() + .SetProjectFile(Solution); + + DotNetRestore(s => settings); + }); + + Target Compile => _ => _ + .DependsOn(Restore, SetVersion) + .Executes(() => + { + DotNetBuildSettings buildSettings = new DotNetBuildSettings() + .SetProjectFile(Solution) + .EnableNoRestore() + .SetConfiguration(Configuration) + .SetProperties(VersionProps); + + if (DoCoreBuildOnly) buildSettings = buildSettings.SetFramework("netcoreapp2.1"); + + DotNetBuild(s => buildSettings); + + DotNetPublishSettings publishSettings = new DotNetPublishSettings() + .EnableNoRestore() + .SetConfiguration(Configuration); + DotNetPublish(s => publishSettings .SetProject(HactoolnetProject) - .SetFramework("net46") - .SetOutput(CliFrameworkDir)); - } + .SetFramework("netcoreapp2.1") + .SetOutput(CliCoreDir) + .SetProperties(VersionProps)); - // Hack around OS newline differences - if (EnvironmentInfo.IsUnix) - { - foreach (string filename in Directory.EnumerateFiles(CliCoreDir, "*.json")) + if (!DoCoreBuildOnly) { - ReplaceLineEndings(filename); + DotNetPublish(s => publishSettings + .SetProject(HactoolnetProject) + .SetFramework("net46") + .SetOutput(CliFrameworkDir) + .SetProperties(VersionProps)); } - } - }); - Target Pack => _ => _ - .DependsOn(Compile) - .Executes(() => - { - DotNetPackSettings settings = new DotNetPackSettings() - .SetProject(LibHacProject) - .EnableNoBuild() - .SetConfiguration(Configuration) - .EnableIncludeSymbols() - .SetOutputDirectory(ArtifactsDirectory); - - if (EnvironmentInfo.IsUnix) - settings = settings.SetProperties( - new Dictionary { ["TargetFrameworks"] = "netcoreapp2.1" }); - - DotNetPack(s => settings); - - if (Host != HostType.AppVeyor) return; - - foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.nupkg")) - { - PushArtifact(filename); - } - }); - - Target Merge => _ => _ - .DependsOn(Compile) - .OnlyWhen(() => EnvironmentInfo.IsWin) - .Executes(() => - { - string[] libraries = Directory.GetFiles(CliFrameworkDir, "*.dll"); - var cliList = new List { CliFrameworkDir / "hactoolnet.exe" }; - cliList.AddRange(libraries); - - var cliOptions = new RepackOptions - { - OutputFile = CliMergedExe, - InputAssemblies = cliList.ToArray(), - SearchDirectories = new[] { "." } - }; - - new ILRepack(cliOptions).Repack(); - - if (Host == HostType.AppVeyor) - { - PushArtifact(CliMergedExe); - } - }); - - Target Test => _ => _ - .DependsOn(Compile) - .Executes(() => - { - DotNetTestSettings settings = new DotNetTestSettings() - .SetProjectFile(LibHacTestProject) - .EnableNoBuild() - .SetConfiguration(Configuration); - - if (EnvironmentInfo.IsUnix) settings = settings.SetFramework("netcoreapp2.1"); - - DotNetTest(s => settings); - }); - - Target Zip => _ => _ - .DependsOn(Pack) - .Executes(() => - { - string[] namesFx = Directory.EnumerateFiles(CliFrameworkDir, "*.exe") - .Concat(Directory.EnumerateFiles(CliFrameworkDir, "*.dll")) - .ToArray(); - - string[] namesCore = Directory.EnumerateFiles(CliCoreDir, "*.json") - .Concat(Directory.EnumerateFiles(CliCoreDir, "*.dll")) - .ToArray(); - - if (EnvironmentInfo.IsWin) - { - ZipFiles(CliFrameworkZip, namesFx); - Console.WriteLine($"Created {CliFrameworkZip}"); - } - - ZipFiles(CliCoreZip, namesCore); - Console.WriteLine($"Created {CliCoreZip}"); - - if (Host == HostType.AppVeyor) - { - PushArtifact(CliFrameworkZip); - PushArtifact(CliCoreZip); - PushArtifact(CliMergedExe); - } - }); - - Target Results => _ => _ - .DependsOn(Test, Zip, Merge) - .Executes(() => - { - Console.WriteLine("SHA-1:"); - using (SHA1 sha = SHA1.Create()) - { - foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory)) + // Hack around OS newline differences + if (EnvironmentInfo.IsUnix) { - using (var stream = new FileStream(filename, FileMode.Open)) + foreach (string filename in Directory.EnumerateFiles(CliCoreDir, "*.json")) { - string hash = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", ""); - Console.WriteLine($"{hash} - {Path.GetFileName(filename)}"); + ReplaceLineEndings(filename); + } + } + }); + + Target Pack => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetPackSettings settings = new DotNetPackSettings() + .SetProject(LibHacProject) + .EnableNoBuild() + .SetConfiguration(Configuration) + .EnableIncludeSymbols() + .SetSymbolPackageFormat(DotNetSymbolPackageFormat.snupkg) + .SetOutputDirectory(ArtifactsDirectory) + .SetProperties(VersionProps); + + if (DoCoreBuildOnly) + settings = settings.SetProperty("TargetFrameworks", "netcoreapp2.1"); + + DotNetPack(s => settings); + + foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.*nupkg")) + { + RepackNugetPackage(filename); + } + + if (Host != HostType.AppVeyor) return; + + foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory, "*.*nupkg")) + { + PushArtifact(filename); + } + }); + + Target Merge => _ => _ + .DependsOn(Compile) + .OnlyWhenStatic(() => !DoCoreBuildOnly) + .Executes(() => + { + string[] libraries = Directory.GetFiles(CliFrameworkDir, "*.dll"); + var cliList = new List { CliFrameworkDir / "hactoolnet.exe" }; + cliList.AddRange(libraries); + + var cliOptions = new RepackOptions + { + OutputFile = CliMergedExe, + InputAssemblies = cliList.ToArray(), + SearchDirectories = new[] { "." } + }; + + new ILRepack(cliOptions).Repack(); + + foreach (AbsolutePath file in ArtifactsDirectory.GlobFiles("*.exe.config")) + { + File.Delete(file); + } + + if (Host == HostType.AppVeyor) + { + PushArtifact(CliMergedExe); + } + }); + + Target Test => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetTestSettings settings = new DotNetTestSettings() + .SetProjectFile(LibHacTestProject) + .EnableNoBuild() + .SetConfiguration(Configuration); + + if (DoCoreBuildOnly) settings = settings.SetFramework("netcoreapp2.1"); + + DotNetTest(s => settings); + }); + + Target Zip => _ => _ + .DependsOn(Pack) + .Executes(() => + { + string[] namesFx = Directory.EnumerateFiles(CliFrameworkDir, "*.exe") + .Concat(Directory.EnumerateFiles(CliFrameworkDir, "*.dll")) + .ToArray(); + + string[] namesCore = Directory.EnumerateFiles(CliCoreDir, "*.json") + .Concat(Directory.EnumerateFiles(CliCoreDir, "*.dll")) + .ToArray(); + + if (!DoCoreBuildOnly) + { + ZipFiles(CliFrameworkZip, namesFx); + Console.WriteLine($"Created {CliFrameworkZip}"); + } + + ZipFiles(CliCoreZip, namesCore); + Console.WriteLine($"Created {CliCoreZip}"); + + if (Host == HostType.AppVeyor) + { + PushArtifact(CliFrameworkZip); + PushArtifact(CliCoreZip); + PushArtifact(CliMergedExe); + } + }); + + Target Publish => _ => _ + .DependsOn(Test) + .OnlyWhenStatic(() => Host == HostType.AppVeyor) + .OnlyWhenStatic(() => AppVeyor.Instance.PullRequestTitle == null) + .Executes(() => + { + AbsolutePath nupkgFile = ArtifactsDirectory.GlobFiles("*.nupkg").Single(); + AbsolutePath snupkgFile = ArtifactsDirectory.GlobFiles("*.snupkg").Single(); + + string apiKey = EnvironmentInfo.Variable("myget_api_key"); + DotNetNuGetPushSettings settings = new DotNetNuGetPushSettings() + .SetApiKey(apiKey) + .SetSymbolApiKey(apiKey) + .SetSource("https://www.myget.org/F/libhac/api/v2/package") + .SetSymbolSource("https://www.myget.org/F/libhac/symbols/api/v2/package"); + + DotNetNuGetPush(settings.SetTargetPath(nupkgFile)); + DotNetNuGetPush(settings.SetTargetPath(snupkgFile)); + }); + + Target Results => _ => _ + .DependsOn(Test, Zip, Merge, Sign, Publish) + .Executes(() => + { + Console.WriteLine("SHA-1:"); + using (SHA1 sha = SHA1.Create()) + { + foreach (string filename in Directory.EnumerateFiles(ArtifactsDirectory)) + { + using (var stream = new FileStream(filename, FileMode.Open)) + { + string hash = BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", ""); + Console.WriteLine($"{hash} - {Path.GetFileName(filename)}"); + } + } + } + }); + + Target Sign => _ => _ + .DependsOn(Test, Zip, Merge) + .OnlyWhenStatic(() => !DoCoreBuildOnly) + .OnlyWhenStatic(() => File.Exists(CertFileName)) + .Executes(() => + { + string pwd = ReadPassword(); + + if (pwd == string.Empty) + { + Console.WriteLine("Skipping sign task"); + return; + } + + SignAndReZip(pwd); + }); + + public static void ZipFiles(string outFile, IEnumerable files) + { + using (var s = new ZipOutputStream(File.Create(outFile))) + { + s.SetLevel(9); + + foreach (string file in files) + { + var entry = new ZipEntry(Path.GetFileName(file)); + entry.DateTime = DateTime.UnixEpoch; + + using (FileStream fs = File.OpenRead(file)) + { + entry.Size = fs.Length; + s.PutNextEntry(entry); + fs.CopyTo(s); } } } - }); + } - public static void ZipFiles(string outFile, string[] files) - { - using (var s = new ZipOutputStream(File.Create(outFile))) + public static void ZipDirectory(string outFile, string directory) { - s.SetLevel(9); - - foreach (string file in files) + using (var s = new ZipOutputStream(File.Create(outFile))) { - var entry = new ZipEntry(Path.GetFileName(file)); - entry.DateTime = DateTime.UnixEpoch; + s.SetLevel(9); - using (FileStream fs = File.OpenRead(file)) + foreach (string filePath in Directory.EnumerateFiles(directory, "*", SearchOption.AllDirectories)) { - entry.Size = fs.Length; - s.PutNextEntry(entry); - fs.CopyTo(s); + string relativePath = Path.GetRelativePath(directory, filePath); + + var entry = new ZipEntry(relativePath); + entry.DateTime = DateTime.UnixEpoch; + + using (FileStream fs = File.OpenRead(filePath)) + { + entry.Size = fs.Length; + s.PutNextEntry(entry); + fs.CopyTo(s); + } } } } - } - public static void PushArtifact(string path) - { - if (!File.Exists(path)) + public static void ZipDirectory(string outFile, string directory, IEnumerable files) { - Console.WriteLine($"Unable to add artifact {path}"); + using (var s = new ZipOutputStream(File.Create(outFile))) + { + s.SetLevel(9); + + foreach (string filePath in files) + { + string absolutePath = Path.Combine(directory, filePath); + + var entry = new ZipEntry(filePath); + entry.DateTime = DateTime.UnixEpoch; + + using (FileStream fs = File.OpenRead(absolutePath)) + { + entry.Size = fs.Length; + s.PutNextEntry(entry); + fs.CopyTo(s); + } + } + } } - var psi = new ProcessStartInfo + public static void UnzipFiles(string zipFile, string outDir) { - FileName = "appveyor", - Arguments = $"PushArtifact \"{path}\"", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }; + using (var s = new ZipInputStream(File.OpenRead(zipFile))) + { + ZipEntry entry; + while ((entry = s.GetNextEntry()) != null) + { + string outPath = Path.Combine(outDir, entry.Name); - var proc = new Process + string directoryName = Path.GetDirectoryName(outPath); + string fileName = Path.GetFileName(outPath); + + if (!string.IsNullOrWhiteSpace(directoryName)) + { + Directory.CreateDirectory(directoryName); + } + + if (!string.IsNullOrWhiteSpace(fileName)) + { + using (FileStream outFile = File.Create(outPath)) + { + s.CopyTo(outFile); + } + } + } + } + } + + public static void PushArtifact(string path) { - StartInfo = psi - }; + if (!File.Exists(path)) + { + Console.WriteLine($"Unable to add artifact {path}"); + } - proc.Start(); + var psi = new ProcessStartInfo + { + FileName = "appveyor", + Arguments = $"PushArtifact \"{path}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; - proc.WaitForExit(); + var proc = new Process + { + StartInfo = psi + }; - Console.WriteLine($"Added AppVeyor artifact {path}"); - } + proc.Start(); - public static void ReplaceLineEndings(string filename) - { - string text = File.ReadAllText(filename); - File.WriteAllText(filename, text.Replace("\n", "\r\n")); + proc.WaitForExit(); + + Console.WriteLine($"Added AppVeyor artifact {path}"); + } + + public static void SetAppVeyorVersion(string version) + { + var psi = new ProcessStartInfo + { + FileName = "appveyor", + Arguments = $"UpdateBuild -Version \"{version}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + var proc = new Process + { + StartInfo = psi + }; + + proc.Start(); + + proc.WaitForExit(); + } + + public static void ReplaceLineEndings(string filename) + { + string text = File.ReadAllText(filename); + File.WriteAllText(filename, Regex.Replace(text, @"\r\n|\n\r|\n|\r", "\r\n")); + } + + public static void SignAssemblies(string password, params string[] fileNames) + { + SignToolSettings settings = new SignToolSettings() + .SetFileDigestAlgorithm("SHA256") + .SetFile(CertFileName) + .SetFiles(fileNames) + .SetPassword(password) + .SetTimestampServerDigestAlgorithm("SHA256") + .SetRfc3161TimestampServerUrl("http://timestamp.digicert.com"); + + SignToolTasks.SignTool(settings); + } + + public void SignAndReZip(string password) + { + AbsolutePath nupkgFile = ArtifactsDirectory.GlobFiles("*.nupkg").Single(); + AbsolutePath snupkgFile = ArtifactsDirectory.GlobFiles("*.snupkg").Single(); + AbsolutePath nupkgDir = TempDirectory / ("sign_" + Path.GetFileName(nupkgFile)); + AbsolutePath netFxDir = TempDirectory / ("sign_" + Path.GetFileName(CliFrameworkZip)); + AbsolutePath coreFxDir = TempDirectory / ("sign_" + Path.GetFileName(CliCoreZip)); + AbsolutePath signedMergedExe = SignedArtifactsDirectory / Path.GetFileName(CliMergedExe); + + try + { + UnzipFiles(CliFrameworkZip, netFxDir); + UnzipFiles(CliCoreZip, coreFxDir); + List pkgFileList = UnzipPackage(nupkgFile, nupkgDir); + + var toSign = new List(); + toSign.AddRange(nupkgDir.GlobFiles("**/LibHac.dll")); + toSign.Add(netFxDir / "hactoolnet.exe"); + toSign.Add(coreFxDir / "hactoolnet.dll"); + toSign.Add(signedMergedExe); + + Directory.CreateDirectory(SignedArtifactsDirectory); + File.Copy(CliMergedExe, signedMergedExe, true); + + SignAssemblies(password, toSign.Select(x => x.ToString()).ToArray()); + + // Avoid having multiple signed versions of the same file + File.Copy(nupkgDir / "lib" / "net46" / "LibHac.dll", netFxDir / "LibHac.dll", true); + File.Copy(nupkgDir / "lib" / "netcoreapp2.1" / "LibHac.dll", coreFxDir / "LibHac.dll", true); + + ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(nupkgFile), nupkgDir, pkgFileList); + ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(CliFrameworkZip), netFxDir); + ZipDirectory(SignedArtifactsDirectory / Path.GetFileName(CliCoreZip), coreFxDir); + + File.Copy(snupkgFile, SignedArtifactsDirectory / Path.GetFileName(snupkgFile)); + + SignNupkg(SignedArtifactsDirectory / Path.GetFileName(nupkgFile), password); + SignNupkg(SignedArtifactsDirectory / Path.GetFileName(snupkgFile), password); + } + catch (Exception) + { + Directory.Delete(SignedArtifactsDirectory, true); + throw; + } + finally + { + Directory.Delete(nupkgDir, true); + Directory.Delete(netFxDir, true); + Directory.Delete(coreFxDir, true); + } + } + + public static string ReadPassword() + { + var pwd = new StringBuilder(); + ConsoleKeyInfo key; + + Console.Write("Enter certificate password (Empty password to skip): "); + do + { + key = Console.ReadKey(true); + + // Ignore any key out of range. + if (((int)key.Key) >= '!' && ((int)key.Key <= '~')) + { + // Append the character to the password. + pwd.Append(key.KeyChar); + Console.Write("*"); + } + + // Exit if Enter key is pressed. + } while (key.Key != ConsoleKey.Enter); + + Console.WriteLine(); + + return pwd.ToString(); + } } } diff --git a/build/RepackNuget.cs b/build/RepackNuget.cs new file mode 100644 index 00000000..942b3340 --- /dev/null +++ b/build/RepackNuget.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Xml.Linq; +using ICSharpCode.SharpZipLib.Zip; +using Nuke.Common.Tools.NuGet; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.IO.PathConstruction; + +namespace LibHacBuild +{ + public partial class Build + { + public void RepackNugetPackage(string path) + { + AbsolutePath tempDir = TempDirectory / Path.GetFileName(path); + AbsolutePath libDir = tempDir / "lib"; + AbsolutePath relsFile = tempDir / "_rels" / ".rels"; + + try + { + EnsureCleanDirectory(tempDir); + List fileList = UnzipPackage(path, tempDir); + + string newPsmdcpName = CalcPsmdcpName(libDir); + string newPsmdcpPath = RenamePsmdcp(tempDir, newPsmdcpName); + EditManifestRelationships(relsFile, newPsmdcpPath); + + int index = fileList.FindIndex(x => x.Contains(".psmdcp")); + fileList[index] = newPsmdcpPath; + + IEnumerable files = Directory.EnumerateFiles(tempDir, "*.json", SearchOption.AllDirectories) + .Concat(Directory.EnumerateFiles(tempDir, "*.xml", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(tempDir, "*.rels", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(tempDir, "*.psmdcp", SearchOption.AllDirectories)) + .Concat(Directory.EnumerateFiles(tempDir, "*.nuspec", SearchOption.AllDirectories)); + + foreach (string filename in files) + { + Console.WriteLine(filename); + ReplaceLineEndings(filename); + } + + ZipDirectory(path, tempDir, fileList); + } + finally + { + Directory.Delete(tempDir, true); + } + } + + public List UnzipPackage(string package, string dest) + { + var fileList = new List(); + + UnzipFiles(package, dest); + + using (var s = new ZipInputStream(File.OpenRead(package))) + { + ZipEntry entry; + while ((entry = s.GetNextEntry()) != null) + { + fileList.Add(entry.Name); + } + } + + return fileList; + } + + public static string CalcPsmdcpName(string libDir) + { + using (SHA256 sha = SHA256.Create()) + { + foreach (string file in Directory.EnumerateFiles(libDir)) + { + byte[] data = File.ReadAllBytes(file); + sha.TransformBlock(data, 0, data.Length, data, 0); + } + + sha.TransformFinalBlock(new byte[0], 0, 0); + + return ToHexString(sha.Hash).ToLower().Substring(0, 32); + } + } + + public static string RenamePsmdcp(string packageDir, string name) + { + string fileName = Directory.EnumerateFiles(packageDir, "*.psmdcp", SearchOption.AllDirectories).Single(); + string newFileName = Path.Combine(Path.GetDirectoryName(fileName), name + ".psmdcp"); + Directory.Move(fileName, newFileName); + + return Path.GetRelativePath(packageDir, newFileName).Replace('\\', '/'); + } + + [SuppressMessage("ReSharper", "PossibleNullReferenceException")] + public void EditManifestRelationships(string path, string psmdcpPath) + { + XDocument doc = XDocument.Load(path); + XNamespace ns = doc.Root.GetDefaultNamespace(); + + foreach (XElement rs in doc.Root.Elements(ns + "Relationship")) + { + using (SHA256 sha = SHA256.Create()) + { + if (rs.Attribute("Target").Value.Contains(".psmdcp")) + { + rs.Attribute("Target").Value = "/" + psmdcpPath; + } + + string s = "/" + psmdcpPath + rs.Attribute("Target").Value; + byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(s)); + string id = "R" + ToHexString(hash).Substring(0, 16); + rs.Attribute("Id").Value = id; + } + } + + doc.Save(path); + } + + public void SignNupkg(string pkgPath, string password) + { + NuGetTasks.NuGet( + $"sign \"{pkgPath}\" -CertificatePath cert.pfx -CertificatePassword {password} -Timestamper http://timestamp.digicert.com", + outputFilter: x => x.Replace(password, "hunter2")); + } + + public static string ToHexString(byte[] arr) + { + return BitConverter.ToString(arr).ToLower().Replace("-", ""); + } + } +} diff --git a/build/_build.csproj b/build/_build.csproj index 1a225dc8..e50e0e1a 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -4,15 +4,17 @@ Exe netcoreapp2.1 false - + LibHacBuild False CS0649;CS0169 - - + + + + diff --git a/src/LibHac.Nand/FatFileSystemProvider.cs b/src/LibHac.Nand/FatFileSystemProvider.cs index 9a1e9f9f..e7539111 100644 --- a/src/LibHac.Nand/FatFileSystemProvider.cs +++ b/src/LibHac.Nand/FatFileSystemProvider.cs @@ -45,7 +45,7 @@ namespace LibHac.Nand { path = ToDiscUtilsPath(PathTools.Normalize(path)); - if (path == @"\\") return true; + if (path == @"\") return true; return Fs.DirectoryExists(path); } diff --git a/src/LibHac.Nand/Nand.cs b/src/LibHac.Nand/Nand.cs index ae02b956..5ef87f82 100644 --- a/src/LibHac.Nand/Nand.cs +++ b/src/LibHac.Nand/Nand.cs @@ -12,6 +12,7 @@ namespace LibHac.Nand { private GuidPartitionInfo ProdInfo { get; } private GuidPartitionInfo ProdInfoF { get; } + private GuidPartitionInfo[] Package2 { get; } private GuidPartitionInfo Safe { get; } private GuidPartitionInfo System { get; } private GuidPartitionInfo User { get; } @@ -23,6 +24,15 @@ namespace LibHac.Nand GuidPartitionInfo[] partitions = disc.Partitions.Select(x => (GuidPartitionInfo)x).ToArray(); ProdInfo = partitions.FirstOrDefault(x => x.Name == "PRODINFO"); ProdInfoF = partitions.FirstOrDefault(x => x.Name == "PRODINFOF"); + Package2 = new[] + { + partitions.FirstOrDefault(x => x.Name == "BCPKG2-1-Normal-Main"), + partitions.FirstOrDefault(x => x.Name == "BCPKG2-2-Normal-Sub"), + partitions.FirstOrDefault(x => x.Name == "BCPKG2-3-SafeMode-Main"), + partitions.FirstOrDefault(x => x.Name == "BCPKG2-4-SafeMode-Sub"), + partitions.FirstOrDefault(x => x.Name == "BCPKG2-5-Repair-Main"), + partitions.FirstOrDefault(x => x.Name == "BCPKG2-6-Repair-Sub") + }; Safe = partitions.FirstOrDefault(x => x.Name == "SAFE"); System = partitions.FirstOrDefault(x => x.Name == "SYSTEM"); User = partitions.FirstOrDefault(x => x.Name == "USER"); @@ -44,6 +54,11 @@ namespace LibHac.Nand return new FatFileSystemProvider(fat); } + public IStorage OpenPackage2(int index) + { + return Package2[index].Open().AsStorage().AsReadOnly(); + } + public FatFileSystemProvider OpenSafePartition() { IStorage encStorage = Safe.Open().AsStorage(); @@ -68,4 +83,4 @@ namespace LibHac.Nand return new FatFileSystemProvider(fat); } } -} +} \ No newline at end of file diff --git a/src/LibHac/BitReader.cs b/src/LibHac/BitReader.cs index efc3754d..fdd7986a 100644 --- a/src/LibHac/BitReader.cs +++ b/src/LibHac/BitReader.cs @@ -122,7 +122,7 @@ namespace LibHac /// Example: /// A 4-bit offset binary value with a positive bias can store /// the values 8 through -7 inclusive. - /// A 4-bit offset binary value with a positive bias can store + /// A 4-bit offset binary value with a negative bias can store /// the values 7 through -8 inclusive. public enum OffsetBias { diff --git a/src/LibHac/IO/AesXtsDirectory.cs b/src/LibHac/IO/AesXtsDirectory.cs index a8ec03cd..2d5227c7 100644 --- a/src/LibHac/IO/AesXtsDirectory.cs +++ b/src/LibHac/IO/AesXtsDirectory.cs @@ -5,14 +5,16 @@ namespace LibHac.IO { public class AesXtsDirectory : IDirectory { - public IFileSystem ParentFileSystem { get; } + IFileSystem IDirectory.ParentFileSystem => ParentFileSystem; + public AesXtsFileSystem ParentFileSystem { get; } + public string FullPath { get; } public OpenDirectoryMode Mode { get; } private IFileSystem BaseFileSystem { get; } private IDirectory BaseDirectory { get; } - public AesXtsDirectory(IFileSystem parentFs, IDirectory baseDir, OpenDirectoryMode mode) + public AesXtsDirectory(AesXtsFileSystem parentFs, IDirectory baseDir, OpenDirectoryMode mode) { ParentFileSystem = parentFs; BaseDirectory = baseDir; @@ -66,7 +68,7 @@ namespace LibHac.IO if (BitConverter.ToUInt32(buffer, 0) != 0x3058414E) return 0; file.Read(buffer, 0x48); - return BitConverter.ToInt32(buffer, 0); + return BitConverter.ToInt64(buffer, 0); } } catch (ArgumentOutOfRangeException) diff --git a/src/LibHac/IO/AesXtsFile.cs b/src/LibHac/IO/AesXtsFile.cs index c211d755..bc42f831 100644 --- a/src/LibHac/IO/AesXtsFile.cs +++ b/src/LibHac/IO/AesXtsFile.cs @@ -31,10 +31,19 @@ namespace LibHac.IO throw new ArgumentException("NAX0 key derivation failed."); } - IStorage encStorage = BaseFile.AsStorage().Slice(HeaderLength, Header.Size); + IStorage encStorage = BaseFile.AsStorage().Slice(HeaderLength, Util.AlignUp(Header.Size, 0x10)); BaseStorage = new CachedStorage(new Aes128XtsStorage(encStorage, Header.DecryptedKey1, Header.DecryptedKey2, BlockSize, true), 4, true); } + public byte[] GetKey() + { + var key = new byte[0x20]; + Array.Copy(Header.DecryptedKey1, 0, key, 0, 0x10); + Array.Copy(Header.DecryptedKey2, 0, key, 0x10, 0x10); + + return key; + } + public override int Read(Span destination, long offset) { int toRead = ValidateReadParamsAndGetSize(destination, offset); @@ -63,6 +72,8 @@ namespace LibHac.IO public override void SetSize(long size) { + Header.SetSize(size, VerificationKey); + throw new NotImplementedException(); } } diff --git a/src/LibHac/IO/AesXtsFileHeader.cs b/src/LibHac/IO/AesXtsFileHeader.cs index b77767d4..58d8f8d9 100644 --- a/src/LibHac/IO/AesXtsFileHeader.cs +++ b/src/LibHac/IO/AesXtsFileHeader.cs @@ -13,7 +13,7 @@ namespace LibHac.IO public uint Magic { get; } public byte[] EncryptedKey1 { get; } = new byte[0x10]; public byte[] EncryptedKey2 { get; } = new byte[0x10]; - public long Size { get; } + public long Size { get; private set; } public byte[] DecryptedKey1 { get; } = new byte[0x10]; public byte[] DecryptedKey2 { get; } = new byte[0x10]; @@ -37,10 +37,10 @@ namespace LibHac.IO } } - public AesXtsFileHeader(byte[] key1, byte[] key2, long fileSize, string path, byte[] kekSeed, byte[] verificationKey) + public AesXtsFileHeader(byte[] key, long fileSize, string path, byte[] kekSeed, byte[] verificationKey) { - Array.Copy(key1, DecryptedKey1, 0x10); - Array.Copy(key2, DecryptedKey2, 0x10); + Array.Copy(key, 0, DecryptedKey1, 0, 0x10); + Array.Copy(key, 0x10, DecryptedKey2, 0, 0x10); Magic = AesXtsFileMagic; Size = fileSize; @@ -63,6 +63,12 @@ namespace LibHac.IO return Util.ArraysEqual(hmac, Signature); } + public void SetSize(long size, byte[] verificationKey) + { + Size = size; + Signature = CalculateHmac(verificationKey); + } + private void DecryptKeys() { Crypto.DecryptEcb(Kek1, EncryptedKey1, DecryptedKey1, 0x10); diff --git a/src/LibHac/IO/AesXtsFileSystem.cs b/src/LibHac/IO/AesXtsFileSystem.cs index e26953f7..d736e5cc 100644 --- a/src/LibHac/IO/AesXtsFileSystem.cs +++ b/src/LibHac/IO/AesXtsFileSystem.cs @@ -33,10 +33,23 @@ namespace LibHac.IO public void CreateFile(string path, long size, CreateFileOptions options) { - long containerSize = AesXtsFile.HeaderLength + Util.AlignUp(size, 0x16); + CreateFile(path, size, options, new byte[0x20]); + } + + /// + /// Creates a new using the provided key. + /// + /// The full path of the file to create. + /// The initial size of the created file. + /// Flags to control how the file is created. + /// Should usually be + /// The 256-bit key containing a 128-bit data key followed by a 128-bit tweak key. + public void CreateFile(string path, long size, CreateFileOptions options, byte[] key) + { + long containerSize = AesXtsFile.HeaderLength + Util.AlignUp(size, 0x10); BaseFileSystem.CreateFile(path, containerSize, options); - var header = new AesXtsFileHeader(new byte[0x10], new byte[0x10], size, path, KekSource, ValidationKey); + var header = new AesXtsFileHeader(key, size, path, KekSource, ValidationKey); using (IFile baseFile = BaseFileSystem.OpenFile(path, OpenMode.Write)) { diff --git a/src/LibHac/IO/ConcatenationStorage.cs b/src/LibHac/IO/ConcatenationStorage.cs index cc6e53ce..67ab58f9 100644 --- a/src/LibHac/IO/ConcatenationStorage.cs +++ b/src/LibHac/IO/ConcatenationStorage.cs @@ -29,18 +29,21 @@ namespace LibHac.IO long inPos = offset; int outPos = 0; int remaining = destination.Length; + int sourceIndex = FindSource(inPos); while (remaining > 0) { - ConcatSource entry = FindSource(inPos); - long sourcePos = inPos - entry.StartOffset; + ConcatSource entry = Sources[sourceIndex]; + long entryPos = inPos - entry.StartOffset; + long entryRemain = entry.StartOffset + entry.Size - inPos; - int bytesToRead = (int)Math.Min(entry.EndOffset - inPos, remaining); - entry.Storage.Read(destination.Slice(outPos, bytesToRead), sourcePos); + int bytesToRead = (int)Math.Min(entryRemain, remaining); + entry.Storage.Read(destination.Slice(outPos, bytesToRead), entryPos); outPos += bytesToRead; inPos += bytesToRead; remaining -= bytesToRead; + sourceIndex++; } } @@ -49,18 +52,21 @@ namespace LibHac.IO long inPos = offset; int outPos = 0; int remaining = source.Length; + int sourceIndex = FindSource(inPos); while (remaining > 0) { - ConcatSource storage = FindSource(inPos); - long sourcePos = inPos - storage.StartOffset; + ConcatSource entry = Sources[sourceIndex]; + long entryPos = inPos - entry.StartOffset; + long entryRemain = entry.StartOffset + entry.Size - inPos; - int bytesToWrite = (int)Math.Min(storage.EndOffset - inPos, remaining); - storage.Storage.Write(source.Slice(outPos, bytesToWrite), sourcePos); + int bytesToWrite = (int)Math.Min(entryRemain, remaining); + entry.Storage.Write(source.Slice(outPos, bytesToWrite), entryPos); outPos += bytesToWrite; inPos += bytesToWrite; remaining -= bytesToWrite; + sourceIndex++; } } @@ -72,27 +78,46 @@ namespace LibHac.IO } } - private ConcatSource FindSource(long offset) + private int FindSource(long offset) { - foreach (ConcatSource info in Sources) + if (offset < 0 || offset >= Length) + throw new ArgumentOutOfRangeException(nameof(offset), offset, "The Storage does not contain this offset."); + + int lo = 0; + int hi = Sources.Length - 1; + + while (lo <= hi) { - if (info.EndOffset > offset) return info; + int mid = lo + ((hi - lo) >> 1); + + long val = Sources[mid].StartOffset; + + if (val == offset) return mid; + + if (val < offset) + { + lo = mid + 1; + } + else + { + hi = mid - 1; + } } - throw new ArgumentOutOfRangeException(nameof(offset), offset, "The Storage does not contain this offset."); + return lo - 1; } private class ConcatSource { public IStorage Storage { get; } public long StartOffset { get; } - public long EndOffset { get; } + public long Size { get; } public ConcatSource(IStorage storage, long startOffset, long length) { Storage = storage; StartOffset = startOffset; - EndOffset = startOffset + length; + Size = length; } } } diff --git a/src/LibHac/IO/FileSystemExtensions.cs b/src/LibHac/IO/FileSystemExtensions.cs index 1c509f45..1fab500d 100644 --- a/src/LibHac/IO/FileSystemExtensions.cs +++ b/src/LibHac/IO/FileSystemExtensions.cs @@ -35,7 +35,7 @@ namespace LibHac.IO destFs.CreateFile(subDstPath, entry.Size, options); using (IFile srcFile = sourceFs.OpenFile(subSrcPath, OpenMode.Read)) - using (IFile dstFile = destFs.OpenFile(subDstPath, OpenMode.Write)) + using (IFile dstFile = destFs.OpenFile(subDstPath, OpenMode.Write | OpenMode.Append)) { logger?.LogMessage(subSrcPath); srcFile.CopyTo(dstFile, logger); @@ -61,7 +61,17 @@ namespace LibHac.IO public static IEnumerable EnumerateEntries(this IFileSystem fileSystem) { - return fileSystem.OpenDirectory("/", OpenDirectoryMode.All).EnumerateEntries("*", SearchOptions.RecurseSubdirectories); + return fileSystem.EnumerateEntries("*"); + } + + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string searchPattern) + { + return fileSystem.EnumerateEntries(searchPattern, SearchOptions.RecurseSubdirectories); + } + + public static IEnumerable EnumerateEntries(this IFileSystem fileSystem, string searchPattern, SearchOptions searchOptions) + { + return fileSystem.OpenDirectory("/", OpenDirectoryMode.All).EnumerateEntries(searchPattern, searchOptions); } public static IEnumerable EnumerateEntries(this IDirectory directory) diff --git a/src/LibHac/IO/LocalFileSystem.cs b/src/LibHac/IO/LocalFileSystem.cs index 0eabad86..c665698e 100644 --- a/src/LibHac/IO/LocalFileSystem.cs +++ b/src/LibHac/IO/LocalFileSystem.cs @@ -6,9 +6,19 @@ namespace LibHac.IO { private string BasePath { get; } + /// + /// Opens a directory on local storage as an . + /// The directory will be created if it does not exist. + /// + /// The path that will be the root of the . public LocalFileSystem(string basePath) { BasePath = Path.GetFullPath(basePath); + + if (!Directory.Exists(BasePath)) + { + Directory.CreateDirectory(BasePath); + } } internal string ResolveLocalPath(string path) diff --git a/src/LibHac/IO/NxFileStream.cs b/src/LibHac/IO/NxFileStream.cs index 895996a0..c0e10a2c 100644 --- a/src/LibHac/IO/NxFileStream.cs +++ b/src/LibHac/IO/NxFileStream.cs @@ -18,7 +18,7 @@ namespace LibHac.IO public override int Read(byte[] buffer, int offset, int count) { int toRead = (int)Math.Min(count, Length - Position); - BaseFile.Read(buffer.AsSpan(offset, count), Position); + BaseFile.Read(buffer.AsSpan(offset, toRead), Position); Position += toRead; return toRead; diff --git a/src/LibHac/IO/RomFs/HierarchicalRomFileTable.cs b/src/LibHac/IO/RomFs/HierarchicalRomFileTable.cs index d27c8e19..f163d230 100644 --- a/src/LibHac/IO/RomFs/HierarchicalRomFileTable.cs +++ b/src/LibHac/IO/RomFs/HierarchicalRomFileTable.cs @@ -207,6 +207,8 @@ namespace LibHac.IO.RomFs path = PathTools.Normalize(path); ReadOnlySpan pathBytes = GetUtf8Bytes(path); + if(path == "/") throw new ArgumentException("Path cannot be empty"); + CreateFileRecursiveInternal(pathBytes, ref fileInfo); } @@ -347,7 +349,7 @@ namespace LibHac.IO.RomFs { ref FileRomEntry entry = ref FileTable.AddOrGet(ref key, out int offset, out bool alreadyExists, out _); entry.Info = fileInfo; - if (alreadyExists) entry.NextSibling = -1; + if (!alreadyExists) entry.NextSibling = -1; ref DirectoryRomEntry parent = ref DirectoryTable.GetValueReference(prevOffset); diff --git a/src/LibHac/IO/RomFs/RomFsDictionary.cs b/src/LibHac/IO/RomFs/RomFsDictionary.cs index fd64c93c..6326ee4b 100644 --- a/src/LibHac/IO/RomFs/RomFsDictionary.cs +++ b/src/LibHac/IO/RomFs/RomFsDictionary.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace LibHac.IO.RomFs @@ -13,8 +14,7 @@ namespace LibHac.IO.RomFs private int[] Buckets { get; set; } private byte[] Entries { get; set; } - // Hack around not being able to get the size of generic structures - private readonly int _sizeOfEntry = 12 + Marshal.SizeOf(); + private readonly int _sizeOfEntry = Unsafe.SizeOf(); public RomFsDictionary(IStorage bucketStorage, IStorage entryStorage) { @@ -49,7 +49,7 @@ namespace LibHac.IO.RomFs public bool TryGetValue(int offset, out RomKeyValuePair value) { - if (offset < 0 || offset + _sizeOfEntry >= Entries.Length) + if (offset < 0 || offset + _sizeOfEntry > Entries.Length) { value = default; return false; @@ -68,13 +68,12 @@ namespace LibHac.IO.RomFs public ref T GetValueReference(int offset) { - ref RomFsEntry entry = ref MemoryMarshal.Cast(Entries.AsSpan(offset))[0]; - return ref entry.Value; + return ref Unsafe.As(ref Entries[offset]).Value; } public ref T GetValueReference(int offset, out Span name) { - ref RomFsEntry entry = ref MemoryMarshal.Cast(Entries.AsSpan(offset))[0]; + ref RomFsEntry entry = ref Unsafe.As(ref Entries[offset]); name = Entries.AsSpan(offset + _sizeOfEntry, entry.KeyLength); return ref entry.Value; @@ -249,7 +248,7 @@ namespace LibHac.IO.RomFs private ref RomFsEntry GetEntryReference(int offset, out Span name) { - ref RomFsEntry entry = ref MemoryMarshal.Cast(Entries.AsSpan(offset))[0]; + ref RomFsEntry entry = ref Unsafe.As(ref Entries[offset]); name = Entries.AsSpan(offset + _sizeOfEntry, entry.KeyLength); return ref entry; @@ -257,7 +256,7 @@ namespace LibHac.IO.RomFs private ref RomFsEntry GetEntryReference(int offset, out Span name, int nameLength) { - ref RomFsEntry entry = ref MemoryMarshal.Cast(Entries.AsSpan(offset))[0]; + ref RomFsEntry entry = ref Unsafe.As(ref Entries[offset]); name = Entries.AsSpan(offset + _sizeOfEntry, nameLength); return ref entry; diff --git a/src/LibHac/IO/StorageStream.cs b/src/LibHac/IO/StorageStream.cs index 7cd63045..c67fc152 100644 --- a/src/LibHac/IO/StorageStream.cs +++ b/src/LibHac/IO/StorageStream.cs @@ -21,7 +21,7 @@ namespace LibHac.IO public override int Read(byte[] buffer, int offset, int count) { int toRead = (int) Math.Min(count, Length - Position); - BaseStorage.Read(buffer.AsSpan(offset, count), Position); + BaseStorage.Read(buffer.AsSpan(offset, toRead), Position); Position += toRead; return toRead; diff --git a/src/LibHac/Keyset.cs b/src/LibHac/Keyset.cs index fd4999a2..9dbdb078 100644 --- a/src/LibHac/Keyset.cs +++ b/src/LibHac/Keyset.cs @@ -10,12 +10,20 @@ namespace LibHac { public class Keyset { + /// + /// The number of keyblobs that were used for < 6.2.0 crypto + /// + private const int UsedKeyblobCount = 6; + public byte[][] KeyblobKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); public byte[][] KeyblobMacKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); public byte[][] EncryptedKeyblobs { get; } = Util.CreateJaggedArray(0x20, 0xB0); public byte[][] Keyblobs { get; } = Util.CreateJaggedArray(0x20, 0x90); public byte[][] KeyblobKeySources { get; } = Util.CreateJaggedArray(0x20, 0x10); public byte[] KeyblobMacKeySource { get; } = new byte[0x10]; + public byte[][] TsecRootKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] MasterKekSources { get; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] MasterKeks { get; } = Util.CreateJaggedArray(0x20, 0x10); public byte[] MasterKeySource { get; } = new byte[0x10]; public byte[][] MasterKeys { get; } = Util.CreateJaggedArray(0x20, 0x10); public byte[][] Package1Keys { get; } = Util.CreateJaggedArray(0x20, 0x10); @@ -28,29 +36,31 @@ namespace LibHac public byte[] KeyAreaKeySystemSource { get; } = new byte[0x10]; public byte[] SaveMacKekSource { get; } = new byte[0x10]; public byte[] SaveMacKeySource { get; } = new byte[0x10]; - public byte[] TitlekekSource { get; } = new byte[0x10]; + public byte[] TitleKekSource { get; } = new byte[0x10]; public byte[] HeaderKekSource { get; } = new byte[0x10]; public byte[] SdCardKekSource { get; } = new byte[0x10]; public byte[][] SdCardKeySources { get; } = Util.CreateJaggedArray(2, 0x20); - public byte[][] SdCardKeySourcesSpecific { get; } = Util.CreateJaggedArray(2, 0x20); public byte[] HeaderKeySource { get; } = new byte[0x20]; public byte[] HeaderKey { get; } = new byte[0x20]; public byte[] XciHeaderKey { get; } = new byte[0x10]; - public byte[][] Titlekeks { get; } = Util.CreateJaggedArray(0x20, 0x10); + public byte[][] TitleKeks { get; } = Util.CreateJaggedArray(0x20, 0x10); public byte[][][] KeyAreaKeys { get; } = Util.CreateJaggedArray(0x20, 3, 0x10); - public byte[] SaveMacKey { get; } = new byte[0x10]; - public byte[][] SdCardKeys { get; } = Util.CreateJaggedArray(2, 0x20); public byte[] EticketRsaKek { get; } = new byte[0x10]; public byte[] RetailSpecificAesKeySource { get; } = new byte[0x10]; public byte[] PerConsoleKeySource { get; } = new byte[0x10]; public byte[] BisKekSource { get; } = new byte[0x10]; - public byte[][] BisKeySource { get; } = Util.CreateJaggedArray(3, 0x20); + public byte[][] BisKeySource { get; } = Util.CreateJaggedArray(4, 0x20); + public byte[] SslRsaKek { get; } = new byte[0x10]; + // Device-specific keys public byte[] SecureBootKey { get; } = new byte[0x10]; public byte[] TsecKey { get; } = new byte[0x10]; public byte[] DeviceKey { get; } = new byte[0x10]; public byte[][] BisKeys { get; } = Util.CreateJaggedArray(4, 0x20); + public byte[] SaveMacKey { get; } = new byte[0x10]; public byte[] SdSeed { get; } = new byte[0x10]; + public byte[][] SdCardKeySourcesSpecific { get; } = Util.CreateJaggedArray(2, 0x20); + public byte[][] SdCardKeys { get; } = Util.CreateJaggedArray(2, 0x20); public RSAParameters EticketExtKeyRsa { get; set; } @@ -128,6 +138,9 @@ namespace LibHac DecryptKeyblobs(logger); ReadKeyblobs(); + Derive620MasterKeks(); + DeriveMasterKeys(); + DerivePerConsoleKeys(); DerivePerFirmwareKeys(); DeriveNcaHeaderKey(); @@ -141,7 +154,7 @@ namespace LibHac bool haveKeyblobMacKeySource = !MasterKeySource.IsEmpty(); var temp = new byte[0x10]; - for (int i = 0; i < 0x20; i++) + for (int i = 0; i < UsedKeyblobCount; i++) { if (KeyblobKeySources[i].IsEmpty()) continue; @@ -160,7 +173,7 @@ namespace LibHac var expectedCmac = new byte[0x10]; var counter = new byte[0x10]; - for (int i = 0; i < 0x20; i++) + for (int i = 0; i < UsedKeyblobCount; i++) { if (KeyblobKeys[i].IsEmpty() || KeyblobMacKeys[i].IsEmpty() || EncryptedKeyblobs[i].IsEmpty()) { @@ -187,21 +200,34 @@ namespace LibHac private void ReadKeyblobs() { - var masterKek = new byte[0x10]; - - bool haveMasterKeySource = !MasterKeySource.IsEmpty(); - - for (int i = 0; i < 0x20; i++) + for (int i = 0; i < UsedKeyblobCount; i++) { if (Keyblobs[i].IsEmpty()) continue; Array.Copy(Keyblobs[i], 0x80, Package1Keys[i], 0, 0x10); + Array.Copy(Keyblobs[i], MasterKeks[i], 0x10); + } + } - if (!haveMasterKeySource) continue; + private void Derive620MasterKeks() + { + for (int i = UsedKeyblobCount; i < 0x20; i++) + { + if (TsecRootKeys[i - UsedKeyblobCount].IsEmpty() || MasterKekSources[i].IsEmpty()) continue; - Array.Copy(Keyblobs[i], masterKek, 0x10); + Crypto.DecryptEcb(TsecRootKeys[i - UsedKeyblobCount], MasterKekSources[i], MasterKeks[i], 0x10); + } + } - Crypto.DecryptEcb(masterKek, MasterKeySource, MasterKeys[i], 0x10); + private void DeriveMasterKeys() + { + if (MasterKeySource.IsEmpty()) return; + + for (int i = 0; i < 0x20; i++) + { + if (MasterKeks[i].IsEmpty()) continue; + + Crypto.DecryptEcb(MasterKeks[i], MasterKeySource, MasterKeys[i], 0x10); } } @@ -242,9 +268,7 @@ namespace LibHac Crypto.DecryptEcb(kek, BisKeySource[1], BisKeys[1], 0x20); Crypto.DecryptEcb(kek, BisKeySource[2], BisKeys[2], 0x20); - - // BIS keys 2 and 3 are the same - Array.Copy(BisKeys[2], BisKeys[3], 0x20); + Crypto.DecryptEcb(kek, BisKeySource[3], BisKeys[3], 0x20); } private void DerivePerFirmwareKeys() @@ -252,7 +276,7 @@ namespace LibHac bool haveKakSource0 = !KeyAreaKeyApplicationSource.IsEmpty(); bool haveKakSource1 = !KeyAreaKeyOceanSource.IsEmpty(); bool haveKakSource2 = !KeyAreaKeySystemSource.IsEmpty(); - bool haveTitleKekSource = !TitlekekSource.IsEmpty(); + bool haveTitleKekSource = !TitleKekSource.IsEmpty(); bool havePackage2KeySource = !Package2KeySource.IsEmpty(); for (int i = 0; i < 0x20; i++) @@ -282,7 +306,7 @@ namespace LibHac if (haveTitleKekSource) { - Crypto.DecryptEcb(MasterKeys[i], TitlekekSource, Titlekeks[i], 0x10); + Crypto.DecryptEcb(MasterKeys[i], TitleKekSource, TitleKeks[i], 0x10); } if (havePackage2KeySource) @@ -322,7 +346,7 @@ namespace LibHac } } - internal static readonly string[] KakNames = {"application", "ocean", "system"}; + internal static readonly string[] KakNames = { "application", "ocean", "system" }; } public static class ExternalKeys @@ -343,14 +367,19 @@ namespace LibHac AllKeyDict = uniqueKeys.Concat(commonKeys).ToDictionary(k => k.Name, k => k); } - public static Keyset ReadKeyFile(string filename, string titleKeysFilename = null, string consoleKeysFilename = null, IProgressReport logger = null) + public static void ReadKeyFile(Keyset keyset, string filename, string titleKeysFilename = null, string consoleKeysFilename = null, IProgressReport logger = null) { - var keyset = new Keyset(); - if (filename != null) ReadMainKeys(keyset, filename, AllKeyDict, logger); if (consoleKeysFilename != null) ReadMainKeys(keyset, consoleKeysFilename, AllKeyDict, logger); if (titleKeysFilename != null) ReadTitleKeys(keyset, titleKeysFilename, logger); keyset.DeriveKeys(logger); + } + + public static Keyset ReadKeyFile(string filename, string titleKeysFilename = null, string consoleKeysFilename = null, IProgressReport logger = null) + { + var keyset = new Keyset(); + + ReadKeyFile(keyset, filename, titleKeysFilename, consoleKeysFilename, logger); return keyset; } @@ -459,12 +488,19 @@ namespace LibHac var sb = new StringBuilder(); int maxNameLength = dict.Values.Max(x => x.Name.Length); + int currentGroup = 0; - foreach (KeyValue keySlot in dict.Values.OrderBy(x => x.Name)) + foreach (KeyValue keySlot in dict.Values.Where(x => x.Group >= 0).OrderBy(x => x.Group).ThenBy(x => x.Name)) { byte[] key = keySlot.GetKey(keyset); if (key.IsEmpty()) continue; + if (keySlot.Group > currentGroup) + { + if (currentGroup > 0) sb.AppendLine(); + currentGroup = keySlot.Group; + } + string line = $"{keySlot.Name.PadRight(maxNameLength)} = {key.ToHexString()}"; sb.AppendLine(line); } @@ -491,7 +527,7 @@ namespace LibHac { var sb = new StringBuilder(); - foreach (KeyValuePair kv in keyset.TitleKeys) + foreach (KeyValuePair kv in keyset.TitleKeys.OrderBy(x => x.Key.ToHexString())) { string line = $"{kv.Key.ToHexString()} = {kv.Value.ToHexString()}"; sb.AppendLine(line); @@ -504,49 +540,61 @@ namespace LibHac { var keys = new List { - new KeyValue("aes_kek_generation_source", 0x10, set => set.AesKekGenerationSource), - new KeyValue("aes_key_generation_source", 0x10, set => set.AesKeyGenerationSource), - new KeyValue("key_area_key_application_source", 0x10, set => set.KeyAreaKeyApplicationSource), - new KeyValue("key_area_key_ocean_source", 0x10, set => set.KeyAreaKeyOceanSource), - new KeyValue("key_area_key_system_source", 0x10, set => set.KeyAreaKeySystemSource), - new KeyValue("titlekek_source", 0x10, set => set.TitlekekSource), - new KeyValue("header_kek_source", 0x10, set => set.HeaderKekSource), - new KeyValue("header_key_source", 0x20, set => set.HeaderKeySource), - new KeyValue("header_key", 0x20, set => set.HeaderKey), - new KeyValue("xci_header_key", 0x10, set => set.XciHeaderKey), - new KeyValue("package2_key_source", 0x10, set => set.Package2KeySource), - new KeyValue("sd_card_kek_source", 0x10, set => set.SdCardKekSource), - new KeyValue("sd_card_nca_key_source", 0x20, set => set.SdCardKeySources[1]), - new KeyValue("sd_card_save_key_source", 0x20, set => set.SdCardKeySources[0]), - new KeyValue("master_key_source", 0x10, set => set.MasterKeySource), - new KeyValue("keyblob_mac_key_source", 0x10, set => set.KeyblobMacKeySource), - new KeyValue("eticket_rsa_kek", 0x10, set => set.EticketRsaKek), - new KeyValue("retail_specific_aes_key_source", 0x10, set => set.RetailSpecificAesKeySource), - new KeyValue("per_console_key_source", 0x10, set => set.PerConsoleKeySource), - new KeyValue("bis_kek_source", 0x10, set => set.BisKekSource), - new KeyValue("save_mac_kek_source", 0x10, set => set.SaveMacKekSource), - new KeyValue("save_mac_key_source", 0x10, set => set.SaveMacKeySource), - new KeyValue("save_mac_key", 0x10, set => set.SaveMacKey) + new KeyValue("keyblob_mac_key_source", 0x10, 0, set => set.KeyblobMacKeySource), + + new KeyValue("master_key_source", 0x10, 60, set => set.MasterKeySource), + new KeyValue("package2_key_source", 0x10, 60, set => set.Package2KeySource), + + new KeyValue("aes_kek_generation_source", 0x10, 70, set => set.AesKekGenerationSource), + new KeyValue("aes_key_generation_source", 0x10, 70, set => set.AesKeyGenerationSource), + + new KeyValue("bis_kek_source", 0x10, 80, set => set.BisKekSource), + + new KeyValue("retail_specific_aes_key_source", 0x10, 90, set => set.RetailSpecificAesKeySource), + new KeyValue("per_console_key_source", 0x10, 90, set => set.PerConsoleKeySource), + + new KeyValue("header_kek_source", 0x10, 100, set => set.HeaderKekSource), + new KeyValue("header_key_source", 0x20, 100, set => set.HeaderKeySource), + new KeyValue("key_area_key_application_source", 0x10, 100, set => set.KeyAreaKeyApplicationSource), + new KeyValue("key_area_key_ocean_source", 0x10, 100, set => set.KeyAreaKeyOceanSource), + new KeyValue("key_area_key_system_source", 0x10, 100, set => set.KeyAreaKeySystemSource), + new KeyValue("titlekek_source", 0x10, 100, set => set.TitleKekSource), + + new KeyValue("save_mac_kek_source", 0x10, 110, set => set.SaveMacKekSource), + new KeyValue("save_mac_key_source", 0x10, 110, set => set.SaveMacKeySource), + new KeyValue("sd_card_kek_source", 0x10, 110, set => set.SdCardKekSource), + new KeyValue("sd_card_nca_key_source", 0x20, 110, set => set.SdCardKeySources[1]), + new KeyValue("sd_card_save_key_source", 0x20, 110, set => set.SdCardKeySources[0]), + + new KeyValue("eticket_rsa_kek", 0x10, 120, set => set.EticketRsaKek), + new KeyValue("ssl_rsa_kek", 0x10, 120, set => set.SslRsaKek), + new KeyValue("xci_header_key", 0x10, 130, set => set.XciHeaderKey), + + new KeyValue("header_key", 0x20, 220, set => set.HeaderKey) }; for (int slot = 0; slot < 0x20; slot++) { int i = slot; - keys.Add(new KeyValue($"keyblob_key_source_{i:x2}", 0x10, set => set.KeyblobKeySources[i])); - keys.Add(new KeyValue($"keyblob_{i:x2}", 0x90, set => set.Keyblobs[i])); - keys.Add(new KeyValue($"master_key_{i:x2}", 0x10, set => set.MasterKeys[i])); - keys.Add(new KeyValue($"package1_key_{i:x2}", 0x10, set => set.Package1Keys[i])); - keys.Add(new KeyValue($"package2_key_{i:x2}", 0x10, set => set.Package2Keys[i])); - keys.Add(new KeyValue($"titlekek_{i:x2}", 0x10, set => set.Titlekeks[i])); - keys.Add(new KeyValue($"key_area_key_application_{i:x2}", 0x10, set => set.KeyAreaKeys[i][0])); - keys.Add(new KeyValue($"key_area_key_ocean_{i:x2}", 0x10, set => set.KeyAreaKeys[i][1])); - keys.Add(new KeyValue($"key_area_key_system_{i:x2}", 0x10, set => set.KeyAreaKeys[i][2])); + keys.Add(new KeyValue($"keyblob_key_source_{i:x2}", 0x10, 0, set => set.KeyblobKeySources[i])); + keys.Add(new KeyValue($"keyblob_{i:x2}", 0x90, 10, set => set.Keyblobs[i])); + keys.Add(new KeyValue($"tsec_root_key_{i:x2}", 0x10, 20, set => set.TsecRootKeys[i])); + keys.Add(new KeyValue($"master_kek_source_{i:x2}", 0x10, 30, set => set.MasterKekSources[i])); + keys.Add(new KeyValue($"master_kek_{i:x2}", 0x10, 40, set => set.MasterKeks[i])); + keys.Add(new KeyValue($"package1_key_{i:x2}", 0x10, 50, set => set.Package1Keys[i])); + + keys.Add(new KeyValue($"master_key_{i:x2}", 0x10, 200, set => set.MasterKeys[i])); + keys.Add(new KeyValue($"package2_key_{i:x2}", 0x10, 210, set => set.Package2Keys[i])); + keys.Add(new KeyValue($"titlekek_{i:x2}", 0x10, 230, set => set.TitleKeks[i])); + keys.Add(new KeyValue($"key_area_key_application_{i:x2}", 0x10, 240, set => set.KeyAreaKeys[i][0])); + keys.Add(new KeyValue($"key_area_key_ocean_{i:x2}", 0x10, 250, set => set.KeyAreaKeys[i][1])); + keys.Add(new KeyValue($"key_area_key_system_{i:x2}", 0x10, 260, set => set.KeyAreaKeys[i][2])); } - for (int slot = 0; slot < 3; slot++) + for (int slot = 0; slot < 4; slot++) { int i = slot; - keys.Add(new KeyValue($"bis_key_source_{i:x2}", 0x20, set => set.BisKeySource[i])); + keys.Add(new KeyValue($"bis_key_source_{i:x2}", 0x20, 80, set => set.BisKeySource[i])); } return keys; @@ -556,24 +604,26 @@ namespace LibHac { var keys = new List { - new KeyValue("secure_boot_key", 0x10, set => set.SecureBootKey), - new KeyValue("tsec_key", 0x10, set => set.TsecKey), - new KeyValue("device_key", 0x10, set => set.DeviceKey), - new KeyValue("sd_seed", 0x10, set => set.SdSeed) + new KeyValue("secure_boot_key", 0x10, 0, set => set.SecureBootKey), + new KeyValue("tsec_key", 0x10, 0, set => set.TsecKey), + new KeyValue("sd_seed", 0x10, 10, set => set.SdSeed), + + new KeyValue("device_key", 0x10, 40, set => set.DeviceKey), + new KeyValue("save_mac_key", 0x10, 60, set => set.SaveMacKey) }; for (int slot = 0; slot < 0x20; slot++) { int i = slot; - keys.Add(new KeyValue($"keyblob_key_{i:x2}", 0x10, set => set.KeyblobKeys[i])); - keys.Add(new KeyValue($"keyblob_mac_key_{i:x2}", 0x10, set => set.KeyblobMacKeys[i])); - keys.Add(new KeyValue($"encrypted_keyblob_{i:x2}", 0xB0, set => set.EncryptedKeyblobs[i])); + keys.Add(new KeyValue($"keyblob_mac_key_{i:x2}", 0x10, 20, set => set.KeyblobMacKeys[i])); + keys.Add(new KeyValue($"keyblob_key_{i:x2}", 0x10, 30, set => set.KeyblobKeys[i])); + keys.Add(new KeyValue($"encrypted_keyblob_{i:x2}", 0xB0, 100, set => set.EncryptedKeyblobs[i])); } for (int slot = 0; slot < 4; slot++) { int i = slot; - keys.Add(new KeyValue($"bis_key_{i:x2}", 0x20, set => set.BisKeys[i])); + keys.Add(new KeyValue($"bis_key_{i:x2}", 0x20, 50, set => set.BisKeys[i])); } return keys; @@ -583,12 +633,14 @@ namespace LibHac { public readonly string Name; public readonly int Size; + public readonly int Group; public readonly Func GetKey; - public KeyValue(string name, int size, Func retrieveFunc) + public KeyValue(string name, int size, int group, Func retrieveFunc) { Name = name; Size = size; + Group = group; GetKey = retrieveFunc; } } diff --git a/src/LibHac/LibHac.csproj b/src/LibHac/LibHac.csproj index 3d2e1e92..8f04d210 100644 --- a/src/LibHac/LibHac.csproj +++ b/src/LibHac/LibHac.csproj @@ -16,11 +16,12 @@ git https://github.com/Thealexbarney/LibHac - 0.2.0 + 0.3.1 $(MSBuildProjectDirectory)=C:/LibHac/ true + snupkg true - $(NoWarn);1591 + $(NoWarn);1591;NU5105 true @@ -34,8 +35,12 @@ - + + + + + diff --git a/src/LibHac/Nca.cs b/src/LibHac/Nca.cs index 6a0baf66..341ee4c1 100644 --- a/src/LibHac/Nca.cs +++ b/src/LibHac/Nca.cs @@ -48,13 +48,13 @@ namespace LibHac } else if (keyset.TitleKeys.TryGetValue(Header.RightsId, out byte[] titleKey)) { - if (keyset.Titlekeks[CryptoType].IsEmpty()) + if (keyset.TitleKeks[CryptoType].IsEmpty()) { MissingKeyName = $"titlekek_{CryptoType:x2}"; } TitleKey = titleKey; - Crypto.DecryptEcb(keyset.Titlekeks[CryptoType], titleKey, TitleKeyDec, 0x10); + Crypto.DecryptEcb(keyset.TitleKeks[CryptoType], titleKey, TitleKeyDec, 0x10); DecryptedKeys[2] = TitleKeyDec; } else diff --git a/src/LibHac/NcaStructs.cs b/src/LibHac/NcaStructs.cs index 7f059f76..07563588 100644 --- a/src/LibHac/NcaStructs.cs +++ b/src/LibHac/NcaStructs.cs @@ -268,13 +268,13 @@ namespace LibHac Control, Manual, Data, - AocData + PublicData } public enum DistributionType { Download, - Gamecard + GameCard } public enum NcaEncryptionType diff --git a/src/LibHac/Util.cs b/src/LibHac/Util.cs index 5a93e623..cb2a3329 100644 --- a/src/LibHac/Util.cs +++ b/src/LibHac/Util.cs @@ -424,6 +424,8 @@ namespace LibHac case 3: return "4.0.0-4.1.0"; case 4: return "5.0.0-5.1.0"; case 5: return "6.0.0-6.0.1"; + case 6: return "6.2.0"; + case 7: return "7.0.0"; default: return "Unknown"; } } diff --git a/src/hactoolnet/hactoolnet.csproj b/src/hactoolnet/hactoolnet.csproj index 63deeb66..91d5cf7c 100644 --- a/src/hactoolnet/hactoolnet.csproj +++ b/src/hactoolnet/hactoolnet.csproj @@ -7,7 +7,7 @@ - 0.2.0 + 0.3.1 $(MSBuildProjectDirectory)=C:/hactoolnet/ diff --git a/tests/LibHac.Tests/RomFsTests.cs b/tests/LibHac.Tests/RomFsTests.cs new file mode 100644 index 00000000..dbbd5ec0 --- /dev/null +++ b/tests/LibHac.Tests/RomFsTests.cs @@ -0,0 +1,204 @@ +using System; +using LibHac.IO.RomFs; +using Xunit; + +namespace LibHac.Tests +{ + public class RomFsTests + { + [Fact] + public void SimpleAddAndRead() + { + const string path = "/a/b"; + + var table = new HierarchicalRomFileTable(); + var item = new RomFileInfo { Length = 1, Offset = 1 }; + + table.AddFile(path, ref item); + bool success = table.TryOpenFile(path, out RomFileInfo readItem); + + Assert.True(success, "Table read failed"); + Assert.Equal(item, readItem); + } + + [Fact] + public void UpdateExistingFile() + { + const string path = "/a/b"; + + var table = new HierarchicalRomFileTable(); + var originalItem = new RomFileInfo { Length = 1, Offset = 1 }; + var newItem = new RomFileInfo { Length = 1, Offset = 1 }; + + table.AddFile(path, ref originalItem); + table.AddFile(path, ref newItem); + + bool success = table.TryOpenFile(path, out RomFileInfo readItem); + + Assert.True(success, "Table read failed"); + Assert.Equal(newItem, readItem); + } + + [Fact] + public void AddingDirectory() + { + var table = new HierarchicalRomFileTable(); + var expectedPosition = new FindPosition { NextDirectory = -1, NextFile = -1 }; + + table.AddDirectory("/dir"); + bool success = table.TryOpenDirectory("/dir", out FindPosition position); + + Assert.True(success, "Opening directory failed"); + Assert.Equal(expectedPosition, position); + } + + [Fact] + public void AddingEmptyPathThrows() + { + var table = new HierarchicalRomFileTable(); + var item = new RomFileInfo(); + + Assert.Throws(() => table.AddFile("", ref item)); + } + + [Fact] + public void OpeningNonexistentFileFails() + { + var table = new HierarchicalRomFileTable(); + + bool success = table.TryOpenFile("/foo", out _); + Assert.False(success); + } + + [Fact] + public void OpeningNonexistentDirectoryFails() + { + var table = new HierarchicalRomFileTable(); + + bool success = table.TryOpenDirectory("/foo", out _); + Assert.False(success); + } + + [Fact] + public void OpeningFileAsDirectoryFails() + { + var table = new HierarchicalRomFileTable(); + var fileInfo = new RomFileInfo(); + table.AddFile("/file", ref fileInfo); + + bool success = table.TryOpenDirectory("/file", out _); + Assert.False(success); + } + + [Fact] + public void OpeningDirectoryAsFileFails() + { + var table = new HierarchicalRomFileTable(); + table.AddDirectory("/dir"); + + bool success = table.TryOpenFile("/dir", out _); + Assert.False(success); + } + + [Fact] + public void ChildFileIteration() + { + const int fileCount = 10; + var table = new HierarchicalRomFileTable(); + + for (int i = 0; i < fileCount; i++) + { + var item = new RomFileInfo { Length = i, Offset = i }; + table.AddFile($"/a/{i}", ref item); + } + + bool openDirSuccess = table.TryOpenDirectory("/a", out FindPosition position); + Assert.True(openDirSuccess, "Error opening directory"); + + for (int i = 0; i < fileCount; i++) + { + var expectedItem = new RomFileInfo { Length = i, Offset = i }; + string expectedName = i.ToString(); + + bool success = table.FindNextFile(ref position, out RomFileInfo actualItem, out string actualName); + + Assert.True(success, $"Failed reading file {i}"); + Assert.Equal(expectedItem, actualItem); + Assert.Equal(expectedName, actualName); + } + + bool endOfFilesSuccess = table.FindNextFile(ref position, out _, out _); + Assert.False(endOfFilesSuccess, "Table returned more files than it should"); + } + + [Fact] + public void ChildFileIterationPeek() + { + var table = new HierarchicalRomFileTable(); + + var itemA = new RomFileInfo { Length = 1, Offset = 1 }; + var itemB = new RomFileInfo { Length = 2, Offset = 2 }; + + table.AddFile("/a/a", ref itemA); + table.AddFile("/a/b", ref itemB); + + table.TryOpenDirectory("/a", out FindPosition position); + + table.TryOpenFile(position.NextFile, out RomFileInfo peekItemA); + Assert.Equal(itemA, peekItemA); + + table.FindNextFile(ref position, out RomFileInfo iterateItemA, out _); + Assert.Equal(itemA, iterateItemA); + + table.TryOpenFile(position.NextFile, out RomFileInfo peekItemB); + Assert.Equal(itemB, peekItemB); + + table.FindNextFile(ref position, out RomFileInfo iterateItemB, out _); + Assert.Equal(itemB, iterateItemB); + } + + [Fact] + public void AddingCousinFiles() + { + var table = new HierarchicalRomFileTable(); + + var itemB1 = new RomFileInfo { Length = 1, Offset = 1 }; + var itemB2 = new RomFileInfo { Length = 2, Offset = 2 }; + var itemB3 = new RomFileInfo { Length = 3, Offset = 3 }; + + table.AddFile("/a/b1/c", ref itemB1); + table.AddFile("/a/b2/c", ref itemB2); + table.AddFile("/a/b3/c", ref itemB3); + + table.TryOpenFile("/a/b1/c", out RomFileInfo actualItemB1); + table.TryOpenFile("/a/b2/c", out RomFileInfo actualItemB2); + table.TryOpenFile("/a/b3/c", out RomFileInfo actualItemB3); + + Assert.Equal(itemB1, actualItemB1); + Assert.Equal(itemB2, actualItemB2); + Assert.Equal(itemB3, actualItemB3); + } + + [Fact] + public void AddingSiblingFiles() + { + var table = new HierarchicalRomFileTable(); + + var itemC1 = new RomFileInfo { Length = 1, Offset = 1 }; + var itemC2 = new RomFileInfo { Length = 2, Offset = 2 }; + var itemC3 = new RomFileInfo { Length = 3, Offset = 3 }; + + table.AddFile("/a/b/c1", ref itemC1); + table.AddFile("/a/b/c2", ref itemC2); + table.AddFile("/a/b/c3", ref itemC3); + + table.TryOpenFile("/a/b/c1", out RomFileInfo actualItemC1); + table.TryOpenFile("/a/b/c2", out RomFileInfo actualItemC2); + table.TryOpenFile("/a/b/c3", out RomFileInfo actualItemC3); + + Assert.Equal(itemC1, actualItemC1); + Assert.Equal(itemC2, actualItemC2); + Assert.Equal(itemC3, actualItemC3); + } + } +}