diff --git a/LibHac/Aes128CtrStream.cs b/LibHac/Aes128CtrStream.cs index 10402f89..073becb2 100644 --- a/LibHac/Aes128CtrStream.cs +++ b/LibHac/Aes128CtrStream.cs @@ -14,6 +14,15 @@ namespace LibHac private readonly Aes128CtrTransform _decryptor; protected readonly byte[] Counter; + /// + /// Creates a new stream + /// + /// The base stream + /// The decryption key + /// The initial counter + public Aes128CtrStream(Stream baseStream, byte[] key, byte[] counter) + : this(baseStream, key, 0, baseStream.Length, counter) { } + /// /// Creates a new stream /// @@ -29,9 +38,11 @@ namespace LibHac /// /// The base stream /// The decryption key + /// Offset to start at in the input stream + /// The length of the created stream /// The initial counter - public Aes128CtrStream(Stream baseStream, byte[] key, byte[] counter) - : base(baseStream, BlockSize) + public Aes128CtrStream(Stream baseStream, byte[] key, long offset, long length, byte[] counter) + : base(baseStream, BlockSize, 1, offset) { _counterOffset = 0; @@ -44,13 +55,11 @@ namespace LibHac } } - Length = baseStream.Length; + Length = length; _tempBuffer = new byte[CryptChunkSize]; _decryptor = new Aes128CtrTransform(key, counter ?? new byte[0x10], CryptChunkSize); Counter = _decryptor.Counter; - - baseStream.Position = 0; } /// diff --git a/LibHac/Aes128CtrTransform.cs b/LibHac/Aes128CtrTransform.cs index 7ee220a1..38db3ada 100644 --- a/LibHac/Aes128CtrTransform.cs +++ b/LibHac/Aes128CtrTransform.cs @@ -63,11 +63,7 @@ namespace LibHac private void IncrementCounter() { - for (int i = Counter.Length - 1; i >= 0; i--) - { - if (++Counter[i] != 0) - break; - } + Util.IncrementByteArray(Counter); } private void XorArrays(byte[] inputBuffer, int inputOffset, byte[] outputBuffer, int outputOffset, byte[] xor, int length) diff --git a/LibHac/Package2.cs b/LibHac/Package2.cs new file mode 100644 index 00000000..0f6a3c1f --- /dev/null +++ b/LibHac/Package2.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using LibHac.Streams; + +namespace LibHac +{ + public class Package2 + { + private const uint Pk21Magic = 0x31324B50; // PK21 + + public Package2Header Header { get; } + public int KeyRevision { get; } + public byte[] Key { get; } + public int PackageSize { get; } + public int HeaderVersion { get; } + + private SharedStreamSource StreamSource { get; } + + public Package2(Keyset keyset, Stream stream) + { + StreamSource = new SharedStreamSource(stream); + SharedStream headerStream = StreamSource.CreateStream(0, 0x200); + + KeyRevision = FindKeyGeneration(keyset, headerStream); + Key = keyset.Package2Keys[KeyRevision]; + + Header = new Package2Header(headerStream, Key); + + PackageSize = BitConverter.ToInt32(Header.Counter, 0) ^ BitConverter.ToInt32(Header.Counter, 8) ^ + BitConverter.ToInt32(Header.Counter, 12); + + HeaderVersion = Header.Counter[4] ^ Header.Counter[6] ^ Header.Counter[7]; + + if (PackageSize != 0x200 + Header.SectionSizes[0] + Header.SectionSizes[1] + Header.SectionSizes[2]) + { + throw new InvalidDataException("Package2 Header is corrupt!"); + } + } + + public Stream OpenHeaderPart1() + { + return StreamSource.CreateStream(0, 0x110); + } + + public Stream OpenHeaderPart2() + { + SharedStream encStream = StreamSource.CreateStream(0x110, 0xF0); + + var counter = new byte[0x10]; + Array.Copy(Header.Counter, counter, 0x10); + Util.IncrementByteArray(counter); + + return new RandomAccessSectorStream(new Aes128CtrStream(encStream, Key, counter)); + } + + public Stream OpenKernel() + { + int offset = 0x200; + SharedStream encStream = StreamSource.CreateStream(offset, Header.SectionSizes[0]); + + return new RandomAccessSectorStream(new Aes128CtrStream(encStream, Key, Header.SectionCounters[0])); + } + + public Stream OpenIni1() + { + int offset = 0x200 + Header.SectionSizes[0]; + SharedStream encStream = StreamSource.CreateStream(offset, Header.SectionSizes[1]); + + return new RandomAccessSectorStream(new Aes128CtrStream(encStream, Key, Header.SectionCounters[1])); + } + + private int FindKeyGeneration(Keyset keyset, Stream stream) + { + var counter = new byte[0x10]; + var decBuffer = new byte[0x10]; + + stream.Position = 0x100; + stream.Read(counter, 0, 0x10); + + for (int i = 0; i < 0x20; i++) + { + var dec = new Aes128CtrStream(stream, keyset.Package2Keys[i], 0x100, 0x100, counter); + dec.Position = 0x50; + dec.Read(decBuffer, 0, 0x10); + + if (BitConverter.ToUInt32(decBuffer, 0) == Pk21Magic) + { + stream.Position = 0; + return i; + } + } + + throw new InvalidDataException("Failed to decrypt package2! Is the correct key present?"); + } + } + + public class Package2Header + { + public byte[] Signature { get; } + public byte[] Counter { get; } + + public byte[][] SectionCounters { get; } = new byte[4][]; + public int[] SectionSizes { get; } = new int[4]; + public int[] SectionOffsets { get; } = new int[4]; + public byte[][] SectionHashes { get; } = new byte[4][]; + + public string Magic { get; } + public int BaseOffset { get; } + public int VersionMax { get; } + public int VersionMin { get; } + + public Package2Header(Stream stream, byte[] key) + { + var reader = new BinaryReader(stream); + + Signature = reader.ReadBytes(0x100); + Counter = reader.ReadBytes(0x10); + + var headerStream = new RandomAccessSectorStream(new Aes128CtrStream(stream, key, 0x100, 0x100, Counter)); + + headerStream.Position = 0x10; + reader = new BinaryReader(headerStream); + + for (int i = 0; i < 4; i++) + { + SectionCounters[i] = reader.ReadBytes(0x10); + } + + Magic = reader.ReadAscii(4); + BaseOffset = reader.ReadInt32(); + + reader.BaseStream.Position += 4; + VersionMax = reader.ReadByte(); + VersionMin = reader.ReadByte(); + + reader.BaseStream.Position += 2; + + for (int i = 0; i < 4; i++) + { + SectionSizes[i] = reader.ReadInt32(); + } + + for (int i = 0; i < 4; i++) + { + SectionOffsets[i] = reader.ReadInt32(); + } + + for (int i = 0; i < 4; i++) + { + SectionHashes[i] = reader.ReadBytes(0x20); + } + } + } +} diff --git a/LibHac/Streams/SectorStream.cs b/LibHac/Streams/SectorStream.cs index b26adf7f..80ac740c 100644 --- a/LibHac/Streams/SectorStream.cs +++ b/LibHac/Streams/SectorStream.cs @@ -63,6 +63,7 @@ namespace LibHac.Streams _offset = offset; _keepOpen = keepOpen; _maxBufferSize = MaxSectors * SectorSize; + baseStream.Position = offset; } public override void Flush() diff --git a/LibHac/Util.cs b/LibHac/Util.cs index fec22b42..3b51a33c 100644 --- a/LibHac/Util.cs +++ b/LibHac/Util.cs @@ -322,6 +322,15 @@ namespace LibHac public static int AlignDown(int value, int multiple) => value - value % multiple; public static long AlignDown(long value, long multiple) => value - value % multiple; + public static void IncrementByteArray(byte[] array) + { + for (int i = array.Length - 1; i >= 0; i--) + { + if (++array[i] != 0) + break; + } + } + public static void MemDump(this StringBuilder sb, string prefix, byte[] data) { @@ -407,5 +416,6 @@ namespace LibHac return (hi.GetHashCode() * 397) ^ lo.GetHashCode(); } + } } diff --git a/README.md b/README.md index a267035f..8c54efae 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Options: -r, --raw Keep raw data, don't unpack. -y, --verify Verify hashes. -k, --keyset Load keys from an external file. - -t, --intype=type Specify input file type [nca, xci, romfs, pk11, switchfs, save, keygen] + -t, --intype=type Specify input file type [nca, xci, romfs, pk11, pk21, switchfs, save, keygen] --titlekeys Load title keys from an external file. NCA options: --section0 Specify Section 0 file path. @@ -51,6 +51,8 @@ XCI options: --nspout Specify file for the created NSP. Package1 options: --outdir Specify Package1 directory path. +Package2 options: + --outdir Specify Package2 directory path. Switch FS options: --sdseed Set console unique seed for SD card NAX0 encryption. --listapps List application info. diff --git a/hactoolnet/CliParser.cs b/hactoolnet/CliParser.cs index b92b37ba..11e62bef 100644 --- a/hactoolnet/CliParser.cs +++ b/hactoolnet/CliParser.cs @@ -147,7 +147,7 @@ namespace hactoolnet sb.AppendLine(" -r, --raw Keep raw data, don\'t unpack."); sb.AppendLine(" -y, --verify Verify hashes."); sb.AppendLine(" -k, --keyset Load keys from an external file."); - sb.AppendLine(" -t, --intype=type Specify input file type [nca, xci, romfs, pk11, switchfs, save, keygen]"); + sb.AppendLine(" -t, --intype=type Specify input file type [nca, xci, romfs, pk11, pk21, switchfs, save, keygen]"); sb.AppendLine(" --titlekeys Load title keys from an external file."); sb.AppendLine("NCA options:"); sb.AppendLine(" --section0 Specify Section 0 file path."); @@ -181,6 +181,8 @@ namespace hactoolnet sb.AppendLine(" --nspout Specify file for the created NSP."); sb.AppendLine("Package1 options:"); sb.AppendLine(" --outdir Specify Package1 directory path."); + sb.AppendLine("Package2 options:"); + sb.AppendLine(" --outdir Specify Package2 directory path."); sb.AppendLine("Switch FS options:"); sb.AppendLine(" --sdseed Set console unique seed for SD card NAX0 encryption."); sb.AppendLine(" --listapps List application info."); diff --git a/hactoolnet/Options.cs b/hactoolnet/Options.cs index 2b051435..7e8ba384 100644 --- a/hactoolnet/Options.cs +++ b/hactoolnet/Options.cs @@ -47,7 +47,8 @@ namespace hactoolnet SwitchFs, Save, Keygen, - Pk11 + Pk11, + Pk21 } internal class Context diff --git a/hactoolnet/ProcessPackage.cs b/hactoolnet/ProcessPackage.cs index ca696472..7d15394d 100644 --- a/hactoolnet/ProcessPackage.cs +++ b/hactoolnet/ProcessPackage.cs @@ -1,5 +1,7 @@ using System.IO; +using System.Text; using LibHac; +using static hactoolnet.Print; namespace hactoolnet { @@ -28,5 +30,58 @@ namespace hactoolnet } } } + + public static void ProcessPk21(Context ctx) + { + using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read)) + { + var package2 = new Package2(ctx.Keyset, file); + + ctx.Logger.LogMessage(package2.Print()); + + string outDir = ctx.Options.OutDir; + + if (outDir != null) + { + Directory.CreateDirectory(outDir); + + package2.OpenKernel().WriteAllBytes(Path.Combine(outDir, "Kernel.bin"), ctx.Logger); + package2.OpenIni1().WriteAllBytes(Path.Combine(outDir, "INI1.bin"), ctx.Logger); + + using (var decFile = new FileStream(Path.Combine(outDir, "Decrypted.bin"), FileMode.Create)) + { + package2.OpenHeaderPart1().CopyTo(decFile); + package2.OpenHeaderPart2().CopyTo(decFile); + package2.OpenKernel().CopyTo(decFile); + package2.OpenIni1().CopyTo(decFile); + } + } + } + } + + private static readonly string[] Package2SectionNames = { "Kernel", "INI1", "Empty" }; + + private static string Print(this Package2 package2) + { + int colLen = 36; + var sb = new StringBuilder(); + sb.AppendLine(); + + sb.AppendLine("PK21:"); + PrintItem(sb, colLen, "Signature:", package2.Header.Signature); + PrintItem(sb, colLen, "Header Version:", $"{package2.HeaderVersion:x2}"); + + for (int i = 0; i < 3; i++) + { + sb.AppendLine($"Section {i} ({Package2SectionNames[i]}):"); + + PrintItem(sb, colLen, " Hash:", package2.Header.SectionHashes[i]); + PrintItem(sb, colLen, " CTR:", package2.Header.SectionCounters[i]); + PrintItem(sb, colLen, " Load Address:", $"{package2.Header.SectionOffsets[i] + 0x80000000:x8}"); + PrintItem(sb, colLen, " Size:", $"{package2.Header.SectionSizes[i]:x8}"); + } + + return sb.ToString(); + } } } diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs index 6fe5f10b..0340636c 100644 --- a/hactoolnet/Program.cs +++ b/hactoolnet/Program.cs @@ -53,6 +53,9 @@ namespace hactoolnet case FileType.Pk11: ProcessPackage.ProcessPk11(ctx); break; + case FileType.Pk21: + ProcessPackage.ProcessPk21(ctx); + break; default: throw new ArgumentOutOfRangeException(); }